MySQL是怎样通讯的?

前言

我们平常使用数据库的场景一般是程序里面代码直接连接使用,然后进行 CRUD 操作。或者使用有 GUI 界面的数据库软件来手动操作数据库, 这类软件有 DataGrip、Navicat等等…。平常很少关心它们的底层数据交互是怎么样的,相信你看了这篇文章一定能有大概的了解。本篇文章的代码使用 Go 语言来实现 MySQL 的协议。

协议简介

MySQL 协议一般分为两个阶段,一个是连接阶段,一个是命令阶段。 连接阶段主要是客户端和服务端进行相互认证的阶段,就像我们平常登陆某个网站的一个操作。 命令阶段主要是客户端向服务端进行的一些指令的发送,然后服务端处理指令并返回结果的一个过程。 在客户端和服务端发送的数据包中,前 3 个字节表示这个数据包的大小,所以这里就有一个问题,就是它有一个大小的限制,数据包大小不能超过16777215 ($2^{24}-1$) bytes,也就是 16M 大小(16进制表示:ff ff ff,刚刚 3 个字节)。这就会有三种情况出现,一种是数据包小于 16M,一种是等于,一种是大于。所以在 MySQL 协议中是这样处理的:

  • 小于 16M:发送一个数据包就可以了
  • 等于 16M:发送两个数据包,第二个包为空包
  • 大于 16M:发送多个数据包,每个数据包大小最大为 16M,当最后一个数据包等于 16M 时,再多发送一个空数据包

每个数据包中的第 4 个字节表示这个数据包的序号ID,这个 ID 在不同阶段会递增,比如在连接阶段,这个 ID 会随着包的数量而递增,当连接阶段完成后进入命令阶段,这个 ID 又会从 0 开始递增,直到这个命令的生命周期结束。

初始握手包

当客户端进行尝试使用 TCP 连接 MySQL 服务端时,服务端就会响应一个初始的握手包,这个握手包有 V9、V10 两个版本。不过现在一般用的都是 V10 版本,如果 MySQL 的版本在 3.21.0 之前,那么服务端响应的是 V9 版本的初始握手包。本篇文章就讲讲现在常用的 V10 版本的初始握手包。

我们可以使用以下代码来尝试连接我们本地的 MySQL 服务:

package main

import "net"

func main()  {
	conn, err := net.Dial("tcp","127.0.0.1:3306")
	if err != nil {
		return
	}
	defer func(conn net.Conn) {
		err := conn.Close()
		if err != nil {

		}
	}(conn)
}

运作程序后,服务端就会响应一个初始握手包给我们,那么怎么清楚明了的查看这个数据包呢?此时我们可以用 Wireshark 这个软件来查看 MySQL 服务端返回的数据包 image.png 可以看到前 4 个字节为 16 进制的数据: 4e 00 00 00 ,表示了这个数据包大小为 78 字节,序号 ID 为 0。具体的字段字节大小和描述如下表示:

字段名字节数据长度(byte)描述
Protocol1初始握手包协议版本,可以根据这个字节数据来判断握手包的协议版本,然后按不同版本来处理接下来的数据
Version直到遇到字节数据为 0 的时候停止MySQL 服务端版本描述字符串
Thread ID4连接 ID
Slat(第一段)8用于处理后续客户端的密码加密
filler1填充一个字节,默认为 0
Service Capability(Low)2服务端能力标志,一共有 4 个字节,这里表示的是低 2 位字节的数据
Server Language1服务端字符编码
Server Status2服务端状态
Service Capability(Upper)2服务端能力标志,这里表示的是高 2 位字节的数据
Authentication Plugin Length1身份验证插件长度
Unused10预留的 10 个字节数据,默认全部为 0
Slat(第二段)计算公式:MAX(13, 身份验证插件长度 - 8)用于处理后续客户端的密码加密
Authentication Plugin直到遇到字节数据为 0 的时候停止身份验证插件

这个初始握手包里包含了很多的数据,在后续的整个连接阶段需要用到里面的大部分数据。

能力标志

上面服务端响应端初始握手包中包含了一个能力标志,这个能力标志一共有 4 个字节来表示,我们知道 1 个字节有 8 个 bit,所以 4 个字节一共有 32 个 bit,其中除了最高的 7 个 bit,另外的每一个 bit 都代表着一个能力标志的状态(0 为不支持,1 为支持),就像下面这样表示

