自己动手和MySQL通讯

基础知识

MySQL数据类型

定长整型

MySQL整型有1,2,4,6,8字节长度,使用小字节序传输。以4字节长度为例说明打包和解包过程:

解包过程:

/**
 * @param buff 解包的字节流
 * @param cursor 当前解析位置指针
 * @return (int,uint32)= (解析后位置,读到的整型值)
 */
func ReadUB4(buff []byte, cursor int) (int, uint32) {
    i := uint32(buff[cursor])
    i |= uint32(buff[cursor + 1]) << 8
    i |= uint32(buff[cursor + 2]) << 16
    i |= uint32(buff[cursor + 3]) << 24
    return cursor + 4, i
}

打包过程:

/**
 * @param buff 当前字节流
 * @param i 整型
 * @return 打包后的字节流
 */
func WriteUB4(buf []byte, i uint32) []byte {
    buf = append(buf, byte(i & 0xFF))
    buf = append(buf, byte((i >> 8) & 0xFF))
    buf = append(buf, byte((i >> 16) & 0xFF))
    buf = append(buf, byte((i >> 24) & 0xFF))
    return buf
}

不定长整型

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

第一个字节 后续字节数 长度值说明
0-250 0 第一个字节值即为数据的真实长度
251 0 空数据,数据的真实长度为零
252 2 后续额外2个字节标识了数据的真实长度
253 3 后续额外3个字节标识了数据的真实长度
254 8 后续额外8个字节标识了数据的真实长度

解包过程:

func ReadLength(buff []byte, cursor int) (int, uint64) {
    length := buff[cursor]
    cursor++
    switch length {
    case 251:
        return cursor, 0
    case 252:
        cursor, u16 := ReadUB2(buff, cursor)
        return cursor, uint64(u16)
    case 253:
        cursor, u24 := ReadUB3(buff, cursor)
        return cursor, uint64(u24)
    case 254:
        cursor, u64 := ReadUB8(buff, cursor)
        return cursor, u64
    default:
        return cursor, uint64(length)

    }
}

打包过程:

func WriteLength(buf []byte, length int64) []byte {
    if length <= 251 {
        buf = WriteByte(buf, byte(length))
    } else if length < 0x10000 {
        buf = WriteByte(buf,252)
        buf = WriteUB2(buf, uint16(length))
    } else if length < 0x1000000 {
        buf = WriteByte(buf,253)
        buf = WriteUB3(buf, uint32(length))
    } else {
        buf = WriteByte(buf,254)
        buf = WriteUB8(buf, uint64(length))
    }
    return buf
}

字符串

字符串有两种,一种是以\0结尾的字符串,一种是被称为Length Coded String

以\0结尾的字符串

解包过程:

func ReadWithNull(buff []byte, cursor int) (int, []byte) {
    ret := []byte{}
    for ; ;  {
        if buff[cursor] != 0 {
            ret = append(ret, buff[cursor])
            cursor++
        } else {
            cursor++
            break
        }
    }
    return cursor, ret
}

打包过程:

func WriteWithNull(buf []byte, from []byte) []byte {
    buf = WriteBytes(buf, from)
    buf = append(buf, byte(0))
    return buf
}

Length Coded String

通俗的说,这种string有两部分组成,string头,string体,string头标注了string体的长度,string头采用不定长整型编码。

打包过程:

func WriteWithLength(buf []byte, from []byte) []byte {
    length := len(from)
    buf = WriteLength(buf, int64(length))
    buf = WriteBytes(buf, from)
    return buf
}

解包过程:

func ReadLengthString(buff []byte, cursor int) (int, string) {
    cursor, strLen := ReadLength(buff, cursor)
    cursor, tmp := ReadBytes(buff, cursor, int(strLen))
    return cursor, string(tmp)
}

包基本格式

一个Packet含了,两部分:

  • 包头 包含3个字节的包体长度,一个字节的包序
  • 包体 长度在包头指定

具体格式如下:

包体长度(3字节) 包序(1字节) 包体(长度不定)

包体长度和包序组成了PacketHead,包体为PacketBody

和MySQL通讯(以发送SHOW TABLES查询为例)

注意:下面的实例中没有包头,是因为包头在socket发送或者接受数据的时候已经加上或者去掉

建立socket连接后,MySQL返回HandsharkProtocol 握手协议

当我们使用socket和mysql建立通讯后,mysql第一步就会发送这个协议给客户端

type HandsharkProtocol struct {
    ProtocolVersion                 byte
    ServerVersion                   string
    ServerThreadID                  uint32
    Seed                            []byte
    ServerCapabilitiesLow       uint16
    CharSet                         byte
    ServerStatus                    uint16
    ServerCapabilitiesHeight   uint16
    RestOfScrambleBuff          []byte
    Auth_plugin_name            string
}

具体协议格式如下:
[图片上传失败...(image-9e5aec-1517473706232)]

