MySQL协议.NET Core实现(一)

导语

个高水平的研发团对,无论使用什么框架、什么工具、什么语言,团队里应该有人有能力把控所使用框架、工具、语言的每一个核心功能的实现细节。团队里的每个成员应该根据自身所长挑选其中一块做深入研究,并把研究成果分享给团队,力争使整个所处团队实力得到提升,达到同行业内顶尖水平。为了实现这个目标,不允许在团队中出现黑盒子,对.NET生态而言,我们需要打开MSBuildRosylnCoreCLR等黑盒子。我作为团队的一员,将花一些业余时间打开MySQL .NET驱动黑盒子,使用.NET Core实现MySQL Client/Server Protocol,并把打开这个黑盒子的过程通过本站点记录下来,包括知识储备、实现思路,框架代码等。

这是使用.NET Core实现MySQL协议系列文章的第一篇。

知识储备(可跳过)

网络中进程之间如何通信?

在网络时代,网络中进程间通信无处不在,在本地可以通过进程PID来唯一标识一个进程,但网络中进程之间是如何通信的?其实TCP/IP协议族已经帮我们解决了这个问题,网络层的 IP地址 可以唯一标识网络中的主机,而传输层的 协议+端口 可以唯一标识主机中的应用程序(进程),因此,可以利用三元组 (IP地址,协议,端口) 标识网络中的进程,网络中的进程通信就可以利用这个标志与其它进程进行交互。无论应用层采用什么协议,最终都要经过传输层/网络层的TCP/IP协议,而TCP/IP编程有一套标准编程接口——Socket。

什么是Socket?

套接字(Socket)是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元,但Socket跟TCP/IP并没有必然的联系,TCP/IP只是一个协议栈,就像操作系统的运行机制一样,必须要具体实现,同时还要提供对外的操作接口,TCP/IP也必须对外提供编程接口——Berkeley sockets interface.,它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:

  1. 连接使用的协议;
  2. 本地主机的IP地址;
  3. 本地进程的协议端口;
  4. 远地主机的IP地址;
  5. 远地进程的协议端口;

TCP三次握手建立连接

我们知道TCP建立连接要进行三次握手,大致流程如下:

  • 客户端向服务器发送一个SYN x
  • 服务器向客户端响应一个SYN y,并对SYN x进行确认ACK x+1
  • 客户端再向服务器发一个确认ACK y+1

TCP四次挥手断开连接

TCP断开连接要进行四次挥手,大致流程如下:

  • 应用进程首先调用close主动关闭连接,这时TCP发送一个FIN x
  • 另一端接收到FIN x之后,执行被动关闭,对这个FIN进行确认
  • 一段时间之后,应用进程调用close关闭它的socket,这导致它的TCP也发送一个FIN y
  • 接收到这个FIN的源发送端TCP对它进行确认

这样每个方向上都有一个FIN和ACK

MySQL协议分析

  • TCP/IP协议(上面的知识储备就是为此准备的,所以,本系列文章假设TCP/IP协议)
  • Unix Socket协议
  • Share Memory协议
  • Named Pipes协议


交互过程

MySQL客户端与服务器的交互主要分为两个阶段

  • 握手认证阶段;
  • 命令执行阶段;

握手认证阶段

握手认证阶段为客户端与服务器通过TCP三次握手建立连接后进行,交互过程如下

  • 服务器 -> 客户端:握手初始化消息
  • 客户端 -> 服务器:登陆认证消息
  • 服务器 -> 客户端:认证结果消息

命令执行阶段

客户端认证成功后,会进入命令执行阶段,交互过程如下

  • 客户端 -> 服务器:执行命令消息
  • 服务器 -> 客户端:命令执行结果

MySQL客户端与服务器的完整交互过程如下


MySQL协议中使用到的基本类型

MySQL官网链接

这里的信息很重要,能不能理解后续代码,这里是关键点之一,务必铭记于心。

这里的信息很重要,能不能理解后续代码,这里是关键点之一,务必铭记于心。

这里的信息很重要,能不能理解后续代码,这里是关键点之一,务必铭记于心。

重要的事情说三遍。

整型值

MySQL报文中整型值分别有1、2、3、4、8字节长度,使用小字节序传输。 注意字节序

字符串(Null-Terminated String)

字符串长度不固定,当遇到'NULL'(0x00)字符时结束。

二进制数据(Length Coded Binary)

数据长度不固定,长度值由数据前的1-9个字节决定,其中长度值所占的字节数不定,字节数由第1个字节决定,如下表:

第一个字节值 后续字节数 长度值说明
0-2500第一个字节值即为数据的真实长度
2510空数据,数据的真实长度为零
2522后续额外2个字节标识了数据的真实长度
2533后续额外3个字节标识了数据的真实长度
2548后续额外8个字节标识了数据的真实长度

字符串(Length Coded String)

字符串长度不固定,无'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-2500第一个字节值即为数据的真实长度
2510空数据,数据的真实长度为零
2522后续额外2个字节标识了数据的真实长度
2533后续额外3个字节标识了数据的真实长度
2548后续额外8个字节标识了数据的真实长度

从上表可以看出,报文的最大长度是8个字节,8字节 * 每字节8bit = 64bit, 结合C#数据类型,能表示8个字节的基础数据类是是Int64UInt64,因为报文长度不可能为负,所以使用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#  class Payload ,以及实现报文 OK_Packet



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值