# Capabilities 字节数据中的低 2 位
Server Capabilities: 0xffff
.... .... .... ...1 = Long Password: Set
.... .... .... ..1. = Found Rows: Set
.... .... .... .1.. = Long Column Flags: Set
.... .... .... 1... = Connect With Database: Set
.... .... ...1 .... = Don't Allow database.table.column: Set
.... .... ..1. .... = Can use compression protocol: Set
.... .... .1.. .... = ODBC Client: Set
.... .... 1... .... = Can Use LOAD DATA LOCAL: Set
.... ...1 .... .... = Ignore Spaces before '(': Set
.... ..1. .... .... = Speaks 4.1 protocol (new flag): Set
.... .1.. .... .... = Interactive Client: Set
.... 1... .... .... = Switch to SSL after handshake: Set
...1 .... .... .... = Ignore sigpipes: Set
..1. .... .... .... = Knows about transactions: Set
.1.. .... .... .... = Speaks 4.1 protocol (old flag): Set
1... .... .... .... = Can do 4.1 authentication: Set

# Capabilities 字节数据中的高 2 位
Extended Server Capabilities: 0xc1ff
.... .... .... ...1 = Multiple statements: Set
.... .... .... ..1. = Multiple results: Set
.... .... .... .1.. = PS Multiple results: Set
.... .... .... 1... = Plugin Auth: Set
.... .... ...1 .... = Connect attrs: Set
.... .... ..1. .... = Plugin Auth LENENC Client Data: Set
.... .... .1.. .... = Client can handle expired passwords: Set
.... .... 1... .... = Session variable tracking: Set
.... ...1 .... .... = Deprecate EOF: Set
1100 000. .... .... = Unused: 0x60

除了服务端响应的初始握手包会返回这个能力标志,后续我们发送给服务端的 HandshakeResponse 数据包中也包含这个能力标志数据,那么我们该怎么发送这个能力标志数据呢?官方给出力各个能力的值,如下表:

序号Capability Flags值【16进制】
1CLIENT_LONG_PASSWORD0x1
2CLIENT_FOUND_ROWS0x2
3CLIENT_LONG_FLAG0x4
4CLIENT_CONNECT_WITH_DB0x8
5CLIENT_NO_SCHEMA0x10
6CLIENT_COMPRESS0x20
7CLIENT_ODBC0x40
8CLIENT_LOCAL_FILES0x80
9CLIENT_IGNORE_SPACE0x100
10CLIENT_PROTOCOL_410x200
11CLIENT_INTERACTIVE0x400
12CLIENT_SSL0x800
13CLIENT_IGNORE_SIGPIPE0x1000
14CLIENT_TRANSACTIONS0x2000
15CLIENT_RESERVED0x4000
16CLIENT_SECURE_CONNECTION0x8000
17CLIENT_MULTI_STATEMENTS0x10000
18CLIENT_MULTI_RESULTS0x20000
19CLIENT_PS_MULTI_RESULTS0x40000
20CLIENT_PLUGIN_AUTH0x80000
21CLIENT_CONNECT_ATTRS0x100000
22CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA0x200000
23CLIENT_CAN_HANDLE_EXPIRED_PASSWORDS0x400000
24CLIENT_SESSION_TRACK0x800000
25CLIENT_DEPRECATE_EOF0x1000000

当我们要发送客户端支持的能力标志时,只要把所有支持的能力标志的值相加,然后转换为 4 字节大小的数据。 例如我们要发送个给服务端说明我们支持 CLIENT_PROTOCOL_41 这个能力,那么我们就可以把这个 16 进制的值转换为 4 个字节的数据来表示,转换后的数据为:[0 0 16 0]。HEX 表示法为:[00 00 10 00]

注意:上面转换的字节数据为小端数据,这方面端知识具体可以查询字节序的大小端

字符编码

初始握手包还有一个字节表示了支持的字符编码,后续我们响应的 HandshakeResponse 数据包中也要发送客户端支持的字符编码,相对应的字符编码对应的 ID 如下表,当我们支持什么字符编码时,只要发送对应编码的 ID 就可以了。

