個高水平的研發團對,無論使用什么框架、什么工具、什么語言,團隊里應該有人有能力把控所使用框架、工具、語言的每一個核心功能的實現細節。團隊里的每個成員應該根據自身所長挑選其中一塊做深入研究,并把研究成果分享給團隊,力爭使整個所處團隊實力得到提升,達到同行業內頂尖水平。為了實現這個目標,不允許在團隊中出現黑盒子,對.NET生態而言,我們需要打開MSBuild, Rosyln, CoreCLR等黑盒子。我作為團隊的一員,將花一些業余時間打開MySQL .NET驅動黑盒子,使用.NET Core實現MySQL Client/Server PRotocol,并把打開這個黑盒子的過程通過本站點記錄下來,包括知識儲備、實現思路,框架代碼等。
這是使用.NET Core實現MySQL協議系列文章的第一篇。
套接字(Socket)是通信的基石,是支持TCP/IP協議的網絡通信的基本操作單元,但Socket跟TCP/IP并沒有必然的聯系,TCP/IP只是一個協議棧,就像操作系統的運行機制一樣,必須要具體實現,同時還要提供對外的操作接口,TCP/IP也必須對外提供編程接口——Berkeley sockets interface.,它是網絡通信過程中端點的抽象表示,包含進行網絡通信必須的五種信息:
連接使用的協議;本地主機的IP地址;本地進程的協議端口;遠地主機的IP地址;遠地進程的協議端口;我們知道TCP建立連接要進行三次握手,大致流程如下:
客戶端向服務器發送一個SYN x服務器向客戶端響應一個SYN y,并對SYN x進行確認ACK x+1客戶端再向服務器發一個確認ACK y+1TCP斷開連接要進行四次揮手,大致流程如下:
應用進程首先調用close主動關閉連接,這時TCP發送一個FIN x另一端接收到FIN x之后,執行被動關閉,對這個FIN進行確認一段時間之后,應用進程調用close關閉它的socket,這導致它的TCP也發送一個FIN y接收到這個FIN的源發送端TCP對它進行確認這樣每個方向上都有一個FIN和ACK

MySQL客戶端與服務器的交互主要分為兩個階段
握手認證階段;命令執行階段;握手認證階段為客戶端與服務器通過TCP三次握手建立連接后進行,交互過程如下
服務器 -> 客戶端:握手初始化消息客戶端 -> 服務器:登陸認證消息服務器 -> 客戶端:認證結果消息客戶端認證成功后,會進入命令執行階段,交互過程如下
客戶端 -> 服務器:執行命令消息服務器 -> 客戶端:命令執行結果MySQL客戶端與服務器的完整交互過程如下

MySQL官網鏈接
這里的信息很重要,能不能理解后續代碼,這里是關鍵點之一,務必銘記于心。
這里的信息很重要,能不能理解后續代碼,這里是關鍵點之一,務必銘記于心。
這里的信息很重要,能不能理解后續代碼,這里是關鍵點之一,務必銘記于心。
重要的事情說三遍。
MySQL報文中整型值分別有1、2、3、4、8字節長度,使用小字節序傳輸。 注意: 字節序
字符串長度不固定,當遇到'NULL'(0x00)字符時結束。
數據長度不固定,長度值由數據前的1-9個字節決定,其中長度值所占的字節數不定,字節數由第1個字節決定,如下表:
| 第一個字節值 | 后續字節數 | 長度值說明 |
|---|---|---|
| 0-250 | 0 | 第一個字節值即為數據的真實長度 |
| 251 | 0 | 空數據,數據的真實長度為零 |
| 252 | 2 | 后續額外2個字節標識了數據的真實長度 |
| 253 | 3 | 后續額外3個字節標識了數據的真實長度 |
| 254 | 8 | 后續額外8個字節標識了數據的真實長度 |
字符串長度不固定,無'NULL'(0x00)結束符,編碼方式與上面的Length Coded Binary相同。
報文分為消息頭和消息體兩部分,其中消息頭占用固定的4個字節,消息體長度由消息頭中的長度字段決定,報文結構如下:

