基础知识
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执行完成。
我的文档也讲完了。。。
不足之处,请大家谅解。
谢谢
网友评论