+--------------------+---------------------+-----+
| CHARACTER_SET_NAME | COLLATION_NAME      | ID  |
+--------------------+---------------------+-----+
| big5               | big5_chinese_ci     |   1 |
| dec8               | dec8_swedish_ci     |   3 |
| cp850              | cp850_general_ci    |   4 |
| hp8                | hp8_english_ci      |   6 |
| koi8r              | koi8r_general_ci    |   7 |
| latin1             | latin1_swedish_ci   |   8 |
| latin2             | latin2_general_ci   |   9 |
| swe7               | swe7_swedish_ci     |  10 |
| ascii              | ascii_general_ci    |  11 |
| ujis               | ujis_japanese_ci    |  12 |
| sjis               | sjis_japanese_ci    |  13 |
| hebrew             | hebrew_general_ci   |  16 |
| tis620             | tis620_thai_ci      |  18 |
| euckr              | euckr_korean_ci     |  19 |
| koi8u              | koi8u_general_ci    |  22 |
| gb2312             | gb2312_chinese_ci   |  24 |
| greek              | greek_general_ci    |  25 |
| cp1250             | cp1250_general_ci   |  26 |
| gbk                | gbk_chinese_ci      |  28 |
| latin5             | latin5_turkish_ci   |  30 |
| armscii8           | armscii8_general_ci |  32 |
| utf8               | utf8_general_ci     |  33 |
| ucs2               | ucs2_general_ci     |  35 |
| cp866              | cp866_general_ci    |  36 |
| keybcs2            | keybcs2_general_ci  |  37 |
| macce              | macce_general_ci    |  38 |
| macroman           | macroman_general_ci |  39 |
| cp852              | cp852_general_ci    |  40 |
| latin7             | latin7_general_ci   |  41 |
| cp1251             | cp1251_general_ci   |  51 |
| utf16              | utf16_general_ci    |  54 |
| utf16le            | utf16le_general_ci  |  56 |
| cp1256             | cp1256_general_ci   |  57 |
| cp1257             | cp1257_general_ci   |  59 |
| utf32              | utf32_general_ci    |  60 |
| binary             | binary              |  63 |
| geostd8            | geostd8_general_ci  |  92 |
| cp932              | cp932_japanese_ci   |  95 |
| eucjpms            | eucjpms_japanese_ci |  97 |
| gb18030            | gb18030_chinese_ci  | 248 |
| utf8mb4            | utf8mb4_0900_ai_ci  | 255 |
+--------------------+---------------------+-----+

客户端握手响应包(HandshakeResponse)

客户端和 MySQL 服务端进行数据交互时,有明文数据交互和SSL加密数据交互,这里贴一张 MySQL 官网给出的一张图,这张图大致的描述了客户端和服务端连接的流程 image.png 本篇文章就讲下简单的明文连接,不管是明文连接和加密连接,客户端都必须返回 HandshakeResponse 这个数据包给服务端。 这个数据包也有两个版本,一个是 HandshakeResponse41,另一个是 HandshakeResponse320。现在一般都是用 HandshakeResponse41 这个版本的数据包。那么服务端要怎么知道客户端发送的数据包到底是什么版本呢? 这个就要用到上面的 CLIENT_PROTOCOL_41 这个能力标志了,服务端只要解析客户端发来的 HandshakeResponse 数据包中的 Capability Flags 数据中是否支持 CLIENT_PROTOCOL_41 这个能力,来判断客户端握手响应包的版本。当 CLIENT_PROTOCOL_41 这个能力为支持状态,说明版本是 HandshakeResponse41,否则就是 HandshakeResponse320

HandshakeResponse41

现在常用的就是 HandshakeResponse41 这个握手响应包,本篇文章就讲一讲这个握手响应包吧。这个包的描述如下:

4              capability flags, CLIENT_PROTOCOL_41 always set
4              max-packet size
1              character set
string[23]	   reserved (all [0])
string[NUL]    username


if capabilities & CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA {
  // 如果支持 CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA 标志就返回这些数据
  lenenc-int     length of auth-response
  string[n]      auth-response
} else if capabilities & CLIENT_SECURE_CONNECTION {
  // 如果支持 CLIENT_SECURE_CONNECTION 标志就返回这些数据
  1              length of auth-response
  string[n]      auth-response
} else {
  // 否则就返回这个数据
  string[NUL]    auth-response
}

if capabilities & CLIENT_CONNECT_WITH_DB {
  // 如果支持 CLIENT_CONNECT_WITH_DB 标志就返回这些数据
  string[NUL]    database
}

if capabilities & CLIENT_PLUGIN_AUTH {
  // 如果支持 CLIENT_PLUGIN_AUTH 标志就返回这些数据
  string[NUL]    auth plugin name
}