解包过程如下:

func DecodeHandshark(buff []byte) HandsharkProtocol {
    var cursor int
    var tmp []byte
    hs := new(HandsharkProtocol)

    cursor, hs.ProtocolVersion = util.ReadByte(buff, cursor)
    cursor, tmp = util.ReadWithNull(buff, cursor)
    hs.ServerVersion = string(tmp)
    cursor, hs.ServerThreadID = util.ReadUB4(buff, cursor)
    cursor, hs.Seed = util.ReadWithNull(buff, cursor)
    cursor, hs.ServerCapabilitiesLow = util.ReadUB2(buff, cursor)
    cursor, hs.CharSet = util.ReadByte(buff, cursor)
    cursor, hs.ServerStatus = util.ReadUB2(buff, cursor)
    cursor, hs.ServerCapabilitiesHeight = util.ReadUB2(buff, cursor)
    cursor, _ = util.ReadBytes(buff, cursor, 11)
    cursor, hs.RestOfScrambleBuff = util.ReadWithNull(buff, cursor)
    cursor, tmp = util.ReadWithNull(buff, cursor)
    hs.Auth_plugin_name = string(tmp)

    fmt.Printf("DecodeHanshark: %+v\n", hs)

    return *hs
}

客户端发送AuthProtocol 登录验证协议

客户端收到HandsharkProtocol后,就可以发送AuthProtocol给服务端了,协议定义如下:

[图片上传失败...(image-361c5c-1517473706232)]

打包过程如下:

func EncodeLogin(hs HandsharkProtocol, uname string, password string, dbname string) []byte {
    buf := []byte{}

    capabilities := GetCapabilities(hs)
    capabilities |= common.CLIENT_CONNECT_WITH_DB

    buf = util.WriteUB4(buf, capabilities)
    buf = util.WriteUB4(buf, 1024 * 1024 * 16)
    buf = util.WriteByte(buf, hs.CharSet)
    for i := 0; i < 23 ; i++  {
        buf = append(buf, 0)
    }
    if len(uname) == 0 {
        buf = append(buf, 0)
    } else {
        buf = util.WriteWithNull(buf, []byte(uname))
    }

    encryPass := util.GetPassword([]byte(password), hs.Seed, hs.RestOfScrambleBuff)
    if (capabilities & common.CLIENT_SECURE_CONNECTION) > 0 {
        buf = util.WriteWithLength(buf, encryPass)
    } else {
        buf = util.WriteBytes(buf, encryPass)
        buf = util.WriteByte(buf, 0)
    }

    buf = util.WriteWithNull(buf, []byte(dbname))
    buf = util.WriteWithNull(buf, []byte(hs.Auth_plugin_name))

    return buf
}

里面password的计算方式我们特殊说一下:

伪代码为:

stage1_hash = SHA1(password), using the password that the user has entered.
token = SHA1(SHA1(stage1_hash), scramble) XOR stage1_hash

go代码为:

func GetPassword(pass []byte, seed []byte, restOfScrambleBuff []byte) []byte {
    salt := []byte{}
    for _,v := range seed {
        salt = append(salt, v)
    }
    for _,v := range restOfScrambleBuff {
        salt = append(salt, v)
    }

    sh := sha1.New()
    sh.Write(pass)
    stage1_hash := sh.Sum(nil)


    sh.Reset()
    sh.Write(stage1_hash)
    stage2_hash := sh.Sum(nil)

    sh.Reset()
    for _, v := range stage2_hash {
        salt = append(salt, v)
    }
    sh.Write(salt)

    stage3_hash := sh.Sum(nil)

    ret := []byte{}
    for k,_ := range stage3_hash  {
        ret = append(ret, stage1_hash[k] ^ stage3_hash[k])
    }
    return ret
}

登录成功后,客户端会收到OkPacket 成功协议

字节 说明
1 OK报文,值恒为0x00
1-9 受影响行数(Length Coded Binary)
1-9 索引ID值(Length Coded Binary)
2 服务器状态
2 告警计数
n 服务器消息(字符串到达消息尾部时结束,无结束符,可选)

解包代码为:

func DecodeOk(buff []byte) OK {
    var cursor int
    ok := new(OK)
    cursor, ok.PacketType   = util.ReadByte(buff, 0)
    cursor, ok.AffectedRows = util.ReadLength(buff, cursor)
    cursor, ok.InsertID     = util.ReadLength(buff, cursor)
    cursor, ok.ServerStatus = util.ReadUB2(buff, cursor)
    cursor, ok.WarningNum   = util.ReadUB2(buff, cursor)
    return *ok
}

恭喜,到了这一步就成功了80%

到了这一步,我们完成mysql的验证,并且完成了大部分的util函数,剩下的工作就变得简单有趣。
MySQL Client发送给MySQL Server的命令都是统一格式的(注意哈,这里特指PacketBody是统一格式的)