用于標記當前請求消息的實際數據長度值,以字節為單位,占用3個字節,最大值為 0xFFFFFF,即接近 16 MB 大小(比16MB少1個字節)。
在一次完整的請求/響應交互過程中,用于保證消息順序的正確,每次客戶端發起請求時,序號值都會從0開始計算。
消息體用于存放請求的內容及響應的數據,長度由消息頭中的長度值決定。
使用.NET Core實現MySQL協議,是一個C#類型與MySQL協議內容雙向Map的過程:
把MySQL協議報文包拆解成C#類型;把C#類型封裝成MySQL協議報文包;這篇文章,我們先實現部分把MySQL協議報文包拆解成C#類型相關代碼,Socket會把MySQL報文包以buffer(byte[])的形式返回給我們,具體C#代碼我們留到下一篇文章。通過把buffer(byte[])一片一片地按照MySQL協議切下來(記錄已經被切掉的位置offset以及buffer的最大位置maxOffset),再把切片組裝成C#類型,代碼如下:
數據長度不固定,長度值由數據前的1-9個字節決定,其中長度值所占的字節數不定,字節數由第1個字節決定,如下表:
| 第一個字節值 | 后續字節數 | 長度值說明 |
|---|---|---|
| 0-250 | 0 | 第一個字節值即為數據的真實長度 |
| 251 | 0 | 空數據,數據的真實長度為零 |
| 252 | 2 | 后續額外2個字節標識了數據的真實長度 |
| 253 | 3 | 后續額外3個字節標識了數據的真實長度 |
| 254 | 8 | 后續額外8個字節標識了數據的真實長度 |
從上表可以看出,報文的最大長度是8個字節,8字節 * 每字節8bit = 64bit, 結合C#數據類型,能表示8個字節的基礎數據類是是Int64、UInt64,因為報文長度不可能為負,所以使用UInt64, 可以使用以下代碼實現Protocol::LengthEncodedInteger
public UInt64 ReadLengthEncodedInteger(){ // https://dev.mysql.com/doc/internals/en/integer.html byte encodedLength = buffer[offset++]; switch (encodedLength) { case 0xFC: return ReadFixedLengthUInt32(readByteCount: 2); case 0xFD: return ReadFixedLengthUInt32(readByteCount: 3); case 0xFE: return ReadFixedLengthUInt64(readByteCount: 8); case 0xFF: throw new FormatException("Length-encoded integer cannot have 0xFF prefix byte."); default: return encodedLength; }}public uint ReadFixedLengthUInt32(int readByteCount){ if (readByteCount <= 0 || readByteCount > 4) throw new ArgumentOutOfRangeException(nameof(readByteCount)); uint result = 0; for (int i = 0; i < readByteCount; i++) result |= ((uint)buffer[offset + i]) << (8 * i); offset += readByteCount; return result;}public ulong ReadFixedLengthUInt64(int readByteCount){ if (readByteCount <= 0 || readByteCount > 8) throw new ArgumentOutOfRangeException(nameof(readByteCount)); ulong result = 0; for (int i = 0; i < readByteCount; i++) result |= ((ulong)buffer[offset + i]) << (8 * i); offset += readByteCount; return result;}讀取整型值
MySQL報文中整型值分別有1、2、3、4、8字節長度,使用小字節序傳輸(注意上面代碼位移操作符)。
public byte ReadByte(){ return buffer[offset++];}public void ReadByte(byte value){ if (ReadByte() != value) throw new FormatException("Expected to read 0x{0:X2} but got 0x{1:X2}".FormatInvariant(value, buffer[offset - 1]));}public short ReadInt16(){ var result = BitConverter.ToInt16(buffer, offset); offset += 2; return result;}public ushort ReadUInt16(){ var result = BitConverter.ToUInt16(buffer, offset); offset += 2; return result;}public int ReadInt32(){ var result = BitConverter.ToInt32(buffer, offset); offset += 4; return result;}public uint ReadUInt32(){ var result = BitConverter.ToUInt32(buffer, offset); offset += 4; return result;}讀取字符串(Null-Terminated String)
public byte[] ReadNullTerminatedByteString(){ // https://dev.mysql.com/doc/internals/en/string.html // Protocol::NulTerminatedString: Strings that are terminated by a 0x00 byte. int index = offset; while (index < maxOffset && buffer[index] != 0x00) index++; if (index == maxOffset) throw new FormatException("Read past end of buffer looking for NUL."); byte[] substring = new byte[index - offset]; Buffer.BlockCopy(buffer, offset, substring, 0, substring.Length); offset = index + 1; return substring;}讀取字符串(Length Coded String)
public ArraySegment<byte> ReadLengthEncodedByteString(){ // https://dev.mysql.com/doc/internals/en/string.html // Protocol::LengthEncodedString var length = checked((int)ReadLengthEncodedInteger()); var result = new ArraySegment<byte>(buffer, offset, length); offset += length; return result;}總結
把以上代碼匯總成ByteArrayReader
using System;namespace MySql.Data{ internal sealed class ByteArrayReader { public ByteArrayReader(byte[] buffer, int offset, int length) { if (buffer == null) throw new ArgumentNullException(nameof(buffer)); if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset)); if (offset + length > buffer.Length) throw new ArgumentOutOfRangeException(nameof(length)); this.buffer = buffer; maxOffset = offset + length; this.offset = offset; } public ByteArrayReader(ArraySegment<byte> arraySegment) : this(arraySegment.Array, arraySegment.Offset, arraySegment.Count) { } public int Offset { get { return offset; } set { if (value < 0 || value > maxOffset) throw new ArgumentOutOfRangeException(nameof(value), "value must be between 0 and {0}".FormatInvariant(maxOffset)); offset = value; } } public byte ReadByte() { VerifyRead(1); return buffer[offset++]; } public void ReadByte(byte value) { if (ReadByte() != value) throw new FormatException("Expected to read 0x{0:X2} but got 0x{1:X2}".FormatInvariant(value, buffer[offset - 1])); } public short ReadInt16() { VerifyRead(2); var result = BitConverter.ToInt16(buffer, offset); offset += 2; return result; } public ushort ReadUInt16() { VerifyRead(2); var result = BitConverter.ToUInt16(buffer, offset); offset += 2; return result; } public int ReadInt32() { VerifyRead(4); var result = BitConverter.ToInt32(buffer, offset); offset += 4; return result; } public uint ReadUInt32() { VerifyRead(4); var result = BitConverter.ToUInt32(buffer, offset); offset += 4; return result; } public uint ReadFixedLengthUInt32(int readByteCount) { if (readByteCount <= 0 || readByteCount > 4) throw new ArgumentOutOfRangeException(nameof(readByteCount)); VerifyRead(readByteCount); uint result = 0; for (int i = 0; i < readByteCount; i++) result |= ((uint) buffer[offset + i]) << (8 * i); offset += readByteCount; return result; } public ulong ReadFixedLengthUInt64(int readByteCount) { if (readByteCount <= 0 || readByteCount > 8) throw new ArgumentOutOfRangeException(nameof(readByteCount)); VerifyRead(readByteCount); ulong result = 0; for (int i = 0; i < readByteCount; i++) result |= ((ulong) buffer[offset + i]) << (8 * i); offset += readByteCount; return result; } public byte[] ReadNullTerminatedByteString() { // https://dev.mysql.com/doc/internals/en/string.html // Protocol::NulTerminatedString: Strings that are terminated by a 0x00 byte. int index = offset; while (index < maxOffset && buffer[index] != 0x00) index++; if (index == maxOffset) throw new FormatException("Read past end of buffer looking for NUL."); byte[] substring = new byte[index - offset]; Buffer.BlockCopy(buffer, offset, substring, 0, substring.Length); offset = index + 1; return substring; } public byte[] ReadByteString(int readByteCount) { VerifyRead(readByteCount); var result = new byte[readByteCount]; Buffer.BlockCopy(buffer, offset, result, 0, result.Length); offset += readByteCount; return result; } public UInt64 ReadLengthEncodedInteger() { // https://dev.mysql.com/doc/internals/en/integer.html byte encodedLength = buffer[offset++]; switch (encodedLength) { case 0xFC: return ReadFixedLengthUInt32(readByteCount: 2); case 0xFD: return ReadFixedLengthUInt32(readByteCount: 3); case 0xFE: return ReadFixedLengthUInt64(readByteCount: 8); case 0xFF: throw new FormatException("Length-encoded integer cannot have 0xFF prefix byte."); default: return encodedLength; } } public ArraySegment<byte> ReadLengthEncodedByteString() { // https://dev.mysql.com/doc/internals/en/string.html // Protocol::LengthEncodedString var length = checked((int) ReadLengthEncodedInteger()); var result = new ArraySegment<byte>(buffer, offset, length); offset += length; return result; } public int BytesRemaining => maxOffset - offset; private void VerifyRead(int length) { if (offset + length > maxOffset) throw new InvalidOperationException("Read past end of buffer."); } private readonly byte[] buffer; private readonly int maxOffset; private int offset; }}ByteArrayReader已經實現了把報文包按照MySQL協議所使用到的基本類型切成片組裝成C#類型,接下來就可以根據MySQL各種報文類型,把MySQL協議報文包實現成C#報文類型。下一篇文章,我們將實現如何從Socket中獲取報文內容封裝成C# classPayload,以及實現報文OK_Packet。
新聞熱點
疑難解答