if capabilities & CLIENT_CONNECT_ATTRS {
  // 如果支持 CLIENT_CONNECT_ATTRS 标志就返回这些数据
  lenenc-int     length of all key-values
  lenenc-str     key
  lenenc-str     value
  if-more data in 'length of all key-values', more keys and value pairs
 }
  • capability_flags – 客户端的能力标志,占用 4 个字节
  • max_packet_size – 客户端要发送到服务器的命令包的最大大小,占用 4 个字节
  • character_set - 连接的默认字符集,占用 1 个字节
  • username – 客户端要登录的 SQL 帐户的名称 – 此字符串应使用character set字段指示的字符集进行编码。
  • auth-response –由 auth plugin name 字段指示的 Authentication Method 生成的加密的身份验证响应数据。
  • database – 用于连接的初始数据库 – 此字符串应使用 character set字段指示的字符集进行编码。
  • auth plugin name – 客户端用此加密方法加密密码然后赋值给 auth-response 返回给服务端

密码加密方式

客户端传输给服务端的 MySQL 账户的密码加密方式采用插件的形式,就是 auth plugin name 这个字段的数据,一般支持以下几种加密方式

名称Auth Plugin Name能力标志
旧密码认证mysql_old_password不能使用,无能力标志
安全密码认证mysql_native_passwordCLIENT_SECURE_CONNECTION
明文认证mysql_clear_passwordCLIENT_PLUGIN_AUTH
Windows 原生身份验证authentication_windows_clientCLIENT_PLUGIN_AUTH
SHA256sha256_passwordCLIENT_PLUGIN_AUTH

现在一般常用的是安全密码认证,就是 Auth Plugin Name 为 mysql_native_password 的认证加密方式。这个方法的加密方式如下

SHA1( password ) XOR SHA1( "20-bytes random data from server" <concat> SHA1( SHA1( password ) ) )

它先对明文密码进行一次 SHA1 的散列运算生成密码 1,然后再将服务端初始握手包中的 20 位 Slat 数据和对明文密码进行两次 SHA1 散列的结果进行连接,然后对连接的结果再进行一次散列运算生成密码 2,最后密码 1 和密码 2 进行异或运算,得到来最终发送给服务端的数据。

响应数据包

当我们发送响应握手包 HandshakeResponse 后,服务端就会返回一个通用的响应包给我们,这个响应包可以是以下其中一个:

  • OK_Packet
  • ERR_Packet
  • EOF_Packet

那么我们要怎么区分这三个包呢?区分的关键在于包的第一个字节的数据,如果第一个字节数据为 0x00,则代表这是一个 OK_Packet 。如果第一个字节数据为 0xff,则表示这是一个 ERR_Packet。如果第一个字节为 0xfe,则代表这是一个 EOF_Packet。 从 MySQL 5.7.5 开始,OK_Packet 包也用于指示 EOF_Packet,并且不推荐使用 EOF_Packet 包。为了确保 MySQL 的旧版本(5.7.5 之前)和新版本(5.7.5 及更高版本)之间的向后兼容性,新客户端会向服务端该送 CLIENT_DEPRECATE_EOF 能力标志。如果没有传送这个能力标志,服务端返回端数据结果集中还是会以 EOF_Packet 包结尾,如果传送了这个能力标志的话,服务端返回端结果集中会以 OK_Packet 包结尾,并且第一个字节数据会是 0xfe。 那么我们怎么区分新版 OK_Packet 包在什么时候代表 OK_Packet,在什么时候代表 EOF_Packet 呢?主要可以通过以下几点来判断:

  • 一个是判断客户端刚才是否传送了 CLIENT_DEPRECATE_EOF 能力标志
  • OK_Packet: 第一个字节数据为 0x00,且数据包长度 > 7
  • EOF_Packet: 第一个字节数据为 0xfe,且数据包长度 < 9

OK_Packet 格式

int<1>			header				[00] or [fe] the OK packet header
int<lenenc>	affected_rows		受影响行数
int<lenenc>	last_insert_id		最后插入 ID

if capabilities & CLIENT_PROTOCOL_41 {
    int<2>		status_flags	状态标志
    int<2>		warnings		警告数
} elseif capabilities & CLIENT_TRANSACTIONS {
    int<2>		status_flags	Status Flags
}

