美文网首页架构师之路程序员
自己动手和MySQL通讯

自己动手和MySQL通讯

作者: jmniu | 来源:发表于2018-02-01 16:25 被阅读0次

    基础知识

    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通讯

        本文链接:https://www.haomeiwen.com/subject/jagdzxtx.html