格式如下:

命令(1字节) 参数(长度不定,具体长度可以有PacketHead.BodySize - 1确定)

我们简单列举几个命令:

类型值 命令 功能
0x02 COM_INIT_DB 切换数据库
0x03 COM_QUERY 查询SQL
0x05 COM_CREATE_DB 创建数据库
0x06 COM_DROP_DB 删除数据库

还有很多很多很多。。自己需要的话不妨看一下MySQL Document, 或者百度,谷歌一下也能解决问题

发送COM_QUERY命令

这个命令如此简单,以至于我们只要发送下面的字符串就好了:

0x03 "SHOW TABLES".toBytes()

代码如下:

func EncodeQuery(sql string) []byte {
    buff := []byte{}
    buff = append(buff, 0x03)
    for _,v := range []byte(sql)  {
        buff = append(buff, v)
    }
    buff = append(buff, 0)
    return buff
}

发送COM_QUERY命令后,MySQL返回ResultSet包

包格式如下:

结构 说明
Result Set Header 1字节,列数量,采用不定长整型编码
Field 列信息(多个)
EOF 列结束
Row Data 行数据(多个)
EOF 数据结束

Field结构

字节 说明
n 目录名称(Length Coded String)
n 数据库名称(Length Coded String)
n 数据表名称(Length Coded String)
n 数据表原始名称(Length Coded String)
n 列(字段)名称(Length Coded String)
4 列(字段)原始名称(Length Coded String)
1 填充值
2 字符编码
4 列(字段)长度
1 列(字段)类型
2 列(字段)标志
1 整型值精度
2 填充值(0x00)
n 默认值(Length Coded String)

RowData结构

在Result Set消息中,会包含多个Row Data结构,每个Row Data结构又包含多个字段值,这些字段值组成一行数据。

字节 说明
n 字段值(Length Coded String)
... (一行数据中包含多个字段值)

字段值:行数据中的字段值,字符串形式

解包过程

整体解包

func DecodeResultSet(conn net.Conn) ResultSet {
    var body []byte
    var resultSet ResultSet
    resultSet.Fields = make([]Field, 0)
    resultSet.Data = make([]RowData, 0)

    //reset header
    _, _, body = socket.ReadPacket(conn)
    _, resultSet.RowNum = util.ReadLength(body, 0)

    //fields
    _, _, body = socket.ReadPacket(conn)
    for ; ; {
        if body[0] == 0xFE {
            break
        }
        resultSet.Fields = append(resultSet.Fields, DecodeField(body))
        _, _, body = socket.ReadPacket(conn)
    }

    //rowdata
    _, _, body = socket.ReadPacket(conn)
    for ; ;  {
        if body[0] == 0xFE {
            break
        }
        resultSet.Data = append(resultSet.Data, DecodeRowData(body))
        _, _, body = socket.ReadPacket(conn)
    }
    return resultSet
}

解Field包

func DecodeField(buf []byte) Field {
    f := new(Field)
    cursor, tmp := util.ReadLengthString(buf, 0)
    f.DirName = string(tmp)
    cursor, tmp = util.ReadLengthString(buf, cursor)
    f.DatabaseName = string(tmp)
    cursor, tmp = util.ReadLengthString(buf, cursor)
    f.TableName = string(tmp)
    cursor, tmp = util.ReadLengthString(buf, cursor)
    f.TablePreName = string(tmp)
    cursor, tmp = util.ReadLengthString(buf, cursor)
    f.ColumnName = string(tmp)
    cursor, tmp = util.ReadLengthString(buf, cursor)
    f.ColumnPreName = string(tmp)
    cursor, _ = util.ReadByte(buf, cursor)
    cursor, f.CharSet = util.ReadUB2(buf, cursor)
    cursor, f.ColumnLength = util.ReadUB4(buf, cursor)
    cursor, f.ColumnType = util.ReadByte(buf, cursor)
    cursor, f.ColumnSign = util.ReadUB2(buf, cursor)
    cursor, f.IntDegree = util.ReadByte(buf, cursor)
    cursor, _ = util.ReadUB2(buf, cursor)
    if cursor < len(buf) {
        cursor, f.DefaultValue = util.ReadLengthString(buf, cursor)
    }
    return *f
}

解RowData

func DecodeRowData(buf []byte) RowData {
    var c int = 0
    var s string
    var rowData RowData

    rowData.Data = make([]string, 0)
    for ; ;  {
        if c >= len(buf) {
            break
        }
        c, s = util.ReadLengthString(buf, c)
        rowData.Data = append(rowData.Data, s)
    }
    return rowData
}

至此,我们发送给MySQL Server的命令show tables执行完成。

我的文档也讲完了。。。

不足之处,请大家谅解。

谢谢

你可能感兴趣的:(自己动手和MySQL通讯)