if capabilities & CLIENT_SESSION_TRACK {
    string<lenenc>	info	人类可读的状态信息
    
    if status_flags & SERVER_SESSION_STATE_CHANGED {
        string<lenenc>	session_state_changes	会话状态信息
    }
    
} else {
    string<EOF>	info		人类可读的状态信息
}

ERR_Packet 格式

int<1>		header				[ff] header of the ERR packet
int<2>		error_code			错误代码

if capabilities & CLIENT_PROTOCOL_41 {
  string<1>	sql_state_marker	SQL 状态的标记
  string<5>	sql_state			SQL 状态
}

string<EOF>	error_message		人类可读的错误信息

EOF_Packet 格式

int<1>		header				[fe] EOF header

if capabilities & CLIENT_PROTOCOL_41 {
  int<2>	warnings			警告数
  int<2>	status_flags		状态标志
}

数据包数据类型介绍

在上面的数据包格式中,你是不是看到例如 int<1>、string、int 等等这些一头雾水?这个是 MySQL 官网文档中表示协议数据类型和长度的。主要数据类型如下表:

数据类型字节长度
int<1>1 字节
int<2>2 字节
int<3>3 字节
int<4>4 字节
int<6>6 字节
int<8>8 字节
int<lenenc>见下文详细介绍
string<lenenc>见下文详细介绍
string<fix>固定字节长度的字符串,其中 fix 代表一个指定的数值,例如 string<5>,其中 fix 就等于 5
string<var>字符串的长度由另一个字段确定或在运行时计算
string<EOF>如果字符串是数据包的最后一个组成部分,则它的长度可以从整个数据包长度减去当前位置来计算。
string<NUL>以 [00] 字节结尾的字符串。

在上面的表格中,大部分的数据类型的长度基本上都可以直接得知,但是其中 int<lenenc>、string<lenenc> 这两个类型的长度需要通过稍微复杂一点的计算来得到最终的数据长度。

int<lenenc>

当要解析这个长度的数据时,它一般开头的第一个字节有 4 中表现形式

  • 第一个字节的值小于 0xfb:代表这个数据就是这一个字节长度,并且第一个字节的值就是对应字段的值
  • 第一个字节的值等于 0xfc:代表这个字节往后的两个字节就是这个字段的数据,就是说这个字段一个占用 3 个字节长度,其中第 1 个字节表示该字段占用的字节长度数据,第 2 和第 3 个字节表示的是这个字段的数据
  • 第一个字节的值等于 0xfd:和上面类似,只是字段数据字节一共占用 4 个字节,其中后 3 个字节表示这个字段的数据
  • 第一个字节的值等于 0xfe:字段数据字节一共占用 9 个字节,其中后 8 个字节表示这个字段的数据

注意:在 MySQL 3.22 版本以前,0xfe 表示的这个字段只有 4 个字节长度。 如果数据包的第一个字节是长度编码的整数并且其字节值为 0xfe,则必须检查数据包的长度以验证它是否有足够的空间容纳 8 字节整数。 如果不是,它可能是一个 EOF_Packet 替代。

所以要得到这个字段对应的字节长度时,只要判断第一个字节的数据,然后就可以轻松获得这个字段的长度了

string<lenenc>

这个数据类型分为两部分

  • length (int) – string 数据的占用字节长度
  • string (string) – [len=$length] string

其中 length 这个数据通过上面 int<lenenc> 的方法获得,然后 string 的数据的字节长度就是 length 的值

发送命令

当我们连接成功后,这时就可以向服务端发送命令了,命令如下表:

HEX 值NAME
00COM_SLEEP
01COM_QUIT
02COM_INIT_DB
03COM_QUERY
04COM_FIELD_LIST
05COM_CREATE_DB
06COM_DROP_DB
07COM_REFRESH
08COM_SHUTDOWN
09COM_STATISTICS
0aCOM_PROCESS_INFO
0bCOM_CONNECT
0cCOM_PROCESS_KILL
0dCOM_DEBUG
0eCOM_PING
0fCOM_TIME
10COM_DELAYED_INSERT
11COM_CHANGE_USER
12COM_BINLOG_DUMP
13COM_TABLE_DUMP
14COM_CONNECT_OUT
15COM_REGISTER_SLAVE
16COM_STMT_PREPARE
17COM_STMT_EXECUTE
18COM_STMT_SEND_LONG_DATA
19COM_STMT_CLOSE
1aCOM_STMT_RESET
1bCOM_SET_OPTION
1cCOM_STMT_FETCH
1dCOM_DAEMON
1eCOM_BINLOG_DUMP_GTID
1fCOM_RESET_CONNECTION

