MySQL 是怎样通讯的?

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 我们平常使用数据库的场景一般是程序里面代码直接连接使用,然后进行 CRUD 操作。或者使用有 GUI 界面的数据库软件来手动操作数据库, 这类软件有 DataGrip、Navicat等等...。平常很少关心它们的底层数据交互是怎么样的,相信你看了这篇文章一定能有大概的了解。本篇文章的代码使用 Go 语言来实现 MySQL 的协议。

网络异常,图片无法展示
|

前言

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

协议简介

MySQL 协议一般分为两个阶段,一个是连接阶段,一个是命令阶段。 连接阶段主要是客户端和服务端进行相互认证的阶段,就像我们平常登陆某个网站的一个操作。 命令阶段主要是客户端向服务端进行的一些指令的发送,然后服务端处理指令并返回结果的一个过程。 在客户端和服务端发送的数据包中,前 3 个字节表示这个数据包的大小,所以这里就有一个问题,就是它有一个大小的限制,数据包大小不能超过16777215 (2^{24}-12241) 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 服务端返回的数据包

网络异常,图片无法展示
|
可以看到前 4 个字节为 16 进制的数据: 4e 00 00 00 ,表示了这个数据包大小为 78 字节,序号 ID 为 0。具体的字段字节大小和描述如下表示:

字段名 字节数据长度(byte) 描述
Protocol 1 初始握手包协议版本,可以根据这个字节数据来判断握手包的协议版本,然后按不同版本来处理接下来的数据
Version 直到遇到字节数据为 0 的时候停止 MySQL 服务端版本描述字符串
Thread ID 4 连接 ID
Slat(第一段) 8 用于处理后续客户端的密码加密
filler 1 填充一个字节,默认为 0
Service Capability(Low) 2 服务端能力标志,一共有 4 个字节,这里表示的是低 2 位字节的数据
Server Language 1 服务端字符编码
Server Status 2 服务端状态
Service Capability(Upper) 2 服务端能力标志,这里表示的是高 2 位字节的数据
Authentication Plugin Length 1 身份验证插件长度
Unused 10 预留的 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进制】
1 CLIENT_LONG_PASSWORD 0x1
2 CLIENT_FOUND_ROWS 0x2
3 CLIENT_LONG_FLAG 0x4
4 CLIENT_CONNECT_WITH_DB 0x8
5 CLIENT_NO_SCHEMA 0x10
6 CLIENT_COMPRESS 0x20
7 CLIENT_ODBC 0x40
8 CLIENT_LOCAL_FILES 0x80
9 CLIENT_IGNORE_SPACE 0x100
10 CLIENT_PROTOCOL_41 0x200
11 CLIENT_INTERACTIVE 0x400
12 CLIENT_SSL 0x800
13 CLIENT_IGNORE_SIGPIPE 0x1000
14 CLIENT_TRANSACTIONS 0x2000
15 CLIENT_RESERVED 0x4000
16 CLIENT_SECURE_CONNECTION 0x8000
17 CLIENT_MULTI_STATEMENTS 0x10000
18 CLIENT_MULTI_RESULTS 0x20000
19 CLIENT_PS_MULTI_RESULTS 0x40000
20 CLIENT_PLUGIN_AUTH 0x80000
21 CLIENT_CONNECT_ATTRS 0x100000
22 CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA 0x200000
23 CLIENT_CAN_HANDLE_EXPIRED_PASSWORDS 0x400000
24 CLIENT_SESSION_TRACK 0x800000
25 CLIENT_DEPRECATE_EOF 0x1000000

当我们要发送客户端支持的能力标志时,只要把所有支持的能力标志的值相加,然后转换为 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 官网给出的一张图,这张图大致的描述了客户端和服务端连接的流程

网络异常,图片无法展示
|
本篇文章就讲下简单的明文连接,不管是明文连接和加密连接,客户端都必须返回 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_password CLIENT_SECURE_CONNECTION
明文认证 mysql_clear_password CLIENT_PLUGIN_AUTH
Windows 原生身份验证 authentication_windows_client CLIENT_PLUGIN_AUTH
SHA256 sha256_password CLIENT_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 的值

发送命令

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

一般我们用的最多的就是 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。 下图是行数据包的表现形式:

网络异常,图片无法展示
|

代码实现

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

网络异常,图片无法展示
|

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

