惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

雷峰网
雷峰网
爱范儿
爱范儿
宝玉的分享
宝玉的分享
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - Franky
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 三生石上(FineUI控件)
人人都是产品经理
人人都是产品经理
阮一峰的网络日志
阮一峰的网络日志
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Last Week in AI
Last Week in AI
博客园 - 聂微东
大猫的无限游戏
大猫的无限游戏
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
罗磊的独立博客
博客园 - 叶小钗
WordPress大学
WordPress大学
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
酷 壳 – CoolShell
酷 壳 – CoolShell
小众软件
小众软件
博客园 - 司徒正美
博客园 - 【当耐特】
IT之家
IT之家

Jiajun的技术笔记

你好,2026! TiDB 源码阅读(六):TiDB Coprocessor 源码解析 性能优化的核心思想 TiDB 源码阅读(五):索引 TiDB 源码阅读(四):AST、逻辑计划、物理计划 CockroachDB Serverless Architecture podman 无故退出 Cursor Control-L (CTRL-L) Keyboard Shortcuts in Terminal Replace docker with podman Using xmonad with xfce4 A RC script for freebsd frpc 自己动手写一个k8s controller AI 会取代你的(编程)岗位吗? 自建DERP服务器提升Tailscale连接速度(使用Nginx转发) 自动升级Docker容器 再读《程序员修炼之道-从小工到专家》 让浏览器下载文件 再读《软件随想录》/《黑客与画家》/《软技能》 HTTP 压力测试中的 Coordinated Omission 2的补码 编程语言中的 context 是什么? flutter macOS 构建出错 Flatpak 使用小记 Golang CAS 操作是怎么实现的 PostgreSQL 当MQ来使用 Clash 结合 工作VPN 的网络设计 使用 PostgreSQL 搭建 JuiceFS PostgreSQL 配置优化和日志分析 有GitHub Copilot?那就可以搭建你的ChatGPT4服务 窗口函数的使用(以PG为例)
TiDB 源码阅读(二):MySQL协议概览
Jiajun Huang · 2025-10-02 · via Jiajun的技术笔记

今天我们来结合TiDB源码,一起看看MySQL通信协议大概的样子,不会深究到每一个字节,有个大概了解即可。

部分内容是AI生成,写的挺好的我就懒得敲键盘了。

概述

MySQL 通信协议是 MySQL 客户端(如命令行工具、JDBC 驱动、ORM 框架等)与服务器(mysqld)进行交互时所遵循的一套规则。 这是一套基于 TCP/IP 的二进制协议,设计目标是轻量、高效。

协议的核心是请求-响应模型:客户端发送一个命令包,服务器返回一个或多个响应包。

协议分层与连接生命周期

一次完整的 MySQL 会话可以分为以下几个阶段:

