MySQL NET协议
MySQL NET协议处于应用层之下、TCP/IP网络层之上。MySQL NET将应用层的数据分别打包。MySQL NET和TCP/IP的关系如下图:
网络包格式
从是否被压缩的角度来讲,MySQL NET的数据报分为两种格式-压缩和非压缩。在双方的握手阶段,由双方根据各自的性能和设置来决定服务端和客户端之间的会话采用哪种包进行通信。
按发送者身份来分,数据包又分为由客户端发来的命令包和服务端发送的结果包。其中服务端发送的消息格式包含数据包、数据流结束标志符、OK包和错误消息包。
所有的MySQL NET包可以归纳为:
握手阶段(当客户端开始连接时)
- 从服务端到客户端:握手初始化包
- 从客户端到服务端:客户端认证包
- 从服务端到客户端:OK包、错误(Error)包
命令包(客户端对服务端的任一请求)
- 从客户端到服务端:命令(Command)Packet
- 从服务端到客户端:Ok包、错误(Error)包、结果集包
下面介绍几个专业术语
Null结尾字符串和带长度标识字符串
在网络发送的数据中,经常包含变长的字符串。如果没有特殊标志来说明的话,我们无从知晓字符串何时结束。
- 以Null结尾的字符串:字符串使用ASCII码“/0”表示。遇到Null意味着字符串结束。例如16进制字符串 0x 72 73 00 73 表示字符串“rs”,而非“rss”;
- 带长度标识的字符串(Length Coded String):也是用于表示变长字符串。以上的例子我们可以发现“以Null结尾的字符串”无法解决内部含有“/0”的情况,所以需要借助带长度标识的字符串这一特殊类型。带长度标识的字符串由两部分组成:长度定义部分和内容部分。长度定义字段可参考下表。内容字段就是字符串的实际内容。
第一字节值 | 后续字节数 | 说明 |
0~250 | 0 | 这个值说明了Length Coded String后面有多少字节数据,这种方式限制了后续数据最多有250个字节 |
251 | 0 | 列值为Null,仅仅用于行数据包 |
252 | 2 | 后面两个字节的值,说明Length Coded String后面有多少字节数据 |
253 | 3 | 后面三个字节的值,说明Length Coded String后面有多少字节数据 |
254 | 8 | 后面八个字节的值,说明Length Coded String后面有多少字节数据 |
例如:对16进制字符串0x 02 61 62的解析如下:
02处于0~250之间,所以02表示了后面有2个字节的数据。“61 62”为内容部分“ab”。
网络包头部格式
所有的MySQL NET数据包都含有一个4字节长度的包头,具体的包头格式如下:
字节数 | 名称 | 描述 |
3 | Myd>SQL NET数据包长度 | 数据包长度记录了数据包头部之后的数据长度。根据2^24,我们可以得出一个包最大长度为16MB,如果超长,则数据包会被分割 |
1 | 数据包序列号 | 和大部分的网络包一样,TCP/IP无法保证收到的顺序,所以需要应用层定义顺序以保证数据按预期顺序收到。 |
当我们的服务器采用了compress方式压缩数据包后
mysql>SHOW VARIABLES LIKE "have_com%"
| have_compress | YES |
则数据包头部格式中就会出现三个额外的字节,这三个字节用于记录被压缩包的数据长度。MySQL使用开源社区的zlib来压缩数据包压缩包。
zlib将原数据包作为参数传给Zlib/compress.c中的compress函数:
int ZEXPORT compress(dest, destLen, source, sourceLen)
Bytef *dest;
uLongf *destLen;
const Bytef &source
uLong sourceLen;
{
return compress2(dest, destLen, source, sourceLen, z_DEFAULT_COMPRESSION);
}
函数的作用是最终得到的一个放在地址dest中、destLen长度的压缩包数据。如果存在压缩失败或者内存不足的情况,compress()函数返回Z_BUF_ERROR
或Z_MEM_ERROR
错误。此时通信两端只能按照非压缩格式来传输数据了。
客户端发送的包
客户端认证包
客户端接收到服务器发来的初始化包之后,将向MySQL服务器发送认证请求,该包的格式如下表:
字节数 | 字段名 |
4 | 字客户端标志 |
4 | 包最大长度(Max_packet_size) |
1 | 客户端字符集ID |
23 | 填充字符(0x00) |
N | 用户名 |
1+N | 密码加密字段(scramble_buff) |
N | 数据库名(可选) |
这其中的密码字段和数据库名字段是可选的,但即使没有密码,密码字段长度也至少为1。
- 客户端标志:与服务器端能力表述有相同作用,也是用位图的形式表述客户端能够接受的连接选项。所有选项都可以在include/mysql_com.h中找到,以CLIENT开头
- 包最大长度:一个包能包容的最大长度
- 客户端字符集ID:MySQL客户端所使用的字符集ID
- 用户名:是以Null结尾的字符串。存放试图登录的用户名
- 密码加密字段:是带长度标识的字符串(Length Coded String)
- 数据库名称:数据库的名称字段
MySQL的源文件libmysql/libmysql.c::mysql_real_connect()函数定义了MySQL客户端的认证过程,而sql/sql_connect.cc::check_connections()函数中定义了服务端认证用户的过程:
static int check_connection(THD *thd)
{
uint connect_errors = 0;
NET *net = &thd->net;
ulong pkt_len = 0;
char *end;
DBUG_PRINT(*info*, (*New connection received on %s", vio_description(net->vio)));
#ifdef SIGNAL_WITH_VIO_CLOSE
thd->sdt_active_vio(net->vio);
#endif
...
}
命令包
一旦握手认证阶段完成并通过后,客户端开始使用命令包向服务端发送各种命令。命令包的格式如下表:
字节数 | 字段名 |
1 | 命令 |
N | 命令参数 |
- 命令字段:十六进制字符表示,定义要执行的命令类型。
源文件include/mysql_com.h中对命令的定义如下:
enum enum_server_command
{
COM_SLEEP, COM_QUIT, COM_INIT_DB, COM_QUERY, COM_FIELD_LIST, COM_CREATE_DB, COM_GROUP_DB, COM_REFERESH, COM_SHUTDOWN, COM_STATISTICS,COM_PROCESS_INFO, COM_CONNECT, COM_PROCESS_KILL, COM_DEBUG, COM_PING, COM_TIME, COM_DELAYED_INSERT, COM_CHANGE_USER, COM_BINLOG_DUMP, COM_TABLE_DUMP, COM_CONNECT_OUT, COM_REGISTER_SLAVE, COM_STMT_PREPARE, COM_STMT_EXECUTE, COM_STMT_SEND_LONG_DATA, COM_STMT_CLOSE, COM_STMT_RESET, COM_SET_OPTION, COM_STMT_FETCH, COM_DREMON, COM_END
}
C语言中,enum类型都可以用数字表示,第一个值COM_SLEEP为0x00,以此类推。这些命令中,最为常见的当属COM_QUERY(0x03)该命令指代所有INSERT、UPDATE、DELETE和SELECT。
从enum类型的enum_server_command可以看出客户端发送给MySQL服务器的命令类型之多。后面我们将以COM_CHANGE_USER和COM_PROCESS_KILL为例加以说明。 - 命令参数:记录了客户端用户的输入,客户端对用户的输入原样的发送给服务器,没有太多的处理(当然会去掉每个语句后的“;”)。
由第四章可知,THD类存放着命令类型的字段,用户可以通过show processlist命令进行查看。
Command列显示,一个线程正执行Sleep命令,即线程在非活动状态(sleep 0x00),另外一个线程则在执行Query(0x03)。Info列显示该命令的参数。
下面是使用MiniSniffer抓取的一个例包:
可以判断02位COM_INIT_DB,即MySQL客户端的use命令。这个包实际上是我们执行了“use test;”。
- COM_CHANGE_USER,这个命令是执行mysql_change_user函数的命令。mysql_change_user函数包含在libmysql/libmysql.c文件中;
- COM_PROCESS_KILL,请求MySQL服务器杀死某个进程的命令包,格式如下:
字节数 | 字段名 |
4 | 进程号字段 |
服务端发送的包
一旦MySQL服务器接收到客户端发送的命令后,服务器在后台直接处理该请求,并生成回答包。
握手初始化包
初始化包是有服务器端在初始化握手阶段发往客户端的,其格式如下表所示:
字节数 | 字段名 |
1 | 协议版本号 |
n=strlen(server_version)+1 | 服务器信息 |
4 | 进程id |
8 | 密码验证 |
1 | 0x00填充位 |
2 | 服务器能力描述 |
1 | 服务器字符集 |
2 | 服务器状态 |
13 | 0x00填充位 |
13 | 密码验证2 |
对该格式我们做下述说明:
- 协议版本号:系统协议版本号在
include/mysql_versiom.h
的PROTOCOL_VERSION
常量中定义,例如:10 - 服务器版本信息:也可以在
include/mysql_version.h
文件中找到。MYSQL_SERVER_VERSION常量中保留着服务器版本信息,例如“5.0.77-community-nt MYSQL Community Edition(GPL)” - 线程号:MySQL服务器为此链接分配的线程号
- 密码验证1:与密码验证2一同用作密码验证
- 服务器能力描述:MySQL用比特位图的形式表述服务器能够接受的连接选项。所有选项都可以在include/mysql_com.h中找到,以CLIENT开头
- 服务器字符集:MySQL服务器所使用的字符集
服务器状态:服务器状态标志,以
SERVER_STATUS_
开头握手初始化包在MySQL开发团队内部也称为“Greeting Packet(欢迎包)”。下面是我们通过MiniSniffer抓取的一个包:
在这个包中,服务器端能力描述2c 82转化为二进制 0010 1110 1000 0010。根据能力描述的定义可知晓该服务器支持CLIENT_MULTI_RESULTS、CLIENT_SSL、CLIENT_COMPRESS、CLIENT_CONNECT_WITH_DB和CLIENT_FOUND_ROWS。
服务端字符集ID号33可用下面的SQL语句校验:
mysql>SELECT CHARACTER_SET_NAME, COLLATION_NAME FROM INFORMATION_SCHEMA.COLLATIONS WHERE ID=33;
结果包分类
应对客户端的认证包或命令包的请求,服务器端都将发送一个回应包,回应包有时是认证请求的通过(OK)或拒绝(ERROR),要么是查询/命令(Command)语句的执行结果。每种包都定义了自己的特殊格式以供客户端区分。
每种包的最开头一个字节定义包类型(其中结果集包、属性包和行数据包将这个字节称为FIELD_COUNT):
结果包的类型 | 字段内容 | 例包 |
OK包 | 0x00 | |
ERROR包 | 0xff | |
结果集包 | 1-250 | Selet * from table1的包 |
属性包 | 1-250 | Select 1+1f返回的包 |
行数据包 | 1-250 | |
EOF包 | 0xfe |
OK包
当MySQL服务器成功执行了一个命令后,它将回复OK包。OK包常常是对以下各种客户端命令的回复:
- COM_PING
- COM_QUERY(此处Query是更加广泛意义上的查询,包含SQL语句中的INSERT、UPDATE和DELETE等)
- COM_REFRESH
- COM_REGISTER_SLAVE
OK包适合于不用返回大量结果集的情况,OK包的信息格式如下表:
字节数 | 字段说明 |
1 | FIELD_COUNT |
1-9 | 命令影响的行数 |
1-9 | 插入ID |
2 | 服务器状态 |
2 | 警告数量 |
N | 消息 |
- FIELD_COUNT:意指该包是OK包,总为0
- 影响的行数:命令DELETE、INSERT和UPDATE影响的行数
- 插入ID:如果该操作引起任何AUTO_INCREMENT的作用,那么插入ID会记录最后一个AUTO_INCREMENT的结果
- 消息:执行语句后,在返回的结果后有一段总结性信息,如:2 rows in set(0.00 sec)
通过网络抓取的一个样例包如下:
ERROR包
相对于OK包,一旦服务器处理命令出错,或者用户的认证信息有问题,那么MySQL就会传递ERROR包,ERROR包的格式如下表所示:
字节数 | 字段说明 |
1 | 其值总为0xff |
2 | 错误号 |
1 | SQL状态标识符,总是“#” |
5 | SQL状态 |
N | 消息 |
- 错误号
在include/mysqld_error.h中定义了内部错误号:
#define ER_ERROR_FIRST 1000
#define ER_HASHCHK 1000
#define ER_NISAMCHK 1001
#define ER_NO 1002
#define ER_YES 1003
#define ER_CANT_CREATE_FILE 1004
#define ER_CANT_CREATE_TABLE 1005
#define ER_CANT_CREATE_DB 1006
#define ER_DB_CREATE_EXISTS 1007
#define ER_dB_DROP_EXISTS 1008
例:
mysql>drop database abcd;
ERROR 1008(HY000):Can't drop database "abcd"; database doesn't exist.
该错误是由于删除了一个并不存在的数据库引起的,错误号为1008,对应的ER_DB_DROP_EXISTS.
2. SQL状态标识符
总是“#”,用于区分该状态出自MySQL 4.1及之后的版本
3. SQL状态
每个错误号对应了一个错误状态。该对应任务由mysql_errno_to_sqlstate()
函数完成。SQL状态include/sql_state.h
中有所记录:
ER_DUP_KEY, *23000*, **,
ER_OUTOPREMORY, *HY001*, *S1001*,
ER_OUT_OF_SORTMEMORY, *HY001*, *S1001*,
ER_CON_COUNT_ERROR, *08004*, **,
ER_BAD_HOST_ERROR, *08S01*, **,
ER_HANDSHAKE_ERROR, *08S01*, **,
ER_DBACCESS_DENIED_ERROR, *42000*, **,
ER_ACCESS_DENIED_ERROR, *28000*, **,
ER_NO_DB_ERROR, *3D000*, **,
ER_UNKNOWN_COM_ERROR, *08S01*, **,
ER_BAD_NULL_ERROR, *23000*, **,
ER_BAD_DB_ERROR, *42000*, **,
ER_TABLE_EXISTS_ERROR, *42S01*, **,
ER_BAD_TABLE_ERROR, *42S02*, **,
ER_NON_UNIQ_ERROR, *23000*, **,
ER_SERVER_SHUTDOWN, *08S01*, **,
ER_BAD_FIELD_ERROR, *42S22*, *S0022*,
ER_WRONG_FIELD_WITH_GROUP, *42000*, *S1009*,
- 消息 :发生错误的具体原因。长度为512字节。
结果集包
类似SELECT、SHOW、CHECK、REPAIR和EXPLAIN的请求服务端会返回一个结果集,一个结果集由一系列的包组成,其中有包头、查询返回的列包、查询返回的行包和EOF包,具体的结果集结构如下图所示:
结果集包头布记录了返回的结果集中包含多少列数据。属性包则为结果集列的描述。EOF标志某一种类的包(如属性包、行数据包)结束。
查询语句中如果得到的结果是n列、m行,则最后结果集包共由m+n+3个包组成,如下面这个查询:
mysql>select * from pet where owner='hiro';
结果返回如下表:
name | owner | species | sex | birth | death |
zhanghai | hiro | cat | m | 0000-00-00 | NULL |
MySQL共为此包发送了6+1+3个包,下面我们详细描述这些包的格式
- 结果集包头部
格式:
字节数 字段名 1-9 FIELD_COUN:区分包类型如Ok包、ERROR包等,这里的FIELD_COUNT记录了返回集的列数 1-9 附属字段:可选,执行show columns等命令时需要,
下面是一个执行SELECT * FROM sqlsys时抓取的网络包:
结果显示sqlsys表中有3个字段(及3列数据)。 - 列(属性)包
结果集中紧接在头部之后的是列描述包。结果集中的一列对应一个列包,FIELD_COUNT为字段记录了后续列包的数量。
以下是一个样包:
- 行数据包
MySQL发送完列(属性)包后,MySQL将以每行数据一个包的形式,发送所有结果集数据。行数据包的格式如下:
字节数 | 字段名 |
N(带长度标识) | 行值 |
不管一行中有多少个列或属性,这些列将置于同一个行数据包中。表示时,各列之间没有任何空格。
4.EOF包
EOF是End of File的缩写。在MySQL的网络环境下表示某种类型的包传输已经结束,所以EOF包出现在列(属性)包、行数据包之后。
EOF的包格式如下,
字节数 | 字段说明 |
1 | FIELD_COUNT,总是等于0xfe |
2 | 警告数量:在传递完所有数据包时,显示所有警告(Warning)的数量 |
2 | 状态标识符:包含各类服务器的状态,如SERVER_STATUS_MORE_RESULTS |