第四章 客户端/服务器通信
# 第四章 客户端/服务器通信
在本章中,我们将讨论MySQL中客户端/服务器通信的详细内容。目的是让你能够查看客户端/服务器通信的二进制转储,并理解其中发生了什么。如果你正在尝试编写一个MySQL代理服务器、一个用于审计网络中MySQL流量的安全应用程序,或者由于某些原因需要了解MySQL客户端/服务器协议底层细节的其他程序,本章内容也会对你有所帮助。
# 协议概述
服务器在TCP/IP端口或本地套接字上监听连接。当客户端连接时,会进行握手和身份验证。如果验证成功,会话开始。客户端发送一个命令,服务器根据发送的命令类型返回一个数据集或相应的消息。当客户端完成操作后,它会发送一个特殊命令告知服务器操作结束,会话随即终止。
通信的基本单元是应用层数据包。命令由一个数据包组成,而响应可能包含多个数据包。
# 数据包格式
数据包有两种类型:压缩数据包和未压缩数据包。在握手阶段,会根据客户端和服务器的功能及设置,决定本次会话使用哪种数据包。
此外,无论是否选择压缩,数据包都分为两类:客户端发送的命令和服务器返回的响应。
服务器响应数据包又分为四类:数据数据包、数据流结束数据包、成功报告(OK)数据包和错误消息数据包。
所有数据包都有一个通用的4字节头部,如表4-1所示。 表4-1. 未压缩数据包的通用4字节头部
长度 | 偏移量 | 描述 |
---|---|---|
3 | 0 | 数据包主体长度,低位字节在前存储。 |
1 | 3 | 数据包序列号。每个新命令都会重置序列号。虽然底层传输协议会确保数据包顺序正确,但此字段用于应用逻辑的合理性检查。 |
压缩数据包会有一个额外的3字节字段,同样低位字节在前,该字段包含紧随其后的压缩数据包主体部分的长度。未压缩数据包的主体紧跟在头部之后。
压缩使用ZLIB库(见http://www.zlib.net)。压缩数据包的主体正是以未压缩主体作为参数调用compress()
函数的返回值。然而,如果压缩后的主体不比未压缩的主体小,或者由于某些原因(例如内存不足)compress()
函数调用失败,数据包主体可能不会被压缩存储。在这种情况下,未压缩长度字段将包含0。
需要注意的是,即使在这种情况下,仍然会使用压缩格式,这就导致每个数据包会浪费3字节。因此,如果一个会话主要使用较小或难以压缩的数据包,关闭压缩功能会使通信速度更快。
你可能已经注意到,3字节的字段将数据包主体长度限制为16MB。如果你需要发送更大的数据包怎么办呢?在3.23及更早版本中,这是不可能的。4.0版本对协议进行了兼容性改进,克服了这个限制。如果数据包的长度大于在sql/net_serv.cc
中定义为224 - 1的MAX_PACKET_LENGTH
的值,数据包会被拆分成多个较小的数据包,其中每个数据包的主体长度为MAX_PACKET_LENGTH
,最后一个数据包的主体长度则小于MAX_PACKET_LENGTH
。即使最后一个短数据包的主体长度必须为0,它也始终会存在。它用于指示该大数据包的数据流中不再有其他数据包部分。
# MySQL协议与操作系统层之间的关系
如果你尝试在MySQL端口上运行网络嗅探器,你会发现有时一个TCP/IP数据包中会包含几个MySQL协议数据包,有时一个MySQL数据包会跨越几个TCP/IP层数据包,而有时一个MySQL数据包恰好能放入一个TCP/IP数据包中。如果你设法拦截本地套接字流量,也会观察到类似的现象。有些缓冲区写入操作恰好包含一个数据包,而其他操作可能包含几个数据包。如果底层套接字缓冲区写入操作对一次能处理的最大字节数有限制,你可能还会看到一个MySQL数据包通过几次缓冲区写入操作进行传输。
为了理解这种现象的原理,让我们来看看服务器或客户端用于发送数据包的API。数据包通过调用sql/net_serv.cc
中定义的my_net_write()
函数放入网络缓冲区。当缓冲区达到容量时,其内容将被刷新,这会导致在套接字上进行操作系统的write()
调用 —— 如果缓冲区的内容无法一次性写入套接字,可能会进行一系列的write()
调用。在操作系统层面,这可能会导致发送一个或多个数据包,具体取决于在操作系统协议约束下容纳数据量所需的操作。
在某些情况下,网络缓冲区中的数据需要立即发送给客户端。这时,会调用sql/net_serv.cc
中定义的net_flush()
函数。
# 认证握手
客户端和服务器之间的会话始于认证握手。在开始之前,服务器会检查客户端连接的主机是否被允许连接到本服务器。如果不允许,服务器会向客户端发送一个错误消息数据包,通知其主机不被允许连接。
如果主机验证成功,服务器会发送一个问候数据包,该数据包具有标准的4字节头部,数据包序列号设置为0,主体格式如表4-2所示。 表4-2. 服务器问候数据包的字段
主体中的偏移量 | 长度 | 描述 |
---|---|---|
0 | 1 | 协议版本号。在最新版本中为十进制10(0x0A)。尽管在4.0和4.1版本中协议有一些变化,但协议版本号保持不变,因为这些变化是完全向后兼容的。 |
1 | ver_len = strlen(server_version) + 1 | 以零结尾的服务器版本字符串。长度可变,根据“长度”列中的公式计算。后续偏移量取决于此字段的长度。 |
ver_len + 1 | 4 | 处理此连接的线程的内部MySQL ID,低位字节在前。 |
ver_len + 5 | 9 | 在4.0及更早版本中,是完整的随机种子字符串。在4.1及更高版本中,是20字节随机种子字符串的前8个字节。末尾有一个终止符0。从4.1版本开始,此字段的长度由include/mysql_com.h 中定义的SCRAMBLE_LENGTH_323 的值控制。在早期版本中,该宏定义在sql/sql_parse.cc 中,名为SCRAMBLE_LENGTH 。加上终止符0字节后,字符串的长度比宏的值大1。 |
ver_len + 14 | 2 | 服务器功能位掩码,低位字节在前。不同位的含义见表4-5。 |
表4-2. 服务器问候数据包的字段(续) | ||
主体中的偏移量 | 长度 | 描述 |
--- | --- | --- |
ver_len + 16 | 1 | 默认字符集代码,更准确地说是默认排序规则的代码。字符集排序规则是一组定义字符顺序的规则。在4.1版本中,可以通过执行SHOW COLLATION LIKE '%' 获取可用排序规则及其代码的列表。 |
ver_len + 17 | 2 | 服务器状态位掩码,低位字节在前。用于报告服务器是否处于事务或自动提交模式、多语句查询是否有其他结果,或者查询优化时是否使用了合适的索引(或某些索引)。详细信息,请参阅include/mysql_com.h 中的SERVER_* 值。 |
ver_len + 19 | 13 | 保留供将来使用。目前全部置零。 |
ver_len + 32 | 13 | 仅在4.1及更高版本中存在。随机种子字符串的其余部分,以零字节结尾。长度等于SCRAMBLE_LENGTH - SCRAMBLE_LENGTH_323 + 1 。 |
客户端用一个凭证数据包进行响应。4.0及更早版本与4.1及更高版本的格式有所不同。表4-3展示了4.1之前版本的格式。表4-4展示了4.1及更高版本中,如果客户端理解并愿意使用4.1协议时的格式。 表4-3. MySQL 4.0及更早版本客户端凭证数据包的字段
主体中的偏移量 | 长度 | 描述 |
---|---|---|
0 2 | 2 3 | 客户端的协议功能位掩码,低位字节在前。 客户端愿意发送或接收的最大数据包长度。值为零表示客户端除了协议中已有的限制外,没有其他自身的限制。 |
5 | 可变;见描述 | 凭证字符串,格式如下:以零结尾的MySQL用户名,如果密码不为空,则接着是加密后的密码(8字节)。可选地,后面可以跟初始数据库名称,在这种情况下,在XOR加密后的密码后立即添加一个零字节终止符,然后是没有终止符的数据库名称字符串。 |
表4-4. MySQL 4.1及更高版本客户端凭证数据包的字段 | ||
主体中的偏移量 | 长度 | 描述 |
--- | --- | --- |
0 | 4 | 客户端的协议功能位掩码,低位字节在前。 |
4 | 4 | 客户端愿意发送或接收的最大数据包长度。值为零表示客户端除了协议中已有的限制外,没有其他自身的限制。 |
8 | 1 | 客户端的默认字符集(更准确地说是排序规则)代码。 |
9 | 23 | 保留空间;目前全部置零。 |
32 | 可变;见描述 | 凭证字符串,格式如下:以零结尾的用户名,接着是SHA1加密密码的长度(十进制20),然后是密码的值(20字节),可选地,后面跟着以零结尾的初始数据库名称。 |
如果客户端和服务器都启用了SSL功能选项,客户端会先发送响应数据包的初始部分,但不包含凭证字符串。当服务器收到该数据包时,它会在功能位掩码中看到SSL功能位已启用,从而知道后续通信应采用SSL。客户端切换到SSL层,并再次安全地发送整个响应数据包。当然,如果不重新发送响应数据包的初始部分会更高效,但由于历史原因,这种小的额外开销使得服务器端代码无需进行全面返工就能保持相当简洁。
一旦服务器收到凭证数据包,它会验证其中的信息。从这一点开始,服务器可以有三种不同的响应方式:
- 如果验证成功,会发送标准的OK响应数据包(详细信息见本章后面的“服务器响应”部分)。
- 如果凭证不符合服务器的预期,会发送标准的错误消息响应。
- 第三种情况是为了支持从4.0版本到4.1版本的过渡。在某些情况下,数据库管理员(DBA)可能已经将客户端和服务器都升级到了4.1版本,但忘记或选择不升级
mysql
数据库中的用户表,该表包含用户名及其相应的密码哈希值。如果该用户的条目使用的是旧格式的密码哈希,就无法使用新的身份验证协议进行身份验证。
在这种情况下,服务器会发送一个特殊数据包,其1字节长的主体包含十进制254,意思是:“请以旧格式发送身份验证凭证”。客户端用一个数据包进行响应,该数据包的主体包含一个以零结尾的加密密码字符串。服务器则以OK或标准错误消息作为回应。
此时,握手完成,客户端开始发出命令。
# 认证协议的安全性
无论是旧协议还是新协议,都不会以明文形式在连接中传输用户密码。然而,旧协议存在一些弱点。首先,知道密码哈希值后,攻击者无需实际知道密码就能进行身份验证。这是因为计算挑战预期响应的方式存在缺陷 —— 它由密码哈希值和挑战值唯一确定(详细信息见sql/password.c
中的scramble_323()
和check_scramble_323()
函数)。因此,如果攻击者能够读取mysql
数据库中的用户表,或者通过其他方式获取存储的密码哈希值,她就可以使用经过特殊修改的MySQL客户端库进行身份验证。
其次,即使无法获取密码哈希值,如果攻击者能多次拦截客户端和服务器之间的认证流量,也可以通过少量尝试猜出正确密码。这是由于旧协议的加密方法存在弱点。加密使用的是自定义的XOR过程(见前面提到的scramble_323()
函数),缺乏真正的加密强度。
4.1版本解决了这些弱点。现在的身份验证方法使用SHA1哈希进行加密,这种方式更难被破解。此外,更改后的挑战 - 验证算法使得仅知道密码哈希值而不知道实际密码就无法进行身份验证。
尽管有了这些改进,也不要对新协议的安全性掉以轻心。仍然建议在防火墙上阻止对MySQL端口的访问,如果无法做到这一点,则要求客户端使用SSL。
# 协议能力位掩码
在认证握手过程中,客户端和服务器会交换关于对方能够或愿意执行操作的信息。这使它们能够调整对对方的预期,避免以不支持的格式发送数据。信息的交换是通过包含协议能力位掩码的字段来完成的。
根据上下文的不同,位掩码的长度可以是4字节或2字节。较新的(4.1及更高版本)客户端和服务器既理解4字节的掩码,也理解2字节的掩码。而较旧的(4.0及更早版本)客户端和服务器只能处理2字节的掩码。
无论版本如何,服务器始终使用2字节的位掩码来声明其能力。尽管较新的客户端和服务器都理解4字节的掩码,但对话中的第一个数据包必须能被任何版本的客户端理解。因此,即使是较新的客户端也期望问候数据包中包含2字节的掩码。
一旦客户端知道它正在与较新的服务器通信,它就可以使用4字节的掩码来声明其能力。然而,如果较新的客户端检测到它正在与较旧的服务器通信,它将仅使用2字节的掩码来声明其能力。自然地,较旧的客户端只能发送2字节的掩码,它们不知道4字节掩码的存在。
表4-5解释了协议能力位掩码中各个位的含义。这些值在include/mysql_com.h
中定义。
表4-5 协议能力位
位宏符号 | 十六进制值 | 描述 |
---|---|---|
CLIENT_LONG_PASSWORD | 0x0001 | 在4.1版本的早期开发中,该位显然用于指示服务器能够使用新的密码格式。 |
CLIENT_FOUND_ROWS | 0x0002 | 通常,在报告UPDATE查询结果时,服务器返回实际被修改的记录数。如果设置了此标志,则要求服务器报告被WHERE子句匹配的记录数。这些记录不一定都会被更新,因为有些记录可能已经包含了所需的值。 |
表4-5 协议能力位(续) | ||
位宏符号 | 十六进制值 | 描述 |
--- | --- | --- |
CLIENT_LONG_FLAG | 0x0004 | 所有现代客户端都会设置此标志。一些旧客户端期望在字段定义记录中只接收1字节的标志,而新客户端期望接收2字节。如果此标志被清除,则表示客户端是旧版本,只需要1字节的字段标志。现代服务器也会设置此标志,以表明它能够以新格式发送字段定义,其中字段标志为2字节。旧版本服务器(3.23之前)不会报告具备此能力。 |
CLIENT_CONNECT_WITH_DB | 0x0008 | 所有现代客户端和服务器都会设置此标志。它表明在认证过程中可以指定初始默认数据库。 |
CLIENT_NO_SCHEMA | 0x0010 | 如果设置了此位,客户端会要求服务器将db_name.table_name.col_name 这种语法视为错误。通常情况下,这种语法是被接受的。 |
CLIENT_COMPRESS | 0x0020 | 设置此位时,表示客户端或服务器能够使用压缩协议。 |
CLIENT_ODBC | 0x0040 | 此位显然是为了指示客户端是ODBC客户端而创建的。目前,它似乎并未被使用。 |
CLIENT_LOCAL_FILES | 0x0080 | 设置此位时,表示客户端能够使用LOAD DATA LOCAL INFILE 上传本地文件。 |
CLIENT_IGNORE_SPACE | 0x0100 | 设置此位时,会通知服务器解析器应忽略标识符与随后的“.”或“(”字符之间的空格字符。此标志支持以下语法:db_name.table_name 或 length (str) ,而这些语法在正常情况下是非法的。 |
CLIENT_PROTOCOL_41 | 0x0200 | 设置此位时,表示客户端或服务器能够使用4.1版本引入的新协议。 |
CLIENT_INTERACTIVE | 0x0400 | 设置此位时,客户端向服务器表明它正在直接接受用户输入的命令。对于服务器来说,这意味着应应用不同的非活动超时值。服务器有两个设置:wait_timeout 和interactive_timeout 。前者用于常规客户端,而后者用于交互式客户端。这种区分是为了解决使用有缺陷的持久连接池的应用程序的问题,这些应用程序会在未先关闭已建立连接的情况下丢失对连接的跟踪,不断创建新连接,最终使服务器的max_connections 限制溢出。解决方法是将wait_timeout 设置为较低的值,以便更快地断开丢失的连接。但遗憾的是,这会产生一个副作用,即过早断开交互式客户端的连接,通过为它们设置单独的超时值解决了这个问题。 |
表4-5 协议能力位(续) | ||
位宏符号 | 十六进制值 | 描述 |
--- | --- | --- |
CLIENT_SSL | 0x0800 | 设置此位时,表示客户端或服务器具备使用SSL的能力。 |
CLIENT_IGNORE_SIGPIPE | 0x1000 | 在3.23和4.0版本的客户端代码中内部使用。SIGPIPE 是一种Unix信号,当进程写入的套接字或管道已被对方关闭时,会发送给该进程。然而,在某些情况下,多线程应用程序中的线程可能会在一些平台上意外收到SIGPIPE 信号。3.23和4.0版本允许客户端程序员选择是否忽略SIGPIPE 信号。4.1版本在客户端初始化期间直接阻塞该信号,此后不再考虑这个问题。 |
CLIENT_TRANSACTIONS | 0x2000 | 当从服务器发送的数据包中设置了此位时,表示服务器支持事务并能够报告事务状态。当此位出现在客户端数据包中时,表示客户端知道服务器支持事务。 |
CLIENT_RESERVED | 0x4000 | 未使用。 |
CLIENT_SECURE_CONNECTION | 0x8000 | 设置此位时,表示客户端或服务器能够使用4.1版本引入的新的SHA1方法进行认证。 |
CLIENT_MULTI_STATEMENTS | 0x10000 | 设置此位时,表示客户端可以在一个查询中发送多个语句,例如:res = mysql_query(con,"SELECT a FROM t1 WHERE id =1; SELECT b FROM t1 WHERE id=3"); |
CLIENT_MULTI_RESULTS | 0x20000 | 设置此位时,表示客户端可以从同一条语句中的多个查询接收结果。 |
CLIENT_REMEMBER_OPTIONS | 0x80000000 | 客户端例程内部使用的标志,从不发送给服务器。 |
# 命令数据包
认证完成后,客户端开始使用命令数据包向服务器发送命令。命令数据包的主体内容如表4-6所示。 表4-6 客户端命令数据包格式
主体偏移量 | 长度 | 描述 |
---|---|---|
0 | 1 | 命令代码。 |
1 | 对于未压缩的数据包,为从包头开始的数据包总长度减1;对于压缩的数据包,为压缩后的主体长度减1。 | 命令的参数(如果有)。 |
命令代码包含在enum server_command
中,在include/mysql_com.h
中定义。命令处理逻辑可以在sql/sql_parse.cc
中的dispatch_command()
函数的switch
语句中找到。
表4-7列出了不同类型的命令及其代码和参数。 表4-7 客户端命令
命令代码枚举值 | 代码数值 | 参数描述 | 命令描述 |
---|---|---|---|
COM_SLEEP | 0 | 无参数 | 客户端从不发送此命令,保留供内部使用。 |
COM_QUIT | 1 | 无参数 | 通知服务器结束会话。由客户端API调用mysql_close() 发出。 |
COM_INIT_DB | 2 | 包含数据库名称的字符串 | 通知服务器将会话的默认数据库更改为参数指定的数据库。由客户端API调用mysql_select_db() 发出。 |
COM_QUERY | 3 | 包含查询语句的字符串 | 通知服务器运行该查询。由客户端API调用mysql_query() 发出。 |
COM_FIELD_LIST | 4 | 包含表名的字符串 | 通知服务器返回指定表的字段列表。这是一个已过时的命令,服务器仍支持它以兼容旧客户端。新客户端使用SHOW FIELDS 查询。 |
COM_CREATE_DB | 5 | 包含数据库名称的字符串 | 通知服务器创建指定名称的数据库。这是一个已过时的命令,服务器仍支持它以兼容旧客户端。新客户端使用CREATE DATABASE 查询。 |
COM_DROP_DB | 6 | 包含数据库名称的字符串 | 通知服务器删除指定名称的数据库。这是一个已过时的命令,服务器仍支持它以兼容旧客户端。新客户端使用DROP DATABASE 查询。 |
表4-7 客户端命令(续) | |||
命令代码枚举值 | 代码数值 | 参数描述 | 命令描述 |
--- | --- | --- | --- |
COM_REFRESH | 7 | 包含重新加载操作位掩码的字节 | 通知服务器刷新表缓存、轮换日志、重新读取访问控制表、清除主机名查找缓存、将状态变量重置为0、清除复制主日志或重置复制从服务器,具体操作取决于位掩码中的选项。由客户端API调用mysql_refresh() 发出。 |
COM_SHUTDOWN | 8 | 无参数 | 通知服务器关闭。由客户端API调用mysql_shutdown() 发出。 |
COM_STATISTICS | 9 | 无参数 | 通知服务器返回一个包含简要状态报告的字符串。由客户端API调用mysql_stat() 发出。 |
COM_PROCESS_INFO | 10 | 无参数 | 通知服务器返回所有正在运行线程的状态报告。这是一个已过时的命令,服务器仍支持它以兼容旧客户端。新客户端使用SHOW PROCESSLIST 查询。 |
COM_CONNECT | 11 | 无参数 | 客户端从不发送此命令,用于内部目的。 |
COM_PROCESS_KILL | 12 | 一个4字节整数,低位字节在前,包含要终止的线程的MySQL ID | 通知服务器终止由参数指定的线程。由客户端API调用mysql_kill() 发出。这是一个已过时的命令,服务器仍支持它以兼容旧客户端。新客户端使用KILL 查询。 |
COM_DEBUG | 13 | 无参数 | 通知服务器将一些调试信息转储到其错误日志中。由客户端API调用mysql_dump_debug_info() 发出。 |
COM_PING | 14 | 无参数 | 通知服务器用一个OK数据包进行响应。如果服务器处于运行状态且可访问,它会进行响应。由客户端API调用mysql_ping() 发出。 |
表4-7 客户端命令(续) | |||
命令代码枚举值 | 代码数值 | 参数描述 | 命令描述 |
--- | --- | --- | --- |
COM_TIME | 15 | 无参数 | 客户端从不发送此命令,用于内部目的。 |
COM_DELAYED_INSERT | 16 | 无参数 | 客户端从不发送此命令,用于内部目的。 |
COM_CHANGE_USER | 17 | 以下格式的字节序列:以零结尾的用户名、加密密码、以零结尾的默认数据库名 | 通知服务器客户端希望更改与此会话关联的用户。由客户端API调用mysql_change_user() 发出。 |
COM_BINLOG_DUMP | 18 | 以下格式的字节序列:4字节整数表示偏移量、2字节整数表示标志、4字节整数表示从服务器ID,以及一个表示日志名称的字符串。所有整数均低位字节在前 | 通知服务器从指定日志的指定偏移量开始,持续发送复制主日志事件。供复制从服务器和mysqlbinlog 命令行实用工具使用。 |
COM_TABLE_DUMP | 19 | 以下格式的字节序列:1字节表示数据库名称长度、数据库名称、1字节表示表名长度、表名 | 通知服务器以原始格式将表定义和数据发送给客户端。当复制从服务器接收到LOAD DATA FROM MASTER 查询时使用。 |
COM_CONNECT_OUT | 20 | 无参数 | 客户端从不发送此命令,用于内部目的。 |
COM_REGISTER_SLAVE | 21 | 以下格式的字节序列:一个4字节整数表示服务器ID,然后是一系列以1字节长度为前缀的字符串,顺序如下:从服务器主机名、用于连接的从服务器用户名、从服务器用户密码。然后是一个2字节的从服务器用户端口、4字节的复制恢复等级,以及另一个目前未使用的4字节字段。所有整数均低位字节在前 | 通知复制主服务器使用参数中提供的信息注册从服务器。此命令是启动的故障安全复制项目的遗留部分。它在4.0早期版本中引入,但此后变化不大。该命令可能会在未来版本中被移除。 |
COM_PREPARE | 22 | 包含语句的字符串 | 通知服务器准备参数指定的语句。由客户端API调用mysql_stmt_prepare() 发出。4.1版本新增。 |
COM_EXECUTE | 23 | 以下格式的字节序列:4字节的语句ID、1字节的标志和4字节的迭代计数。所有整数均低位字节在前 | 通知服务器执行由语句ID引用的语句。由客户端API调用mysql_stmt_execute() 发出。4.1版本新增。 |
表4-7 客户端命令(续) | |||
命令代码枚举值 | 代码数值 | 参数描述 | 命令描述 |
--- | --- | --- | --- |
COM_LONG_DATA | 24 | 以下格式的字节序列:4字节语句ID、2字节参数编号、参数字符串。两个整数均低位字节在前 | 通知服务器该数据包包含预准备语句中一个绑定参数的数据。当绑定参数的值很长时,用于避免不必要的大量数据复制。由客户端API调用mysql_stmt_send_long_data() 发出。4.1版本新增。 |
COM_CLOSE_STMT | 25 | 4字节的语句ID,低位字节在前 | 通知服务器关闭由语句ID指定的预准备语句。由客户端API调用mysql_stmt_close() 发出。4.1版本新增。 |
COM_RESET_STMT | 26 | 4字节的语句ID,低位字节在前 | 通知服务器丢弃由语句ID指定的预准备语句中当前可能已通过COM_LONG_DATA 设置的参数值。由客户端API调用mysql_stmt_reset() 发出。4.1版本新增。 |
COM_SET_OPTION | 27 | 2字节的选项代码,低位字节在前 | 通知服务器启用或禁用由代码指定的选项。目前,它似乎仅用于启用或禁用在一个查询字符串中支持多个语句的功能。由客户端API调用mysql_set_server_option() 发出。4.1版本新增。 |
COM_END | 28 | 无参数 | 客户端从不发送此命令,用于内部目的。 |
当MySQL开发者添加新命令时,为了保持对旧客户端的向后兼容性,所有新命令都会立即添加到enum server_command
中的COM_END
之前。将新命令添加到其他任何位置都会改变现有命令的数字代码,从而导致旧客户端中插入点之后的所有命令都无法正常工作。这一要求在一定程度上使我们能够轻松追溯功能的历史。例如,我们可以看出预准备语句是在复制功能之后添加的,因为COM_PREPARE
紧跟在COM_BINLOG_DUMP
之后。
# 服务器响应
服务器收到命令后,会对其进行处理并发送一个或多个响应数据包。本节将讨论几种不同类型的响应。
# 数据字段
数据字段是许多服务器响应数据包中的关键组成部分。一个数据字段由一个长度指定序列和紧随其后的实际数据值构成。通过研究sql/pack.c
中net_store_length()
函数的定义,能更好地理解长度指定序列:
char *
net_store_length(char *pkg, ulonglong length) {
uchar *packet=(uchar*) pkg;
if (length < (ulonglong) LL(251)) {
*packet=(uchar) length;
return (char*) packet+1;
}
/* 251 是为NULL保留的 */
if (length < (ulonglong) LL(65536)) {
*packet++=252;
int2store(packet,(uint) length);
return (char*) packet+2;
}
if (length < (ulonglong) LL(16777216))
{
*packet++=253;
int3store(packet,(ulong) length);
return (char*) packet+3;
}
*packet++=254;
int8store(packet,length);
return (char*) packet+8;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
可以看到,如果length
的值不超过251(即它可以在不与保留值冲突的情况下用1个字节表示),代码就直接将其存储在1个字节中。如果值大于等于251但可以用2个字节表示,代码会在前面加上252,然后用接下来的2个字节写入该值。如果2个字节不够,但4个字节足够,代码就用253作为标识,然后用接下来的4个字节存储长度值。如果4个字节也不够,代码就用254作为标识,并将长度值存储在8个字节中。需要注意的是,标识后面的所有长度值都是按低位字节在前的顺序存储的。
有人可能会问,为什么1字节长度限制为251,而net_store_length()
函数中的第一个保留值是252呢?代码251有特殊含义,它表示该代码后面没有长度值或数据,字段的值为SQL NULL。
为什么要这么复杂呢?大多数情况下,数据字段相当短,特别是当查询返回大量记录和(或)选择很多列时,响应中可能会有大量的数据字段。在这种情况下,每个字段哪怕只浪费1个字节,累积起来也会造成很大的开销。字段长度大于250的概率相对较低,即便出现这种情况,浪费1个字节也几乎察觉不到,因为此时服务器至少已经发送253个字节:至少2个字节用于表示长度,至少251个字节用于表示字段值。
紧跟在长度序列之后的是实际数据值,这些数据值会被转换为字符串表示形式。
在4.1之前的版本中,用于在缓冲区中存储数据字段的标准服务器API调用是net_store_data()
,它有几种变体,对应每种可能的数据参数类型。在旧版本中,net_store_data()
系列函数位于sql/net_pkg.cc
中。4.1及更高版本使用Protocol::store()
,在简单协议的情况下,它只是对net_store_data()
进行了封装。这两个函数都在sql/protocol.cc
中实现。
需要注意的是,在4.1版本中,当返回预处理语句字段的数据且数据值不是字符串时,数据会以原始二进制格式发送,低位字节在前,且没有长度指定符。
# OK数据包
OK数据包用于表明服务器已成功完成命令。在以下命令执行成功后会发送OK数据包:
COM_PING
- 如果
COM_QUERY
查询不需要返回结果集(例如INSERT
、UPDATE
或ALTER TABLE
) COM_REFRESH
COM_REGISTER_SLAVE
这种类型的数据包适用于不返回结果集的命令。不过,它的格式允许发送一些额外的状态信息,比如修改的记录数、自动生成的主键值,或者字符串格式的自定义状态消息。数据包主体的结构如表4-8所示。
主体中的偏移量 | 长度 | 描述 |
---|---|---|
0 | 1 | 值为0的字节,表示该数据包没有字段。 |
1 | rows_len | 查询更改的记录数,采用本章前面 “数据字段” 部分描述的字段长度格式。其长度会根据值的大小而变化。为表示后续偏移量,我将其长度记为rows_len 。 |
1 + rows_len | id_len | 生成的主键自动递增ID的值。如果在当前上下文中不适用,则设置为0。该值采用数据字段的字段长度格式存储。我将这个值的长度记为id_len 。 |
1 + rows_len + id_len | 2 | 服务器状态位掩码,低位字节在前。有关不同值的详细信息,请查看include/mysql_com.h 中以STATUS_ 开头的宏。在4.0及更早版本的协议中,只有状态字段的值不为零时才会出现。在4.1及更高版本的协议中,会无条件报告该字段。 |
3 + rows_len + id_len | 2 | 仅在4.1及更高版本的协议中存在。包含上一条命令生成的警告数量。例如,如果命令是带有LOAD DATA INFILE 的COM_QUERY ,且某些字段或行无法正确导入,就会生成一些警告。该数量以低位字节在前的方式存储。 |
在4.1及更高版本的协议中为5 + rows_len + id_len ;在旧协议中,根据是否包含服务器状态位掩码,为1 + rows_len + id_len 或3 + rows_len + id_len | msg_len | 可选字段,用于存储状态消息(如果有的话),采用标准数据字段格式,即字段长度后跟字段值,这里的字段值是一个字符串。 |
要在服务器内部发送OK数据包,必须调用send_ok()
函数。在4.1及更高版本中,该函数在sql/protocol.h
中声明,在sql/protocol.cc
中定义。在早期版本中,它在sql/mysql_priv.h
中声明,在sql/net_pkg.cc
中定义。
# 错误数据包
当命令处理出现问题时,服务器会返回一个错误数据包。其格式如表4-9所示。
主体中的偏移量 | 长度 | 描述 |
---|---|---|
0 | 1 | 值为255的字节。客户端始终会将以值为255的字节开头的响应数据包视为错误消息。 |
1 | 2 | 错误代码,低位字节在前。如果服务器与非常古老的3.23之前的客户端通信,则不会包含此字段,在这种情况下,后续偏移量应相应调整。 |
3 | 2 | 字符# ,后面跟着包含ODBC/JDBC SQL状态值的字节。仅在4.1及更高版本中存在。 |
在4.1及更高版本中为5;在4.0及更早版本中为3 | 可变 | 以零结尾的错误消息文本。 |
要在服务器内部发送错误数据包,需调用send_error()
函数。在4.1及更高版本中,该函数在sql/protocol.cc
中定义;在4.0及更早版本中,它在sql/net_pkg.cc
中定义。
# EOF数据包
文件结束(EOF,End-of-file)数据包用于传达多种消息:
- 结果集中的字段结束信息数据
- 结果集中的行结束数据
- 服务器对
COM_SHUTDOWN
的确认 - 服务器对
COM_SET_OPTION
和COM_DEBUG
的成功响应报告 - 认证期间对旧风格凭证的请求
EOF数据包的主体始终以值为254(十进制)的字节开头。在4.1之前的版本中,除了这个字节,主体中没有其他内容。4.1版本添加了另外4个字节的状态字段,最多可能达到7个字节。4.1版本EOF数据包主体的格式如表4-10所示。
主体中的偏移量 | 长度 | 描述 |
---|---|---|
0 | 1 | 值为254(十进制)的字节 |
1 | 2 | 警告数量 |
3 | 2 | 服务器状态位掩码 |
状态字节区域限制为7个字节的原因是,数据包主体开头的254(十进制)字节后跟一个8字节字符串可能有不同的含义:它可以使用本章前面 “数据字段” 部分描述的字段长度格式来指定结果集中的字段数量。
要发送EOF数据包,服务器会使用send_eof()
函数。在4.1及更高版本中,该函数在sql/protocol.cc
中定义;在早期版本中,它在sql/net_pkg.cc
中定义。
# 结果集数据包
大量查询会产生结果集,例如SELECT
、SHOW
、CHECK
、REPAIR
和EXPLAIN
等查询。只要查询预期返回的信息不只是简单的状态报告,就会返回一个结果集。
结果集由一系列数据包组成。首先,在4.1及更高版本中,服务器会调用sql/protocol.cc
中的Protocol::send_fields()
函数发送有关字段的信息。在旧版本中,该函数名为send_fields()
,位于sql/net_pkg.cc
中。这个阶段会生成以下数据包序列:
- 一个数据包,其主体由标准字段长度指定序列构成。不过,此时这个数字的含义不同,它表示结果集中的字段数量。
- 一组字段描述数据包(有关格式说明,请见下文),结果集中每个字段对应一个,按照字段顺序排列。
- 一个终止的EOF数据包。
字段描述数据包主体的格式如表4-11和表4-12所示。表4-11展示了4.0及更早版本的格式,表4-12展示了4.1及更高版本的格式。由于大多数数据包元素的长度是可变的,偏移量取决于前面字段的内容。因此,在格式描述中我将省略偏移量这一列。最后,表4-13解释了不同的字段选项标志。
长度 | 描述 |
---|---|
可变 | 字段的表名,采用数据字段格式。如果查询中对表进行了别名设置,则包含别名。 |
可变 | 字段的列名,采用数据字段格式。如果查询中对列进行了别名设置,则包含别名。 |
4 | 字段长度的值,采用数据字段格式,低位字节在前。 |
2 | 根据include/mysql_com.h 中的enum field_types 定义的字段类型代码,采用数据字段格式。 |
1 | 十进制值3,表示接下来的3个字节包含数据。这样做是为了使该序列看起来像一个标准数据字段。 |
2 | 字段选项标志的位掩码(低位字节在前)。有关位的解释,请见表4-12。 |
1 | 字段的小数点精度。 |
可变 | 可选元素。如果存在,包含字段的默认值,采用标准字段数据格式。 |
4 | 包含ASCII字符串def 的数据字段(见本章前面 “数据字段” 部分)。 |
可变 | 字段所属数据库的名称,采用数据字段格式。 |
可变 | 字段的表名,采用数据字段格式。如果查询中对表进行了别名设置,则包含别名。 |
可变 | 字段的表名,采用数据字段格式。如果查询中对表进行了别名设置,则包含表的原名。 |
可变 | 字段的列名,采用数据字段格式。如果查询中对列进行了别名设置,则包含别名。 |
可变 | 字段的列名,采用数据字段格式。如果查询中对列进行了别名设置,则包含表的原名。 |
1 | 值为12(十进制)的字节,表示后面跟着12个字节的数据。这样做是为了使该序列看起来像一个标准数据字段。 |
2 | 字段的字符集代码(低位字节在前)。 |
4 | 字段长度(低位字节在前)。 |
1 | 根据include/mysql_com.h 中的enum field_types 定义的字段类型代码。 |
2 | 字段选项标志的位掩码(低位字节在前)。有关位的解释,请见表4-13。 |
1 | 字段值的小数点精度。 |
2 | 保留字段。 |
可变 | 可选元素。如果存在,包含字段的默认值,采用标准字段数据格式。 |
位宏 | 十六进制位值 |
NOT_NULL_FLAG | 0x0001 |
PRI_KEY_FLAG | 0x0002 |
UNIQUE_KEY_FLAG | 0x0004 |
MULTIPLE_KEY_FLAG | 0x0008 |
BLOB_FLAG | 0x0010 |
UNSIGNED_FLAG | 0x0020 |
ZEROFILL_FLAG | 0x0040 |
BINARY_FLAG | 0x0080 |
ENUM_FLAG | 0x0100 |
AUTO_INCREMENT_FLAG | 0x0200 |
TIMESTAMP_FLAG | 0x0400 |
SET_FLAG | 0x0800 |
NUM_FLAG | 0x8000 |
在发送完字段定义数据包序列后,服务器会发送实际的数据行,每行数据对应一个数据包。每个行数据数据包由一系列采用标准字段数据格式存储的值构成。在报告常规查询(通过COM_QUERY
发送)的结果时,字段数据会转换为字符串格式。在使用预处理语句(COM_PREPARE
)时,字段数据会以其原生格式发送,低位字节在前。
发送完所有数据行后,数据包序列会以一个EOF数据包结束。