一般我们用的最多的就是 COM_QUERY 这个命令,像 CRUD 都可以通过这个命令来发送,例如我们发送一个查询当前数据库,就可以发送下面的字节数据给服务端

0f 00 00 00 	03 73 68 6f 77 20 64 61 74 61 62 61
73 65 73

其中前 4 个字节代表这个包的大小和序号 ID,后面的字节数据就是我们发送的命令。 03 代表这个命令是 COM_QUERY。 后面所有的字节数据都是 show databses 转换 byte 后的字节数据

结果集

当你发送的 COM_QUERY 命令时,它返回三种数据包的其中一种。我们可以通过第一个字节来判断它:

  • 当第一个字节数据等于 0x00:返回的是 OK_Packet
  • 当第一个字节的数据等于 0xff:返回的是 ERR_Packet
  • 当第一个字节的数据不是以上两个值时:返回一个结果集,并且第一个字节的值代表返回结果集中列(columns)的总数。

结果集分 3 个部分来读取:

  • 第一个数据包表示返回结果集中列(columns)的总数。
  • 然后通过第一个数据包获取的列总数来读取相关列的所有数据包,一列有一个数据包,比如说上面得到列总数为 3,那么接下来的 3 个数据包就是这 3 列的说明。
  • 读完列的所有数据包后,紧接着就是没行数据的数据包了,一个数据包代表一行数据,每个数据包中有所有列的字段值。其中,如果值长度的值为 0xfe 时,则代表这行中这列的数据为 NULL。行数据直到读取到 OK_Packet/EOF_Packet 包出现为止。

COM_QUERY_Response 格式

// 字节长度计算方法见上面的 int<lenenc> 介绍
int<lenenc>     结果集中列(columns)的总数。

列数据包格式

列数据包格式也分为两种格式,也是通过客户端上传的 CLIENT_PROTOCOL_41 能力标志来觉得的。 如果客户端支持 CLIENT_PROTOCOL_41 这个能力标志,服务端返回 ColumnDefinition41 这个列数据包。 如果客户端不支持 CLIENT_PROTOCOL_41 这个能力标志的话,服务端就返回 ColumnDefinition320 这个版本的列数据包。 现在一般都使用 ColumnDefinition41 这个数据包,这个数据包描述如下:

string<lenenc>      catalog				目录 (固定为 "def")
string<lenenc>      schema				数据库
string<lenenc>      table				虚拟表
string<lenenc>      org_table			源表
string<lenenc>      name				虚拟名称
string<lenenc>      org_name			源名称
string<lenenc>      length of fixed-length fields [0c]
2              		character set		字符集
4              		column length		字段的最大长度
1              		type				列类型
2              		flags				标志
1              		decimals			显示的小数位数
2              		filler [00] [00]	两个空占位符

if command was COM_FIELD_LIST {
    int<lenenc>     length of default-values
    string[$len]   	default values
}

行数据包格式

行数据包里面包含了所有列的字段数据,每个列的字段的数据可以通过 string<lenenc> 数据类型的计算的方式获得,其中要注意的是,如果字段长度描述字节的数据等于 0xfe 时,代表这行中这列的数据为 NULL。 下图是行数据包的表现形式:

image.png

代码实现

通过以上的介绍,现在我们可以用代码来实现不用第三方驱动的情况下手动连接 MySQL 服务器,然后发送一条查询 databses 的命令。

Untitled-2022-02-10-1029

下面的是代码片段,完整代码连接:https://gist.github.com/greycodee/22f98464fece7792a83433a1fba58e2a

连接 MySQL 服务器

type MySQLClient struct {
	conn net.Conn
	addr string
	username string
	password	string
}

func (m *MySQLClient) init()  {
	// 连接阶段
	handshake := m.startConn()
	m.sendHandshakeResponse41(handshake)
}


/*
	连接 MySQL 服务器
*/
func (m *MySQLClient) startConn() *HandshakeV10 {
	m.conn, _ = net.Dial("tcp",m.addr)
	initResp := make([]byte,1024)
	readLen, _ := m.conn.Read(initResp)
	return ReadHandShakeV10(initResp[:readLen])
}

解析初始握手数据包

