强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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)

  1. 客户端发起 TCP 连接
  2. 服务器发送 HandshakeV10 数据包,包含协议版本、服务器版本字符串、能力标志(Capability Flags)、认证随机数(Auth Plugin Data)等
  3. 客户端回复 HandshakeResponse41,包含用户名、加密后的密码、数据库名、能力标志、字符集等
  4. 服务器验证认证信息,返回 OK_PacketERR_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.xMySQL 3.x早期版本,使用旧的认证方式
Protocol V9MySQL 3.23引入 4.0 之前的旧握手格式
Protocol V10MySQL 4.1+当前主流版本,引入 HandshakeV10、32 字节随机数、新认证插件
SSL/TLS 支持MySQL 4.1+握手阶段支持 SSL 升级
Auth PluginMySQL 5.5+可插拔认证架构
caching_sha2_passwordMySQL 8.0+默认认证插件变更
X ProtocolMySQL 5.7+基于 Protobuf 的新协议(端口 33060)

Protocol V10 的里程碑意义

从 MySQL 4.1 开始,协议升级为 V10,带来了以下关键改进:

  1. 安全的挑战-应答认证:使用 20 字节 + 13 字节的随机数(scramble),不再明文传输密码
  2. 能力标志(Capability Flags):32 位整数表示客户端/服务器支持的特性
  3. 插件化认证:通过 auth_plugin_name 字段指定认证方式
  4. 事务状态追踪:服务器返回 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_timeout28800 秒非交互式连接的空闲超时
interactive_timeout28800 秒交互式连接的空闲超时
connect_timeout10 秒握手阶段的超时
net_read_timeout30 秒读取数据的超时
net_write_timeout60 秒写入数据的超时
max_connections151最大并发连接数
-- 查看当前超时设置
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 驱动

语言驱动协议实现
Clibmysqlclient官方 C 实现
Gogo-sql-driver/mysql纯 Go 实现
PythonPyMySQL纯 Python 实现
Pythonmysql-connector-pythonOracle 官方实现
JavaMySQL Connector/J纯 Java 实现
Node.jsmysql2JavaScript 实现
Rustsqlx异步 Rust 实现
PHPmysqlndC 实现的原生驱动

学习协议实现的好方法

阅读开源驱动的源码是学习 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 注意事项

重要提醒

  1. 协议版本兼容性:MySQL 8.0 默认使用 caching_sha2_password 认证插件,而 MySQL 5.7 及更早版本使用 mysql_native_password。开发客户端驱动时需同时支持两种认证方式。

  2. 字符集协商:握手阶段协商的字符集决定了后续通信中字符串的编码方式。推荐使用 utf8mb4(字符集 ID = 255)。

  3. 能力标志的重要性:客户端在 HandshakeResponse 中声明的能力标志必须是服务器 HandshakeV10 中声明的子集,否则会导致未定义行为。

  4. 序列号规则:每个新的命令请求从序列号 0 开始,服务器的响应序列号从 1 开始递增。认证阶段(多轮交互)的序列号是连续的。

  5. 半双工限制:由于协议的半双工特性,客户端不能在接收结果集的过程中发送新命令。这影响了连接池的设计。


1.10 业务场景

场景一:理解连接风暴

当应用启动或扩容时,大量连接同时建立,每个连接都要经历完整的握手认证过程。理解握手协议有助于:

  • 估算握手阶段的网络往返次数(RTT)
  • 计算认证加密的 CPU 开销
  • 合理配置连接池大小

场景二:排查连接超时

客户端报告 “Lost connection to MySQL server during query” 时,可能是:

  • net_read_timeout 设置过短
  • 结果集过大,传输时间超过超时值
  • TCP 连接被中间设备(防火墙、负载均衡器)断开

理解协议的数据包格式和超时机制,有助于快速定位根因。

场景三:安全审计

通过分析协议握手过程,可以检查:

  • 是否使用了安全的认证插件
  • 密码是否以安全的方式传输(挑战-应答 vs 明文)
  • 是否启用了 SSL/TLS 加密

1.11 扩展阅读


下一章02 - 握手过程 —— 深入分析 HandshakeV10 数据包的每个字段。