介绍
每次使用MySQL的时候,都是直接使用编写好的驱动,只关注业务部分。这次想探索一下连接的过程,因此有了这次总结。
与MySQL服务器的交互,可以分为四个阶段
建立TCP连接
握手认证阶段
命令执行阶段
断开连接
这里主要探索握手认证的阶段,注意这里的握手认证,和TCP的三次握手不是同一个,是先建立了TCP连接,即已经完成了TCP三次握手,才进入到MySQL的握手认证。
MySQL数据报文固定格式
MySQL的数据报文,由固定4byte的消息头和消息体组成。
其中消息头的前3个byte代表消息体的实际大小,第4个byte代表序号,用于保证消息顺序的正确。
解析握手包
当客户端与MySQL服务器建立起TCP连接的时候,MySQL服务器会发送一个握手包给客户端,称为HandshakeV10,来分析一下这个报文结构:
大小(单位byte)
名称
解释
1
protocol version
协议版本号
n
server version
MySQL服务器版本,string[NUL]类型
4
connection id
服务器线程ID
8
auth-plugin-data-part-1
挑战随机数第一部分
1
filler
1byte的填充数,值为0x00
2
capability flags
服务器权能标志,低16位
1
character set
字符编码
2
status flags
服务器状态
2
capability flags
服务器权能标志,高16位
1
length of auth-plugin-data
挑战数长度
10
reserved
10byte的填充数,值为0x00
13
auth-plugin-data-part-2
挑战随机数第二部分,最少12字节,最长13字节
n
auth-plugin name
验证插件的名称,string[NUL]类型
string[NUL] 以0x00结尾的字符串
首先MySQL采用的是挑战/应答的模式进行验证,因此获取到这个握手包之后,要对该报文进行解析,获取到挑战随机数,用于下个步骤中进行应答。
定义一个结构体 HandsharkProtocol
type HandsharkProtocol struct {
ProtocolVersion byte
ServerVersion string
ConnectionId uint32
AuthPluginDataPart1 []byte
Filler byte
CapabilityFlag1 []byte
CharacterSet byte
StatusFlags []byte
CapabilityFlag2 []byte
AuthPluginDataLen byte
Reserved []byte
AuthPluginDataPart2 []byte
AuthPluginName string
}
复制代码
解析握手包的过程,就按照定义,逐个解析
func DecodeHandshark(b []byte) *HandsharkProtocol {
hs := HandsharkProtocol{}
buff := bytes.NewBuffer(b) // 将[]byte转成buffer,方便处理
buff.Next(4) // 前四个字节为消息头,不处理
hs.ProtocolVersion, _ = buff.ReadByte() // 协议版本号
hs.ServerVersion, _ = buff.ReadString(0x00) // MySQL服务器版本 0x00结尾的字符串
hs.ConnectionId = binary.LittleEndian.Uint32(buff.Next(4)) // 服务器线程ID 小端法存储
hs.AuthPluginDataPart1 = buff.Next(8) // 挑战随机数第一部分
hs.Filler, _ = buff.ReadByte() // 1byte的填充
hs.CapabilityFlag1 = buff.Next(2) // 服务器权能标志 低16位
if buff.Len() > 0 { // if more data in the packet
hs.CharacterSet, _ = buff.ReadByte() // 字符编码
hs.StatusFlags = buff.Next(2) // 服务器状态
hs.CapabilityFlag2 = buff.Next(2) // 服务器权能标志 高16位
hs.AuthPluginDataLen, _ = buff.ReadByte() // 挑战数长度
hs.Reserved = buff.Next(10) // 10 byte的填充
hs.AuthPluginDataPart2 = buff.Next(12) // 挑战随机数第二部分
buff.ReadByte() // 挑战随机数第13个byte,0x00 因此无视
hs.AuthPluginName, _ = buff.ReadString(0x00) // 验证插件的名称 0x00结尾的字符串
}
return &hs
}
复制代码
来测试一下,解析出来的握手包结构
func main() {
c, err := net.Dial("tcp", ":3306")
if err != nil {
fmt.Println(err)
}
pkg := make([]byte, 1024)
_, _ = c.Read(pkg)
h := DecodeHandshark(pkg)
fmt.Println(h)
}
复制代码
h 为 &{10 8.0.19 421 [56 49 17 70 10 105 114 72] 0 [255 255] 255 [2 0] [255 199] 21 [0 0 0 0 0 0 0 0 0 0] [24 87 2 88 38 4 19 40 89 59 84 62] caching_sha2_password},协议版本为10,服务器版本8.0.19等等信息都可以直观的看到,接下来就是进行验证应答。
发送握手包响应数据
大小(单位byte)
名称
解释
4
capability flags
客户端全能标志位,一般值为CLIENT_PROTOCOL_41(0x00000200)
4
max-packet size
包的最大长度,可以设置成0x00
1
character set
字符编码
23
reserved
23byte的填充数,值为0x00
n
username
要登录的用户名 string[NUL]型
1
length of auth-response
应答挑战数的长度
n
auth-response
应答挑战数
n
database
要连接的数据库名称 string[NUL]型
n
auth plugin name
验证插件名称
由于采用的是caching_sha2_password验证的方式,因此根据该方式,hash数据库的登录密码
// 这段代码是使用 github.com/go-sql-driver/mysql包中的方法
func scrambleSHA256Password(scramble []byte, password string) []byte {
if len(password) == 0 {
return nil
}
crypt := sha256.New()
crypt.Write([]byte(password))
message1 := crypt.Sum(nil)
crypt.Reset()
crypt.Write(message1)
message1Hash := crypt.Sum(nil)
crypt.Reset()
crypt.Write(message1Hash)
crypt.Write(scramble)
message2 := crypt.Sum(nil)
for i := range message1 {
message1[i] ^= message2[i]
}
return message1
}
复制代码
有了这个基础之后,就可以开始构造报文了
func GetHandshakeResponsePacket(authResp []byte, plugin string) []byte {
//消息体的长度
authLen := len(authResp)
pkgBodyLen := 4 + 4 + 1 + 23 + len(user) + 1 + authLen + len(authResp) + len(dbName) + 1 + len(plugin) + 1
data := make([]byte, pkgBodyLen + 4) // + 4的消息头长度
// capability flags
var capabilityFlags uint32 = 512 | 8 | 524288
// 客户端权能标志位
// 512 代表 clientProtocol41,
// 8 代表 clientConnectWithDB,
// 524288 代表 clientPluginAuth
// 使用小端法表示,参照 binary.LittleEndian.PutUint32() 方法
data[4] = byte(capabilityFlags)
data[5] = byte(capabilityFlags >> 8)
data[6] = byte(capabilityFlags >> 16)
data[7] = byte(capabilityFlags >> 24)
// max-size
data[8] = 0x00
data[9] = 0x00
data[10] = 0x00
data[11] = 0x00
// character set
data[12] = 255 // 255 对应 utf-8
// reserved
pos := 13
for ; pos < 13 + 23; pos++ {
data[pos] = 0
}
//username
pos += copy(data[pos:], user)
data[pos] = 0x00
pos++
// authResp
pos += copy(data[pos:], []byte{byte(uint64(authLen))})
pos += copy(data[pos:], authResp)
// dbName
pos += copy(data[pos:], dbName)
data[pos] = 0x00
pos++
// plugin
pos += copy(data[pos:], plugin)
data[pos] = 0x00
pos++
return data[:pos]
}
复制代码
最后再补上消息头,就可以发送给MySQL服务器
const user = "root" // 用户名
const password = "123" // 密码
const dbName = "test" // 数据库名
func main() {
c, err := net.Dial("tcp", ":3306")
if err != nil {
fmt.Println(err)
}
pkg := make([]byte, 1024)
_, _ = c.Read(pkg)
h := DecodeHandshark(pkg)
// 构造响应报文
scramble := append(h.AuthPluginDataPart1, h.AuthPluginDataPart2...)
authResp := scrambleSHA256Password(scramble, password)
respPacket := GetHandshakeResponsePacket(authResp, h.AuthPluginName)
pktLen := len(respPacket) - 4 // 减掉消息头长度
respPacket[0] = byte(pktLen)
respPacket[1] = byte(pktLen >> 8)
respPacket[2] = byte(pktLen >> 16) // 前3个byte小端法表示消息体大小
respPacket[3] = 1 // 序号
c.Write(respPacket)
// 这两句为了阻塞程序,方便查看是否已经连接上mysql
ch := make(
}
复制代码
确认认证结果,完成认证
最后来看看是否连接成功,在程序没有连接之前,mysql服务器里只有两个客户端连接
然后运行程序
可以看到多了一个客户端连接,说明握手认证成功,之后就可以发送命令给mysql执行。
总结
虽然在平常的工作中,不会自己编写连接驱动,但是秉着好奇的心,研究了一下mysql的连接过程,还是受益颇多~。
Thanks!