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

推荐订阅源

Engineering at Meta
Engineering at Meta
人人都是产品经理
人人都是产品经理
大猫的无限游戏
大猫的无限游戏
博客园 - 三生石上(FineUI控件)
量子位
腾讯CDC
The Cloudflare Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
云风的 BLOG
云风的 BLOG
Vercel News
Vercel News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
L
LangChain Blog
aimingoo的专栏
aimingoo的专栏
The Hacker News
The Hacker News
T
The Exploit Database - CXSecurity.com
B
Blog
S
SegmentFault 最新的问题
P
Privacy & Cybersecurity Law Blog
T
Threatpost
博客园 - 聂微东
T
Tailwind CSS Blog
The Last Watchdog
The Last Watchdog
C
Check Point Blog
N
Netflix TechBlog - Medium
D
DataBreaches.Net
爱范儿
爱范儿
IT之家
IT之家
S
Secure Thoughts
M
MIT News - Artificial intelligence
NISL@THU
NISL@THU
C
Cisco Blogs
TaoSecurity Blog
TaoSecurity Blog
有赞技术团队
有赞技术团队
A
Arctic Wolf
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
P
Proofpoint News Feed
Spread Privacy
Spread Privacy
Schneier on Security
Schneier on Security
Simon Willison's Weblog
Simon Willison's Weblog
G
GRAHAM CLULEY
雷峰网
雷峰网
Project Zero
Project Zero
博客园 - Franky
H
Heimdal Security Blog
A
About on SuperTechFans
Security Latest
Security Latest
Webroot Blog
Webroot Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Hugging Face - Blog
Hugging Face - Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More

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为例) 读《为什么学生不喜欢上学》 OpenAI Prompt Engineering 摘录和总结 读《打造真正的新产品》 VueJS 总结 Linux 自动挂载 alist 提供的webdav FreeBSD 使用 vm-bhyve 安装Debian虚拟机 FreeBSD 和 Linux 网卡聚合实现提速 GPT 帮我搞定了时区转换问题 长任务系统如何处理? macOS/Linux 编译 InputLeap 使用开源软KVM - synergy-core 解决 macOS 终端hostname一直变化问题 KVM 共享 Intel 集成显卡 PromQL 备忘 读《格鲁夫给经理人的第一课》 读《打开心智》 为什么要把复杂的联表操作拆成多个单表查询? 红包系统的设计 MySQL Index Condition Pushdown Optimization Go mod 简明教程 OpenWRT 使用 Android/iOS USB 网络 搭建旁路由 Golang gRPC 错误处理 编写可维护的单元测试代码 OAuth 2 详解(六):Authorization Code Flow with PKCE OAuth 2 详解(五):Device Authorization Flow OAuth 2 详解(三):Resource Owner Password Credentials Grant OAuth 2 详解(四):Client Credentials Flow OAuth 2 详解(二):Implict Grant Flow OAuth 2 详解(一):简介及 Authorization Code 模式 ElasticSearch 学习笔记 三种git流程以及发版模型 错误处理实践 权限模型(RBAC/ABAC) OIDC(OpenID Connect) 简介 任务队列简介 PostgreSQL 操作笔记 使用Drone CI构建CI/CD系统 Golang migrate 做数据库变更管理 使用PostgreSQL做搜索引擎 Nginx 源码阅读(三): 连接池、内存池 Nginx 源码阅读(二): 请求处理 Nginx 源码阅读(一): 启动流程 Go 泛型简明教程 KVM 显卡穿透给 Windows 使用 HTTP Router 处理 Telegram Bot 按钮回调 使用反射(reflect)对结构体赋值 GIN 是如何绑定参数的 你好 2022(2021 年终总结) 用Go导入大型CSV到PostgreSQL 使用 OpenWRT 搭建软路由 使用软KVM切换器 barrier 共享键鼠 SQL 防注入及原理 使用 gomock 测试 Go 代码 gevent不是黑魔法(二): gevent 实现 gevent不是黑魔法(一): greenlet 实现 用 entgo 替代 gorm 应用内使用crontab不是那么方便 单测时要不要 mock 数据库? Sentry 自建指南 用selenium完成自动化任务 用闲置的安卓手机做垃圾电话短信过滤 推荐三个时间管理工具 一次事故反思 当JS遇到uint64:JS整数溢出问题 SQLite3 存储以及ACID原理 Redis源码阅读:pub/sub实现 Redis源码阅读:zset实现 Redis源码阅读:bitmap 位图的运算 Redis源码阅读:set是怎么做交并集运算的?
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中是如何实现的。同时了解了半双工协议这个概念,也知道了正是因为 半双工通信,我们平时使用数据库的时候,才需要涉及到连接池的管理。