连接 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         |
+--------------------+
相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
3天前
|
NoSQL 关系型数据库 MySQL
微服务架构下的数据库选择:MySQL、PostgreSQL 还是 NoSQL?
在微服务架构中,数据库的选择至关重要。不同类型的数据库适用于不同的需求和场景。在本文章中,我们将深入探讨传统的关系型数据库(如 MySQL 和 PostgreSQL)与现代 NoSQL 数据库的优劣势,并分析在微服务架构下的最佳实践。
|
5天前
|
存储 SQL 关系型数据库
使用MySQL Workbench进行数据库备份
【9月更文挑战第13天】以下是使用MySQL Workbench进行数据库备份的步骤:启动软件后,通过“Database”菜单中的“管理连接”选项配置并选择要备份的数据库。随后,选择“数据导出”,确认导出的数据库及格式(推荐SQL格式),设置存储路径,点击“开始导出”。完成后,可在指定路径找到备份文件,建议定期备份并存储于安全位置。
65 11
|
29天前
|
SQL 关系型数据库 MySQL
【揭秘】MySQL binlog日志与GTID:如何让数据库备份恢复变得轻松简单?
【8月更文挑战第22天】MySQL的binlog日志记录数据变更,用于恢复、复制和点恢复;GTID为每笔事务分配唯一ID,简化复制和恢复流程。开启binlog和GTID后,可通过`mysqldump`进行逻辑备份,包含binlog位置信息,或用`xtrabackup`做物理备份。恢复时,使用`mysql`命令执行备份文件,或通过`innobackupex`恢复物理备份。GTID模式下的主从复制配置更简便。
124 2
|
24天前
|
弹性计算 关系型数据库 数据库
手把手带你从自建 MySQL 迁移到云数据库,一步就能脱胎换骨
阿里云瑶池数据库来开课啦!自建数据库迁移至云数据库 RDS原来只要一步操作就能搞定!点击阅读原文完成实验就可获得一本日历哦~
|
28天前
|
关系型数据库 MySQL 数据库
RDS MySQL灾备服务协同解决方案构建问题之数据库备份数据的云上云下迁移如何解决
RDS MySQL灾备服务协同解决方案构建问题之数据库备份数据的云上云下迁移如何解决
|
6天前
|
存储 SQL 关系型数据库
一篇文章搞懂MySQL的分库分表,从拆分场景、目标评估、拆分方案、不停机迁移、一致性补偿等方面详细阐述MySQL数据库的分库分表方案
MySQL如何进行分库分表、数据迁移?从相关概念、使用场景、拆分方式、分表字段选择、数据一致性校验等角度阐述MySQL数据库的分库分表方案。
一篇文章搞懂MySQL的分库分表,从拆分场景、目标评估、拆分方案、不停机迁移、一致性补偿等方面详细阐述MySQL数据库的分库分表方案
|
25天前
|
人工智能 小程序 关系型数据库
【MySQL】黑悟空都掌握的技能,数据库隔离级别全攻略
本文以热门游戏《黑神话:悟空》为契机,深入浅出地解析了数据库事务的四种隔离级别:读未提交、读已提交、可重复读和串行化。通过具体示例,展示了不同隔离级别下的事务行为差异及可能遇到的问题,如脏读、不可重复读和幻读等。此外,还介绍了在MySQL中设置隔离级别的方法,包括全局和会话级别的调整,并通过实操演示了各隔离级别下的具体效果。本文旨在帮助开发者更好地理解和运用事务隔离级别,以提升数据库应用的一致性和性能。
103 2
【MySQL】黑悟空都掌握的技能,数据库隔离级别全攻略
|
30天前
|
数据可视化 关系型数据库 MySQL
Mysql8 如何在 Window11系统下完成跳过密钥校验、完成数据库密码的修改?
这篇文章介绍了如何在Windows 11系统下跳过MySQL 8的密钥校验,并通过命令行修改root用户的密码。
Mysql8 如何在 Window11系统下完成跳过密钥校验、完成数据库密码的修改?
|
27天前
|
SQL 关系型数据库 MySQL
【MySQL 慢查询秘籍】慢SQL无处遁形!实战指南:一步步教你揪出数据库性能杀手!
【8月更文挑战第24天】本文以教程形式深入探讨了MySQL慢SQL查询的分析与优化方法。首先介绍了如何配置MySQL以记录执行时间过长的SQL语句。接着,利用内置工具`mysqlslowlog`及第三方工具`pt-query-digest`对慢查询日志进行了详细分析。通过一个具体示例展示了可能导致性能瓶颈的查询,并提出了相应的优化策略,包括添加索引、缩小查询范围、使用`EXPLAIN`分析执行计划等。掌握这些技巧对于提升MySQL数据库性能具有重要意义。
54 1
|
29天前
|
关系型数据库 MySQL Linux
在Linux中,如何配置数据库服务器(如MySQL或PostgreSQL)?
在Linux中,如何配置数据库服务器(如MySQL或PostgreSQL)?

热门文章

最新文章