/*
	解析初始握手包 HandShakeV10
*/
func ReadHandShakeV10(data []byte) *HandshakeV10 {
	index := 0
	var h = &HandshakeV10{}
	index+=4
	h.ProtocolVersion= int32(data[index])
	index++
	var serverVersion []byte
	for data[index]!=0 {
		serverVersion = append(serverVersion,data[index])
		index++
	}
	h.ServerVersion = string(serverVersion)
	index++
	connectByte := data[index:index+4]
	for i :=range connectByte{
		h.ConnectionId+=int32(connectByte[i])
	}
	index+=4
	var apdp1 []byte
	apdp1Byte := data[index:index+8]
	for i := range apdp1Byte {
		apdp1 = append(apdp1, apdp1Byte[i])
	}
	h.AuthPluginDataPart_1 = string(apdp1)
	index+=9
	// 能力低2位
	c_flag_low_1 := strings.Split(fmt.Sprintf("%b\n",data[index+1]),"")
	c_flag_low_2 := strings.Split(fmt.Sprintf("%b\n",data[index]),"")

	index+=2
	// 编码获取
	h.CharacterSet = int32(data[index])
	index++
	// 服务器状态
	index+=2
	// 能力高2位
	c_flag_up_1 := strings.Split(fmt.Sprintf("%b\n",data[index+1]),"")
	c_flag_up_2 := strings.Split(fmt.Sprintf("%b\n",data[index]),"")
	var capabilityFlags []string
	capabilityFlags = append(capabilityFlags,c_flag_up_1...)
	capabilityFlags = append(capabilityFlags,c_flag_up_2...)
	capabilityFlags = append(capabilityFlags,c_flag_low_1...)
	capabilityFlags = append(capabilityFlags,c_flag_low_2...)
	index+=2
	if strings.EqualFold("1",capabilityFlags[19]){
		h.AuthPluginDataLen= int32(data[index])
	}
	index++
	index+=10
	if strings.EqualFold("1",capabilityFlags[15]){
		p2Len := 13
		p2len1 := int(h.AuthPluginDataLen-8)
		if p2Len < p2len1 {
			p2Len = p2len1
		}
		h.AuthPluginDataPart_2 = string(data[index:index+p2Len])
		index+=p2Len
	}
	if strings.EqualFold("1",capabilityFlags[19]) {
		var authPlugName []byte
		for data[index] != 0 {
			authPlugName = append(authPlugName,data[index])
			index++
		}
		h.AuthPluginName = string(authPlugName)
	}

	return h
}
type HandshakeV10 struct {
	ProtocolVersion      int32  `protobuf:"varint,1,opt,name=protocol_version,json=protocolVersion,proto3" json:"protocol_version,omitempty"`
	ServerVersion        string `protobuf:"bytes,2,opt,name=server_version,json=serverVersion,proto3" json:"server_version,omitempty"`
	ConnectionId         int32  `protobuf:"varint,3,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"`
	AuthPluginDataPart_1 string `protobuf:"bytes,4,opt,name=auth_plugin_data_part_1,json=authPluginDataPart1,proto3" json:"auth_plugin_data_part_1,omitempty"`
	CharacterSet         int32  `protobuf:"varint,6,opt,name=character_set,json=characterSet,proto3" json:"character_set,omitempty"`
	StatusFlags          int32  `protobuf:"varint,7,opt,name=status_flags,json=statusFlags,proto3" json:"status_flags,omitempty"`
	AuthPluginDataLen    int32  `protobuf:"varint,8,opt,name=auth_plugin_data_len,json=authPluginDataLen,proto3" json:"auth_plugin_data_len,omitempty"`
	AuthPluginDataPart_2 string `protobuf:"bytes,9,opt,name=auth_plugin_data_part_2,json=authPluginDataPart2,proto3" json:"auth_plugin_data_part_2,omitempty"`
	AuthPluginName       string `protobuf:"bytes,10,opt,name=auth_plugin_name,json=authPluginName,proto3" json:"auth_plugin_name,omitempty"`
}

发送初始响应数据包