1. 连接建立阶段

  1. TCP 三次握手:客户端与服务器的 3306 端口建立 TCP 连接。
  2. 握手初始化
    • 服务器主动发送一个 Greeting(或 Handshake Initialization 包)给客户端。这个包包含了:
      • 协议版本
      • 服务器版本(例如:8.0.33
      • 服务器线程 ID
      • 随机生成的 Auth Seed(用于后续的密码加密)
      • 服务器能力标志(如是否支持 SSL、是否使用 4.1 认证协议等)
  3. 客户端认证
    • 客户端收到 Greeting 包后,发送一个 Login Request。这个包包含了:
      • 客户端能力标志
      • 用户名
      • 加密后的密码(使用 Auth Seed 和用户密码通过算法生成)
      • 默认数据库名称(可选)
      • 字符集信息
  4. 认证结果
    • 服务器验证客户端提供的凭据。如果成功,返回一个 OK Packet;如果失败,返回一个 ERR Packet,并关闭连接。

注意:从 MySQL 8.0.4 开始,默认的认证插件从 mysql_native_password 切换到了 caching_sha2_password。这两者使用的密码加密算法不同,这也是很多老版本客户端/驱动连接新版本服务器时出现 authentication protocol 错误的原因。

2. 命令执行阶段

认证成功后,连接进入命令执行阶段。这个阶段的通信模式固定:

COM_QUERY 命令(文本协议) 这是最常用的命令,用于执行 SQL 语句(如 SELECT, INSERT, UPDATE 等)。

  1. 客户端发送 COM_QUERY:包内容就是纯文本的 SQL 语句。
  2. 服务器响应:响应类型取决于 SQL 语句的类型。
    • 对于 SELECT / SHOW 等返回数据的语句
      • 第一步:发送一个 Column Definition,描述结果集的列结构(列名、类型、长度等)。有多少列,就会发送多少个这样的包。
      • 第二步:发送一个 EOF Packet(在旧协议中)或直接开始发送数据行,来标识列定义部分的结束。
      • 第三步:发送多个 Row Data,每个包包含一行的数据(二进制格式,按照列定义解析)。
      • 第四步:发送一个 EOF PacketOK Packet 来标识结果集的结束,并包含附加信息如 affected rows, last insert id, warnings 等。
    • 对于 INSERT / UPDATE / DELETE 等不返回数据的语句
      • 服务器直接返回一个 OK Packet,其中包含了受影响的行数、上次插入的 ID 等信息。
    • 对于出错的语句
      • 服务器返回一个 ERR Packet,其中包含错误码和错误信息。

COM_STMT_PREPARECOM_STMT_EXECUTE 命令(二进制协议/预处理语句) 这是更高效的命令执行方式,主要用于防止 SQL 注入和提高重复查询的性能。

  1. 准备阶段:客户端发送 COM_STMT_PREPARE 包,包含带占位符(?)的 SQL 语句。
    • 服务器解析 SQL,生成一个预处理语句,并返回一个 STMT_PREPARE_OK 包,其中包含了语句 ID、参数个数、结果集列数等信息。
  2. 参数绑定:客户端使用语句 ID,发送 COM_STMT_SEND_LONG_DATA(用于发送 BLOB 等长数据)和 COM_STMT_EXECUTE 包。在 EXECUTE 包中,客户端以二进制形式发送每个占位符参数的值。
  3. 执行阶段:服务器收到 COM_STMT_EXECUTE 后,使用绑定的参数执行预处理好的语句,返回结果的过程与 COM_QUERY 类似(列定义、数据行、EOF/OK)。
  4. 销毁阶段:执行完毕后,客户端可以发送 COM_STMT_CLOSE 来释放服务器端的资源。

文本协议 vs. 二进制协议 * 文本协议:SQL 和结果集都以字符串形式传输,客户端需要做字符串到特定数据类型的解析和转换。 * 二进制协议:参数和数据以原生二进制格式传输,效率更高。并且通过预处理,可以避免 SQL 注入,因为数据和指令是分开发送的。

3. 连接关闭阶段

  • 客户端可以发送 COM_QUIT 命令,优雅地关闭连接。
  • 服务器或客户端任何一方也可以直接关闭底层的 TCP 连接。

由此我们可以看出来,MySQL通信协议是半双工协议,此处我们需要了解一下什么是半双工:

半双工

比喻: 像对讲机或一条单车道的桥梁。

定义: 通信的双方都可以发送和接收数据,但不能同时进行。在同一时刻,只能有一方在说话,另一方必须听着。当一方说完后,需要说一个“完毕”(并释放信道),另一方才能开始说话。

关键特点: 轮流传输。存在明确的“发言权”切换。

全双工

比喻: 像电话通话或一条双车道的桥梁。

定义: 通信的双方都可以同时发送和接收数据。你说你的,我听我的,我也可以同时说我的,你听你的。两个方向的数据流互不干扰。

关键特点: 并发传输。发送和接收是完全独立的。

一开始我以为只有MySQL是这样,但是搜索一番发现PG也是半双工,想想也是,我们用PG、MySQL都要调节连接池参数,半双工就是原因 所在,因为建立一个连接以后,这个连接上只能处于一种状态,要么发送,要么接收数据,当并发上来以后,如果不使用连接池,就会 阻塞和等待。


数据包结构

所有在网络上传输的 MySQL 数据包都有统一的头部结构:

字段 长度(字节) 描述
Payload Length 3 数据包体(Payload)的长度(小端序)。
Sequence ID 1 数据包序列号。从 0 开始,每发送一个新的请求/响应就加 1,用于检测数据包是否丢失或乱序。
Payload Payload Length 实际的数据,可能是命令、响应数据、错误信息等。

源码分析

我们此处主要看看握手的代码:

// handshake works like TCP handshake, but in a higher level, it first writes initial packet to client,
// during handshake, client and server negotiate compatible features and do authentication.
// After handshake, client can send sql query to server.
func (cc *clientConn) handshake(ctx context.Context) error {
    // 服务端主动发送Greeting包
	if err := cc.writeInitialHandshake(ctx); err != nil {
		if errors.Cause(err) == io.EOF {
			logutil.Logger(ctx).Debug("Could not send handshake due to connection has be closed by client-side")
		} else {
			logutil.Logger(ctx).Debug("Write init handshake to client fail", zap.Error(errors.SuspendStack(err)))
		}
		return err
	}
    // 客户端发送Login请求,服务端读取并校验
	if err := cc.readOptionalSSLRequestAndHandshakeResponse(ctx); err != nil {
		err1 := cc.writeError(ctx, err)
		if err1 != nil {
			logutil.Logger(ctx).Debug("writeError failed", zap.Error(err1))
		}
		return err
	}

	// MySQL supports an "init_connect" query, which can be run on initial connection.
	// The query must return a non-error or the client is disconnected.
	if err := cc.initConnect(ctx); err != nil {
		logutil.Logger(ctx).Warn("init_connect failed", zap.Error(err))
		initErr := servererr.ErrNewAbortingConnection.FastGenByArgs(cc.connectionID, "unconnected", cc.user, cc.peerHost, "init_connect command failed")
		if err1 := cc.writeError(ctx, initErr); err1 != nil {
			terror.Log(err1)
		}
		return initErr
	}

    // 服务端验证通过,返回OK
	data := cc.alloc.AllocWithLen(4, 32)
	data = append(data, mysql.OKHeader)
	data = append(data, 0, 0)
	if cc.capability&mysql.ClientProtocol41 > 0 {
		data = dump.Uint16(data, mysql.ServerStatusAutocommit)
		data = append(data, 0, 0)
	}

	err := cc.writePacket(data)
	cc.pkt.SetSequence(0)
	if err != nil {
		err = errors.SuspendStack(err)
		logutil.Logger(ctx).Debug("write response to client failed", zap.Error(err))
		return err
	}

	err = cc.flush(ctx)
	if err != nil {
		err = errors.SuspendStack(err)
		logutil.Logger(ctx).Debug("flush response to client failed", zap.Error(err))
		return err
	}

    // ...

	return err
}

另外上一篇我们看到 dispatch 函数中,根据第一个字节来区分不同类型的数据包,然后调用不同的函数来处理,此处就不再展开代码了。

对返回结果写入的代码位于 pkg/server/conn.go 中的 func (cc *clientConn) writeChunksWithFetchSize(ctx context.Context, rs resultset.CursorResultSet, serverStatus uint16, fetchSize int) error 函数中。

总结

这篇文章中,我们概览了一下MySQL协议,并且看了一下TiDB中是如何实现的。同时了解了半双工协议这个概念,也知道了正是因为 半双工通信,我们平时使用数据库的时候,才需要涉及到连接池的管理。