01 - MySQL 协议概述
第 01 章:MySQL 协议概述
1.1 什么是 MySQL 协议?
MySQL 协议(MySQL Client/Server Protocol)是 MySQL 客户端与服务器之间进行通信的应用层协议。它基于 TCP/IP 传输层,定义了数据包的格式、命令的编码方式、结果集的返回结构等。
协议栈层次
┌─────────────────────────────────────────────┐
│ 应用层 (SQL 语句) │
├─────────────────────────────────────────────┤
│ MySQL 协议层 (应用层协议) │
│ 握手认证 / 命令发送 / 结果集返回 │
├─────────────────────────────────────────────┤
│ TLS/SSL 层 (可选加密) │
├─────────────────────────────────────────────┤
│ TCP/IP 传输层 │
├─────────────────────────────────────────────┤
│ 网络链路层 │
└─────────────────────────────────────────────┘
MySQL 协议运行在 TCP 之上,默认端口 3306。连接建立后,客户端与服务器之间通过请求-响应模式交互,所有数据以 MySQL 自定义的数据包(Packet)格式传输。
协议特性
| 特性 | 说明 |
|---|---|
| 传输方式 | 基于 TCP,面向连接 |
| 默认端口 | 3306 |
| 通信模式 | 请求-响应(Request-Response) |
| 数据包大小 | 单包最大 16 MB(2^24 - 1 字节) |
| 序列号 | 每个请求/响应包都有序列号(0-255 循环) |
| 字符集协商 | 握手阶段协商,支持 UTF-8、GBK 等 |
| SSL/TLS 支持 | 可选,握手阶段升级 |
| 认证方式 | 插件化(mysql_native_password、caching_sha2_password 等) |
1.2 客户端-服务器通信模型
基本交互流程
MySQL 协议采用经典的**客户端-服务器(Client-Server)**架构。整个通信生命周期如下:
客户端 (Client) 服务器 (Server)
│ │
│ ──── TCP 三次握手 (端口 3306) ──────→ │
│ │
│ ←─── HandshakeV10 (服务器问候) ─────── │ ① 握手阶段
│ ──── HandshakeResponse (客户端响应) ─→ │
│ ←─── OK / ERR ─────────────────────── │
│ │
│ ──── COM_QUERY "SELECT ..." ────────→ │ ② 命令阶段
│ ←─── Column Count ─────────────────── │
│ ←─── Column Definition(s) ─────────── │
│ ←─── Row Data(s) ──────────────────── │
│ ←─── EOF / OK ─────────────────────── │
│ │
│ ──── COM_QUIT ──────────────────────→ │ ③ 断开阶段
│ │
│ ──── TCP 四次挥手 ──────────────────→ │
│ │
连接生命周期
一次完整的 MySQL 连接包含三个阶段:
阶段一:握手与认证(Handshake & Authentication)
- 客户端发起 TCP 连接
- 服务器发送
HandshakeV10数据包,包含协议版本、服务器版本字符串、能力标志(Capability Flags)、认证随机数(Auth Plugin Data)等 - 客户端回复
HandshakeResponse41,包含用户名、加密后的密码、数据库名、能力标志、字符集等 - 服务器验证认证信息,返回
OK_Packet或ERR_Packet
阶段二:命令执行(Command Execution)
连接建立后,客户端可以发送各种命令:
COM_QUERY:执行 SQL 查询COM_STMT_PREPARE:预处理 SQL 语句COM_STMT_EXECUTE:执行预处理语句COM_PING:心跳检测COM_QUIT:断开连接- 等等…
阶段三:连接关闭(Connection Teardown)
客户端发送 COM_QUIT 命令,随后关闭 TCP 连接。
通信模式详解
MySQL 协议是**半双工(Half-Duplex)**的——在同一时刻,数据只能单向流动:要么客户端发给服务器,要么服务器发给客户端。这意味着:
- 客户端发送命令后,必须等待服务器完全返回结果
- 服务器处理请求时,不能同时接收新的命令
- 客户端必须读取完整个结果集后才能发送下一个命令
时间线:
Client ──→ [请求包] ──→ Server
Client ←── [响应包1] ←── Server
Client ←── [响应包2] ←── Server
Client ←── [响应包3] ←── Server
Client ──→ [下一个请求] ──→ Server
注意:这不意味着 TCP 层不能全双工传输,而是 MySQL 协议在应用层规定了这种行为。
1.3 MySQL 协议版本演进
MySQL 协议并非一成不变,它经历了多次重要演进:
| 版本 | 时间 | 关键变化 |
|---|---|---|
| Protocol V3.x | MySQL 3.x | 早期版本,使用旧的认证方式 |
| Protocol V9 | MySQL 3.23 | 引入 4.0 之前的旧握手格式 |
| Protocol V10 | MySQL 4.1+ | 当前主流版本,引入 HandshakeV10、32 字节随机数、新认证插件 |
| SSL/TLS 支持 | MySQL 4.1+ | 握手阶段支持 SSL 升级 |
| Auth Plugin | MySQL 5.5+ | 可插拔认证架构 |
| caching_sha2_password | MySQL 8.0+ | 默认认证插件变更 |
| X Protocol | MySQL 5.7+ | 基于 Protobuf 的新协议(端口 33060) |
Protocol V10 的里程碑意义
从 MySQL 4.1 开始,协议升级为 V10,带来了以下关键改进:
- 安全的挑战-应答认证:使用 20 字节 + 13 字节的随机数(scramble),不再明文传输密码
- 能力标志(Capability Flags):32 位整数表示客户端/服务器支持的特性
- 插件化认证:通过
auth_plugin_name字段指定认证方式 - 事务状态追踪:服务器返回
SERVER_STATUS_AUTOCOMMIT等状态标志
1.4 第一个示例:用 Python 抓取握手包
下面我们用 Python 直接建立 TCP 连接,观察 MySQL 服务器返回的原始握手数据。
示例代码:捕获握手数据包
"""
mysql_handshake_capture.py
连接 MySQL 服务器并捕获握手阶段的原始数据包
"""
import socket
import struct
def capture_handshake(host='127.0.0.1', port=3306):
"""建立 TCP 连接并捕获服务器发送的握手数据包"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
try:
sock.connect((host, port))
print(f"[+] 已连接到 {host}:{port}")
# 接收服务器的握手数据包
# MySQL 数据包格式:3 字节长度 + 1 字节序列号 + 有效载荷
header = sock.recv(4)
if len(header) < 4:
print("[-] 无法读取完整的数据包头")
return
# 解析包头:前 3 字节是小端序的长度,第 4 字节是序列号
payload_length = struct.unpack('<I', header[0:3] + b'\x00')[0]
sequence_number = header[3]
print(f"[+] 包长度: {payload_length} 字节")
print(f"[+] 序列号: {sequence_number}")
# 读取有效载荷
payload = b''
while len(payload) < payload_length:
chunk = sock.recv(payload_length - len(payload))
if not chunk:
break
payload += chunk
print(f"[+] 有效载荷大小: {len(payload)} 字节")
print(f"[+] 原始数据 (前 100 字节): {payload[:100].hex()}")
print()
# 解析握手数据包
parse_handshake_v10(payload)
except socket.timeout:
print("[-] 连接超时")
except ConnectionRefusedError:
print("[-] 连接被拒绝,请确认 MySQL 服务是否运行")
finally:
sock.close()
def parse_handshake_v10(payload):
"""解析 HandshakeV10 数据包"""
offset = 0
# 协议版本号 (1 字节)
protocol_version = payload[offset]
offset += 1
print(f" 协议版本: {protocol_version}")
# 服务器版本字符串 (以 null 结尾)
null_pos = payload.index(b'\x00', offset)
server_version = payload[offset:null_pos].decode('ascii')
offset = null_pos + 1
print(f" 服务器版本: {server_version}")
# 连接 ID (4 字节, 小端序)
connection_id = struct.unpack('<I', payload[offset:offset+4])[0]
offset += 4
print(f" 连接 ID: {connection_id}")
# 认证数据第一部分 (8 字节)
auth_data_part1 = payload[offset:offset+8]
offset += 8
print(f" 认证数据 Part1: {auth_data_part1.hex()}")
# 填充字节 (1 字节, 0x00)
offset += 1
# 能力标志低位 (2 字节)
capability_flags_low = struct.unpack('<H', payload[offset:offset+2])[0]
offset += 2
# 字符集 (1 字节)
character_set = payload[offset]
offset += 1
print(f" 字符集 ID: {character_set}")
# 服务器状态 (2 字节)
status_flags = struct.unpack('<H', payload[offset:offset+2])[0]
offset += 2
print(f" 状态标志: 0x{status_flags:04X}")
# 能力标志高位 (2 字节)
capability_flags_high = struct.unpack('<H', payload[offset:offset+2])[0]
offset += 2
# 合并能力标志
capability_flags = (capability_flags_high << 16) | capability_flags_low
print(f" 能力标志: 0x{capability_flags:08X}")
# 解析部分关键能力标志
parse_capability_flags(capability_flags)
# 认证数据长度 (1 字节) 或保留字节
auth_data_length = payload[offset]
offset += 1
print(f" 认证数据长度: {auth_data_length}")
# 保留字节 (10 字节)
offset += 10
# 认证数据第二部分 (至少 13 字节, 以 null 结尾)
if auth_data_length > 8:
auth_data_part2_length = auth_data_length - 8
else:
# 尽可能读取到 null 结尾
try:
null_pos = payload.index(b'\x00', offset)
auth_data_part2_length = null_pos - offset
except ValueError:
auth_data_part2_length = len(payload) - offset
auth_data_part2 = payload[offset:offset+auth_data_part2_length]
offset += auth_data_part2_length + 1 # +1 跳过 null 终止符
# 合并完整认证数据
full_auth_data = auth_data_part1 + auth_data_part2
print(f" 完整认证数据 ({len(full_auth_data)} 字节): {full_auth_data.hex()}")
# 认证插件名称 (以 null 结尾)
if offset < len(payload):
auth_plugin = payload[offset:-1].decode('ascii', errors='replace')
print(f" 认证插件: {auth_plugin}")
def parse_capability_flags(flags):
"""解析并打印关键能力标志"""
key_flags = {
(1 << 0): "CLIENT_LONG_PASSWORD",
(1 << 1): "CLIENT_FOUND_ROWS",
(1 << 2): "CLIENT_LONG_FLAG",
(1 << 3): "CLIENT_CONNECT_WITH_DB",
(1 << 4): "CLIENT_NO_SCHEMA",
(1 << 5): "CLIENT_COMPRESS",
(1 << 6): "CLIENT_ODBC",
(1 << 7): "CLIENT_LOCAL_FILES",
(1 << 8): "CLIENT_IGNORE_SPACE",
(1 << 9): "CLIENT_PROTOCOL_41",
(1 << 10): "CLIENT_INTERACTIVE",
(1 << 11): "CLIENT_SSL",
(1 << 13): "CLIENT_TRANSACTIONS",
(1 << 14): "CLIENT_RESERVED",
(1 << 15): "CLIENT_SECURE_CONNECTION",
(1 << 16): "CLIENT_MULTI_STATEMENTS",
(1 << 17): "CLIENT_MULTI_RESULTS",
(1 << 18): "CLIENT_PS_MULTI_RESULTS",
(1 << 19): "CLIENT_PLUGIN_AUTH",
(1 << 20): "CLIENT_CONNECT_ATTRS",
(1 << 21): "CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA",
(1 << 22): "CLIENT_CAN_HANDLE_EXPIRED_PASSWORDS",
(1 << 23): "CLIENT_SESSION_TRACK",
(1 << 25): "CLIENT_DEPRECATE_EOF",
(1 << 27): "CLIENT_OPTIONAL_RESULTSET_METADATA",
(1 << 29): "CLIENT_ZSTD_COMPRESSION_ALGORITHM",
(1 << 30): "CLIENT_QUERY_ATTRIBUTES",
(1 << 31): "MULTI_FACTOR_AUTHENTICATION",
}
print("\n 已启用的能力标志:")
for bit, name in sorted(key_flags.items()):
if flags & bit:
print(f" ✓ {name} (0x{bit:08X})")
if __name__ == '__main__':
capture_handshake(host='127.0.0.1', port=3306)
运行示例
# 启动 MySQL 服务(如果未运行)
sudo systemctl start mysql
# 运行捕获脚本
python3 mysql_handshake_capture.py
预期输出
[+] 已连接到 127.0.0.1:3306
[+] 包长度: 78 字节
[+] 序列号: 0
[+] 有效载荷大小: 78 字节
[+] 原始数据 (前 100 字节): 0a382e302e3335...
协议版本: 10
服务器版本: 8.0.35
连接 ID: 12
认证数据 Part1: 4a5b3c2d1e0f6a7b
字符集 ID: 255 (utf8mb4)
状态标志: 0x0002
能力标志: 0xFFFFF7FF
认证数据长度: 21
完整认证数据 (21 字节): 4a5b3c2d1e0f6a7b1122334455667788aabbccddeeff
认证插件: caching_sha2_password
已启用的能力标志:
✓ CLIENT_LONG_PASSWORD (0x00000001)
✓ CLIENT_PROTOCOL_41 (0x00000200)
✓ CLIENT_SECURE_CONNECTION (0x00008000)
✓ CLIENT_PLUGIN_AUTH (0x00080000)
...
1.5 用 Wireshark 抓包分析
如果你更喜欢图形化工具,Wireshark 是分析 MySQL 协议的最佳选择。
抓包步骤
# 方法一:使用 tshark 命令行
sudo tshark -i lo -f "tcp port 3306" -Y "mysql" -V
# 方法二:先抓包保存,再分析
sudo tshark -i lo -f "tcp port 3306" -w mysql_capture.pcap
# 然后在 Wireshark 中打开 mysql_capture.pcap
# 方法三:使用 tcpdump
sudo tcpdump -i lo port 3306 -w mysql_capture.pcap
在 Wireshark 中观察
在 Wireshark 中打开抓包文件后,你会看到:
Frame 1: TCP SYN → 客户端发起连接
Frame 2: TCP SYN-ACK ← 服务器确认
Frame 3: TCP ACK → 三次握手完成
Frame 4: MySQL Protocol ← HandshakeV10 (服务器问候)
Frame 5: MySQL Protocol → Login Request (客户端登录)
Frame 6: MySQL Protocol ← OK (认证成功)
Frame 7: MySQL Protocol → COM_QUERY (SQL 查询)
Frame 8: MySQL Protocol ← Result Set (查询结果)
Frame 9: MySQL Protocol → COM_QUIT (断开连接)
提示:Wireshark 自带 MySQL 协议解析器,能自动识别并解码 MySQL 数据包中的各个字段。
1.6 MySQL 与 TCP 的关系
连接建立
MySQL 使用标准的 TCP 三次握手建立连接。默认监听端口 3306(可通过 port 配置项修改)。
# 查看 MySQL 监听端口
ss -tlnp | grep mysql
# 输出: LISTEN 0 128 *:3306 *:* users:(("mysqld",pid=1234,fd=34))
Keep-Alive 与超时
MySQL 服务器通过以下参数控制连接的生命周期:
| 参数 | 默认值 | 说明 |
|---|---|---|
wait_timeout | 28800 秒 | 非交互式连接的空闲超时 |
interactive_timeout | 28800 秒 | 交互式连接的空闲超时 |
connect_timeout | 10 秒 | 握手阶段的超时 |
net_read_timeout | 30 秒 | 读取数据的超时 |
net_write_timeout | 60 秒 | 写入数据的超时 |
max_connections | 151 | 最大并发连接数 |
-- 查看当前超时设置
SHOW VARIABLES LIKE '%timeout%';
-- 查看当前连接数
SHOW STATUS LIKE 'Threads_connected';
连接的底层实现
在 Linux 上,MySQL 使用以下系统调用管理连接:
服务器端:
socket() → bind() → listen() → accept() → recv()/send() → close()
客户端:
socket() → connect() → send()/recv() → close()
1.7 MySQL X Protocol(扩展协议)
从 MySQL 5.7 开始,MySQL 引入了基于 Protobuf 的 X Protocol,运行在端口 33060。
X Protocol 与传统协议对比
| 特性 | 传统协议 (3306) | X Protocol (33060) |
|---|---|---|
| 序列化格式 | 自定义二进制格式 | Protocol Buffers |
| 数据模型 | 关系型 (SQL) | 文档型 (JSON) + 关系型 |
| 异步支持 | 不支持 | 支持 |
| 流式操作 | 有限 | 完整支持 |
| 语言支持 | 所有主流语言 | MySQL Shell, Node.js, Python, Java |
| 连接复用 | 每连接单请求 | 支持多路复用 |
本教程聚焦于传统协议(端口 3306),它是目前绝大多数应用使用的协议。
1.8 协议实现的现实世界
主流语言的 MySQL 驱动
| 语言 | 驱动 | 协议实现 |
|---|---|---|
| C | libmysqlclient | 官方 C 实现 |
| Go | go-sql-driver/mysql | 纯 Go 实现 |
| Python | PyMySQL | 纯 Python 实现 |
| Python | mysql-connector-python | Oracle 官方实现 |
| Java | MySQL Connector/J | 纯 Java 实现 |
| Node.js | mysql2 | JavaScript 实现 |
| Rust | sqlx | 异步 Rust 实现 |
| PHP | mysqlnd | C 实现的原生驱动 |
学习协议实现的好方法
阅读开源驱动的源码是学习 MySQL 协议最有效的方式:
# 克隆 PyMySQL(纯 Python,易于阅读)
git clone https://github.com/PyMySQL/PyMySQL.git
# 核心协议代码在 pymysql/protocol.py 和 pymysql/connections.py
# 克隆 Go 驱动
git clone https://github.com/go-sql-driver/mysql.git
# 核心协议代码在 packets.go
1.9 注意事项
重要提醒
协议版本兼容性:MySQL 8.0 默认使用
caching_sha2_password认证插件,而 MySQL 5.7 及更早版本使用mysql_native_password。开发客户端驱动时需同时支持两种认证方式。字符集协商:握手阶段协商的字符集决定了后续通信中字符串的编码方式。推荐使用
utf8mb4(字符集 ID = 255)。能力标志的重要性:客户端在
HandshakeResponse中声明的能力标志必须是服务器HandshakeV10中声明的子集,否则会导致未定义行为。序列号规则:每个新的命令请求从序列号 0 开始,服务器的响应序列号从 1 开始递增。认证阶段(多轮交互)的序列号是连续的。
半双工限制:由于协议的半双工特性,客户端不能在接收结果集的过程中发送新命令。这影响了连接池的设计。
1.10 业务场景
场景一:理解连接风暴
当应用启动或扩容时,大量连接同时建立,每个连接都要经历完整的握手认证过程。理解握手协议有助于:
- 估算握手阶段的网络往返次数(RTT)
- 计算认证加密的 CPU 开销
- 合理配置连接池大小
场景二:排查连接超时
客户端报告 “Lost connection to MySQL server during query” 时,可能是:
net_read_timeout设置过短- 结果集过大,传输时间超过超时值
- TCP 连接被中间设备(防火墙、负载均衡器)断开
理解协议的数据包格式和超时机制,有助于快速定位根因。
场景三:安全审计
通过分析协议握手过程,可以检查:
- 是否使用了安全的认证插件
- 密码是否以安全的方式传输(挑战-应答 vs 明文)
- 是否启用了 SSL/TLS 加密
1.11 扩展阅读
- MySQL Internals Manual: Client/Server Protocol
- MySQL Source Code: sql/protocol.cc
- PyMySQL Source Code
- go-sql-driver/mysql Source Code
- Wireshark MySQL Protocol Wiki
- RFC 793: Transmission Control Protocol
下一章:02 - 握手过程 —— 深入分析 HandshakeV10 数据包的每个字段。