/*
	发送初始响应数据包 HandshakeResponse41,包含登陆信息
*/
func (m *MySQLClient) sendHandshakeResponse41(serverResp *HandshakeV10) {
	resp := make([]byte,0)
	resp = append(resp, Int32ToBytesOfLittle(19833351)...)
	resp = append(resp, Int32ToBytesOfLittle(16777215)...)
	resp = append(resp, 33)
	reserved := make([]byte,23)
	resp = append(resp, reserved...)
	resp = append(resp, []byte(m.username)...)
	resp = append(resp, 0)
	resp = append(resp, 20)
	resp = append(resp, CalcPassword([]byte(serverResp.AuthPluginDataPart_1+serverResp.AuthPluginDataPart_2)[:20],[]byte(m.password))...)
	resp = append(resp, []byte("mysql_native_password")...)
	resp = append(resp, 0)

	_, _ = m.conn.Write(Pack(resp,1))
	flag := m.handleResponse()
	if flag == 0xff {
		panic("连接失败")
	}
	return
}

判断连接结果

/*
	解析通用响应数据包 OK_Packet、ERR_Packet、数据集
*/
func (m *MySQLClient) handleResponse() uint8 {
	resp := make([]byte,1024)
	readLen, _ := m.conn.Read(resp)
	data := resp[:readLen]
	data = data[4:]
	switch data[0] {
	case 0x00:
		fmt.Println("成功")
		return 0x00
	case 0xff:
		fmt.Println("失败")
		return 0xff
	default:
		parseResultSet(data)
		return 0xfe
	}
}

发送命令

/*
	CommandQuery
	发送 COM_QUERY 命令,并读取数据
*/
func (m *MySQLClient) CommandQuery(sql string) {
	resp := make([]byte,0)
	resp = append(resp, 3)
	resp = append(resp, []byte(sql)...)
	_, _ = m.conn.Write(Pack(resp,0))
	m.handleResponse()
}

解析结果集

func parseResultSet(resp []byte)  {
	index := 0
	fieldLen := resp[0]
	index+=1
	headRows := make([]string,0)
	headIndex := 1
	// 读取列数据
	for headIndex <= int(fieldLen){
		n,l := readColumn(resp,index)
		index+=l
		headRows = append(headRows, n)
		headIndex++
	}
	table, err := gotable.Create(headRows...)
	if err != nil {
		fmt.Println("Create table failed: ", err.Error())
		return
	}

	// 读取行内容
	for  {
		// 判断是否是 EOF 数据包
		if resp[index+4] == 0xfe{
			packLen := 0
			for _,v :=range resp[index:index+3]{
				packLen+=int(v)
			}
			if packLen<9 {
				break
			}
		}
		rows,ll := readRow(resp,index, int(fieldLen))
		table.AddRow(rows)
		index+=ll
	}
	// 打印
	fmt.Println(table)
}
func readColumn(data []byte, startIndex int) (name string,length int) {
	packLen := data[startIndex:startIndex+3]
	for i :=range packLen{
		length+=int(packLen[i])
	}
	length += 4

	startIndex+=4
	startIndex+=int(data[startIndex]+1)
	startIndex+=int(data[startIndex]+1)
	startIndex+=int(data[startIndex]+1)
	startIndex+=int(data[startIndex]+1)
	nameLen := int(data[startIndex])
	name = string(data[startIndex+1:startIndex+nameLen+1])
	return
}
func readRow(data []byte, startIndex int, fieldNum int) (name []string,length int) {
	packLen := data[startIndex:startIndex+3]
	for i :=range packLen{
		length+=int(packLen[i])
	}
	length += 4
	startIndex+=4
	f:=0
	for f < fieldNum{
		dataLen := 0
		// 计算字节数据长度
		if data[startIndex] < 0xfb {
			// NULL
			dataLen = int(data[startIndex])
		}else if data[startIndex] == 0xfc {
			for _,v := range data[startIndex+1:startIndex+3]{
				dataLen+=int(v)
			}
		}else if data[startIndex] == 0xfd {
			for _,v :=range data[startIndex+1:startIndex+5]{
				dataLen+=int(v)
			}
		}else if data[startIndex] == 0xfe {
			for _,v :=range data[startIndex+1:startIndex+9]{
				dataLen+=int(v)
			}
		}
		name = append(name, string(data[startIndex+1:startIndex+dataLen+1]))
		startIndex += dataLen+1
		f++
	}
	return
}

控制台结果输出

执行上面的代码后,控制台就会输出所有的数据库名字

+--------------------+
|      Database      |
+--------------------+
| information_schema |
|      greycode      |
|       mysql        |
| performance_schema |
|        sys         |
+--------------------+

参考资料

https://dev.mysql.com/doc/internals/en/client-server-protocol.html