怒肝四个月MySQL源码,我总结出这篇MySQL协议(详尽版)!!

| 字节 | 说明 |

| — | — |

| 4 | 预处理语句的ID值(小字节序) |

COM_RESET_STMT 消息报文

功能:将预处理语句的参数缓存清空。多数情况和COM_LONG_DATA一起使用。

| 字节 | 说明 |

| — | — |

| 4 | 预处理语句的ID值(小字节序) |

COM_SET_OPTION 消息报文

功能:设置语句选项,选项值为/include/mysql_com.h头文件中定义的enum_mysql_set_option枚举类型:

  • MYSQL_OPTION_MULTI_STATEMENTS_ON

  • MYSQL_OPTION_MULTI_STATEMENTS_OFF

| 字节 | 说明 |

| — | — |

| 2 | 选项值(小字节序) |

COM_FETCH_STMT 消息报文

功能:获取预处理语句的执行结果(一次可以获取多行数据)。

| 字节 | 说明 |

| — | — |

| 4 | 预处理语句的ID值(小字节序) |

| 4 | 数据的行数(小字节序) |

服务器响应报文(服务器 -> 客户端)

当客户端发起认证请求或命令请求后,服务器会返回相应的执行结果给客户端。客户端在收到响应报文后,需要首先检查第1个字节的值,来区分响应报文的类型。

| 响应报文类型 | 第1个字节取值范围 |

| — | — |

| OK 响应报文 | 0x00 |

| Error 响应报文 | 0xFF |

| Result Set 报文 | 0x01 - 0xFA |

| Field 报文 | 0x01 - 0xFA |

| Row Data 报文 | 0x01 - 0xFA |

| EOF 报文 | 0xFE |

注:响应报文的第1个字节在不同类型中含义不同,比如在OK报文中,该字节并没有实际意义,值恒为0x00;而在Result Set报文中,该字节又是长度编码的二进制数据结构(Length Coded Binary)中的第1字节。

响应报文

客户端的命令执行正确时,服务器会返回OK响应报文。

MySQL 4.0 及之前的版本

| 字节 | 说明 |

| — | — |

| 1 | OK报文,值恒为0x00 |

| 1-9 | 受影响行数(Length Coded Binary) |

| 1-9 | 索引ID值(Length Coded Binary) |

| 2 | 服务器状态 |

| n | 服务器消息(字符串到达消息尾部时结束,无结束符) |

MySQL 4.1 及之后的版本

| 字节 | 说明 |

| — | — |

| 1 | OK报文,值恒为0x00 |

| 1-9 | 受影响行数(Length Coded Binary) |

| 1-9 | 索引ID值(Length Coded Binary) |

| 2 | 服务器状态 |

| 2 | 告警计数 |

| n | 服务器消息(字符串到达消息尾部时结束,无结束符,可选) |

受影响行数:当执行INSERT/UPDATE/DELETE语句时所影响的数据行数。

索引ID值:该值为AUTO_INCREMENT索引字段生成,如果没有索引字段,则为0x00。注意:当INSERT插入语句为多行数据时,该索引ID值为第一个插入的数据行索引值,而非最后一个。

服务器状态:客户端可以通过该值检查命令是否在事务处理中。

告警计数:告警发生的次数。

服务器消息:服务器返回给客户端的消息,一般为简单的描述性字符串,可选字段。

响应报文

MySQL 4.0 及之前的版本

| 字节 | 说明 |

| — | — |

| 1 | Error报文,值恒为0xFF |

| 2 | 错误编号(小字节序) |

| n | 服务器消息 |

MySQL 4.1 及之后的版本

| 字节 | 说明 |

| — | — |

| 1 | Error报文,值恒为0xFF |

| 2 | 错误编号(小字节序) |

| 1 | 服务器状态标志,恒为’#'字符 |

| 5 | 服务器状态(5个字符) |

| n | 服务器消息 |

错误编号:错误编号值定义在源代码/include/mysqld_error.h头文件中。

服务器状态:服务器将错误编号通过mysql_errno_to_sqlstate函数转换为状态值,状态值由5字节的ASCII字符组成,定义在源代码/include/sql_state.h头文件中。

服务器消息:错误消息字符串到达消息尾时结束,长度可以由消息头中的长度值计算得出。消息长度为0-512字节。

Result Set 消息

当客户端发送查询请求后,在没有错误的情况下,服务器会返回结果集(Result Set)给客户端。

Result Set 消息分为五部分,结构如下:

| 结构 | 说明 |

| — | — |

| [Result Set Header] | 列数量 |

| [Field] | 列信息(多个) |

| [EOF] | 列结束 |

| [Row Data] | 行数据(多个) |

| [EOF] | 数据结束 |

Result Set Header 结构

| 字节 | 说明 |

| — | — |

| 1-9 | Field结构计数(Length Coded Binary) |

| 1-9 | 额外信息(Length Coded Binary) |

Field结构计数:用于标识Field结构的数量,取值范围0x00-0xFA。

额外信息:可选字段,一般情况下不应该出现。只有像SHOW COLUMNS这种语句的执行结果才会用到额外信息(标识表格的列数量)。

Field 结构

Field为数据表的列信息,在Result Set中,Field会连续出现多次,次数由Result Set Header结构中的IField结构计数值决定。

MySQL 4.0 及之前的版本

| 字节 | 说明 |

| — | — |

| n | 数据表名称(Length Coded String) |

| n | 列(字段)名称(Length Coded String) |

| 4 | 列(字段)长度(Length Coded String) |

| 2 | 列(字段)类型(Length Coded String) |

| 2 | 列(字段)标志(Length Coded String) |

| 1 | 整型值精度 |

| n | 默认值(Length Coded String) |

MySQL 4.1 及之后的版本

| 字节 | 说明 |

| — | — |

| n | 目录名称(Length Coded String) |

| n | 数据库名称(Length Coded String) |

| n | 数据表名称(Length Coded String) |

| n | 数据表原始名称(Length Coded String) |

| n | 列(字段)名称(Length Coded String) |

| 4 | 列(字段)原始名称(Length Coded String) |

| 1 | 填充值 |

| 2 | 字符编码 |

| 4 | 列(字段)长度 |

| 1 | 列(字段)类型 |

| 2 | 列(字段)标志 |

| 1 | 整型值精度 |

| 2 | 填充值(0x00) |

| n | 默认值(Length Coded String) |

目录名称:在4.1及之后的版本中,该字段值为"def"。

数据库名称:数据库名称标识。

数据表名称:数据表的别名(AS之后的名称)。

数据表原始名称:数据表的原始名称(AS之前的名称)。

列(字段)名称:列(字段)的别名(AS之后的名称)。

列(字段)原始名称:列(字段)的原始名称(AS之前的名称)。

字符编码:列(字段)的字符编码值。

列(字段)长度:列(字段)的长度值,真实长度可能小于该值,例如VARCHAR(2)类型的字段实际只能存储1个字符。

列(字段)类型:列(字段)的类型值,取值范围如下(参考源代码/include/mysql_com.h头文件中的enum_field_type枚举类型定义):

| 类型值 | 名称 |

| — | — |

| 0x00 | FIELD_TYPE_DECIMAL |

| 0x01 | FIELD_TYPE_TINY |

| 0x02 | FIELD_TYPE_SHORT |

| 0x03 | FIELD_TYPE_LONG |

| 0x04 | FIELD_TYPE_FLOAT |

| 0x05 | FIELD_TYPE_DOUBLE |

| 0x06 | FIELD_TYPE_NULL |

| 0x07 | FIELD_TYPE_TIMESTAMP |

| 0x08 | FIELD_TYPE_LONGLONG |

| 0x09 | FIELD_TYPE_INT24 |

| 0x0A | FIELD_TYPE_DATE |

| 0x0B | FIELD_TYPE_TIME |

| 0x0C | FIELD_TYPE_DATETIME |

| 0x0D | FIELD_TYPE_YEAR |

| 0x0E | FIELD_TYPE_NEWDATE |

| 0x0F | FIELD_TYPE_VARCHAR (new in MySQL 5.0) |

| 0x10 | FIELD_TYPE_BIT (new in MySQL 5.0) |

| 0xF6 | FIELD_TYPE_NEWDECIMAL (new in MYSQL 5.0) |

| 0xF7 | FIELD_TYPE_ENUM |

| 0xF8 | FIELD_TYPE_SET |

| 0xF9 | FIELD_TYPE_TINY_BLOB |

| 0xFA | FIELD_TYPE_MEDIUM_BLOB |

| 0xFB | FIELD_TYPE_LONG_BLOB |

| 0xFC | FIELD_TYPE_BLOB |

| 0xFD | FIELD_TYPE_VAR_STRING |

| 0xFE | FIELD_TYPE_STRING |

| 0xFF | FIELD_TYPE_GEOMETRY |

列(字段)标志:各标志位定义如下(参考源代码/include/mysql_com.h头文件中的宏定义):

| 标志位 | 名称 |

| — | — |

| 0x0001 | NOT_NULL_FLAG |

| 0x0002 | PRI_KEY_FLAG |

| 0x0004 | UNIQUE_KEY_FLAG |

| 0x0008 | MULTIPLE_KEY_FLAG |

| 0x0010 | BLOB_FLAG |

| 0x0020 | UNSIGNED_FLAG |

| 0x0040 | ZEROFILL_FLAG |

| 0x0080 | BINARY_FLAG |

| 0x0100 | ENUM_FLAG |

| 0x0200 | AUTO_INCREMENT_FLAG |

| 0x0400 | TIMESTAMP_FLAG |

| 0x0800 | SET_FLAG |

数值精度:该字段对DECIMALNUMERIC类型的数值字段有效,用于标识数值的精度(小数点位置)。

默认值:该字段用在数据表定义中,普通的查询结果中不会出现。

:Field结构的相关处理函数:

  • 客户端:/client/client.c源文件中的unpack_fields函数

  • 服务器:/sql/sql_base.cc源文件中的send_fields函数

EOF 结构

EOF结构用于标识Field和Row Data的结束,在预处理语句中,EOF也被用来标识参数的结束。

MySQL 4.0 及之前的版本

| 字节 | 说明 |

| — | — |

| 1 | EOF值(0xFE) |

MySQL 4.1 及之后的版本

| 字节 | 说明 |

| — | — |

| 1 | EOF值(0xFE) |

| 2 | 告警计数 |

| 2 | 状态标志位 |

告警计数:服务器告警数量,在所有数据都发送给客户端后该值才有效。

状态标志位:包含类似SERVER_MORE_RESULTS_EXISTS这样的标志位。

:由于EOF值与其它Result Set结构共用1字节,所以在收到报文后需要对EOF包的真实性进行校验,校验条件为:

  • 第1字节值为0xFE

  • 包长度小于9字节

:EOF结构的相关处理函数:

  • 服务器:protocol.cc源文件中的send_eof函数

Row Data 结构

在Result Set消息中,会包含多个Row Data结构,每个Row Data结构又包含多个字段值,这些字段值组成一行数据。

| 字节 | 说明 |

| — | — |

| n | 字段值(Length Coded String) |

| … | (一行数据中包含多个字段值) |

字段值:行数据中的字段值,字符串形式。

:Row Data结构的相关处理函数:

  • 客户端:/client/client.c源文件中的read_rows函数

Row Data 结构(二进制数据)

该结构用于传输二进制的字段值,既可以是服务器返回的结果,也可以是由客户端发送的(当执行预处理语句时,客户端使用Result Set消息来发送参数及数据)。

| 字节 | 说明 |

| — | — |

| 1 | 结构头(0x00) |

| (列数量 + 7 + 2) / 8 | 空位图 |

| n | 字段值 |

| … | (一行数据中包含多个字段值) |

空位图:前2个比特位被保留,值分别为0和1,以保证不会和OK、Error包的首字节冲突。在MySQL 5.0及之后的版本中,这2个比特位的值都为0。

字段值:行数据中的字段值,二进制形式。

PREPARE_OK 响应报文(Prepared Statement)

用于响应客户端发起的预处理语句报文,组成结构如下:

| 结构 | 说明 |

| — | — |

| [PREPARE_OK] | PREPARE_OK结构 |

| 如果参数数量大于0 | |

| [Field] | 与Result Set消息结构相同 |

| [EOF] | |

| 如果列数大于0 | |

| [Field] | 与Result Set消息结构相同 |

| [EOF] | |

其中 PREPARD_OK 的结构如下:

| 字节 | 说明 |

| — | — |

| 1 | OK报文,值为0x00 |

| 4 | 预处理语句ID值 |

| 2 | 列数量 |

| 2 | 参数数量 |

| 1 | 填充值(0x00) |

| 2 | 告警计数 |

Parameter 响应报文(Prepared Statement)

预处理语句的值与参数正确对应后,服务器会返回 Parameter 报文。

| 字节 | 说明 |

| — | — |

| 2 | 类型 |

| 2 | 标志 |

| 1 | 数值精度 |

| 4 | 字段长度 |

类型:与 Field 结构中的字段类型相同。

标志:与 Field 结构中的字段标志相同。

数值精度:与 Field 结构中的数值精度相同。

字段长度:与 Field 结构中的字段长度相同。

代码分析


议程

协议头

协议类型 网络协议相关函数 NET缓冲 VIO缓冲 MySQL API

协议头

● 数据变成在网络里传输的数据,需要额外的在头部添加4 个字节的包头.

. packet length(3字节), 包体的长度

. packet number(1字节), 从0开始的递增的

● sql “select 1” 的网络协议是?

协议头

● packet length三个字节意味着MySQL packet最大16M大于16M则被分包(net_write_command, my_net_write)

● packet number分包从0开始,依次递增.每一次执行sql, packet_number清零(sql/net_serv.c:net_clear)

协议类型

● handshake

● auth

● ok|error

● resultset

​ ○ header

​ ○ field

​ ○ eof

​ ○ row

● command packet

连接时的交互

协议说明

● 协议内字段分三种形式

​ ○ 固定长度(include/my_global.h)

​ ■ uint*korr 解包 *

​ ■ int*store 封包

​ ○ length coded binary(sql-common/pack.c)

​ ■ net_field_length 解包

​ ■ net_store_length 封包

​ ○ null-terminated string

● length coded binary

​ ○ 避免binary unsafe string, 字符串的长度保存在字符串的前面

​ ■ length<251 1 byte

​ ■ length <256^2 3 byte(第一个byte是252)

​ ■ length<256^3 4byte(第一个byte是253)

​ ■ else 9byte(第一个byte是254)

handshake packet

● 该协议由服务端发送客户端

● 括号内为字节数,字节数为n为是null-terminated string;字节数为大写的N表示length code binary.

● salt就是scramble.分成两个部分是为了兼容4.1版本

● sql_connect.cc:check_connection

● sql_client.c:mysql_real_connect

auth packet

● 该协议是从客户端对密码使用scramble加密后发送到服务端

● 其中databasename是可选的.salt就是加密后的密码.

● sql_client.c:mysql_real_connect

● sql_connect.c:check_connection

ok packet

● ok包,命令和insert,update,delete的返回结果

● 包体首字节为0.

● insert_id, affect_rows也是一并发过来.

● src/protocol.cc:net_send_ok

error packet

● 错误的命令,非法的sql的返回包

● 包体首字节为255.

● error code就是CR_***,include/errmsg.h

● sqlstate marker是#

● sqlstate是错误状态,include/sql_state.h

● message是错误的信息

● sql/protocol.cc:net_send_error_packet

resultset packet

● 结果集的数据包,由多个packet组合而成

● 例如查询一个结构集,顺序如下:

○ header

○ field1…fieldN

○ eof

○ row1…rowN

○ eof

● sql/client.c:cli_read_query_result

● 下面是一个sql "select * from d"查询结果集的例子,结果 集是6行,3个字段

○ 公式:假设结果集有N行, M个字段.则包的个数为,header(1) + field (M) + eof(1) + row(N) + eof(1)

○ 所以这个例子的MySQL packet的个数是12个

resultset packet - header

● field packet number决定了接下来的field packet的个数.

● 一个返回6行记录,3个字段的查询语句

resultset packet - field

● 结果集中一个字段一个field packet.

● tables_alias是sql语句里表的别名,org_table才是表的真 实名字.

● sql/protocol.cc:Protocol::send_fields

● sql/client.c:cli_read_query_result

resultset packet - eof

● eof包是用于分割field packet和row packet.

● 包体首字节为254

● sql/protocol.cc:net_send_eof

resultset packet - row

● row packet里才是真正的数据包.一行数据一个packet.

● row里的每个字段都是length coded binary

● 字段的个数在header packet里

● sql/client.c:cli_read_rows

command packet

● 命令包,包括我们的sql语句还有一些常见的命令.

● 包体首字母表示命令的类型(include/mysql_com.h),大 部分命令都是COM_QUERY.

网络协议关键函数

● net_write_command(sql/net_serv.cc)所有的sql最终调用这个命令发送出去.

● my_net_write(sql/net_serv.cc)连接阶段的socket write操作调用这个函数.

● my_net_read读取包,会判断包大小,是否是分包

● my_real_read解析MySQL packet,第一次读取4字节,根据packet length再读取余下来的长度

● cli_safe_read客户端解包函数,包含了my_net_read

NET缓冲

● 每次socket操作都会先把数据写,读到net->buff,这是一 个缓冲区, 减少系统调用调用的次数.

● 当写入的数据和buff内的数据超过buff大小才会发出一次 write操作,然后再把要写入的buff里插入数, 写入不会 导致buff区区域扩展.(sql/net_serv.cc: net_write_buff).

● net->buff大小初始net->max_packet, 读取会导致会导致 buff的realloc最大net->max_packet_size

● 一次sql命令的结束都会调用net_flush,把buff里的数据 都写到socket里.

VIO缓冲

● 从my_read_read可以看出每次packet读取都是按需读取, 为了减少系统调用,vio层面加了一个read_buffer.

● 每次读取前先判断vio->read_buffer所需数据的长度是 否足够.如果存在则直接copy. 如果不够,则触发一次 socket read 读取2048个字(vio/viosocket.c: vio_read_buff)

MySQL API

● 数据从mysql_send_query处发送给服务端,实际调用的是 net_write_command.

● cli_read_query_result解析header packet, field packet,获 得field_count的个数

● mysql_store_result解析了row packet,并存储在result- >data里

● myql_fetch_row其实遍历result->data

PACKET NUMBER

在做proxy的时候在这里迷糊过,翻了几遍代码才搞明白,细节如下: 客户端服务端的net->pkt_nr都从0开始.接受包时比较packet number 和net->pkt_nr是否相等,否则报packet number乱序,连接报错;相等则pkt_nr自增.发送包时把net->pkt_nr作为packet number发送,然后对net->pkt_nr进行自增保持和对端的同步.

接收包

sql/net_serv.c:my_real_read

if (net->buff[net->where_b + 3] != (uchar) net->pkt_nr)

发送包

sql/net_serv.c:my_net_write

int3store(buff,len);

buff[3]= (uchar) net->pkt_nr++;

我们来几个具体场景的packet number, net->pkt_nr的变化

连接

c ———–> s 0 connect

c <—-0——s 1 handshake

c —–1—–>s 1 auth

c <—–2——s 0 ok

开始两方都为0,服务端发送handshake packet(pkt=0)之后自增为1,然后等待对端发送过来pkt=1的包

查询

每次查询,服务客户端都会对net->pkt_nr进行清零

include/mysql_com.h

#define net_new_transaction(net) ((net)->pkt_nr=0)

sql/sql_parse.cc:do_command

net_new_transaction(net);

sql/client.c:cli_advanced_command

net_clear(&mysql->net, (command != COM_QUIT));

开始两方net->pkt_nr皆为0, 命令发送后客户端端为1,服务端开始发送分包,分包的pkt_nr的依次递增,客户端的net->pkt_nr也随之增加.

c ——0—–> s 0 query

c <—-1——s 2 resultset

c <—-2——s 3 resultset

解包的细节

my_net_read负责解包,首先读取4个字节,判断packet number是否等于net->pkt_nr然后再次读取packet_number长度的包体。

伪代码如下:

remain=4

for(i = 0; i < 2; i++) {

//数据是否读完

while (remain>0) {

length = read(fd, net->buff, remain)

remain = remain - length

}

//第一次

if (i=0) {

remain = uint3korr(net->buff+net->where_b);

}

}

网络层优化

总结

这份面试题几乎包含了他在一年内遇到的所有面试题以及答案,甚至包括面试中的细节对话以及语录,可谓是细节到极致,甚至简历优化和怎么投简历更容易得到面试机会也包括在内!也包括教你怎么去获得一些大厂,比如阿里,腾讯的内推名额!

某位名人说过成功是靠99%的汗水和1%的机遇得到的,而你想获得那1%的机遇你首先就得付出99%的汗水!你只有朝着你的目标一步一步坚持不懈的走下去你才能有机会获得成功!

成功只会留给那些有准备的人!

c ———–> s 0 connect

c <—-0——s 1 handshake

c —–1—–>s 1 auth

c <—–2——s 0 ok

开始两方都为0,服务端发送handshake packet(pkt=0)之后自增为1,然后等待对端发送过来pkt=1的包

查询

每次查询,服务客户端都会对net->pkt_nr进行清零

include/mysql_com.h

#define net_new_transaction(net) ((net)->pkt_nr=0)

sql/sql_parse.cc:do_command

net_new_transaction(net);

sql/client.c:cli_advanced_command

net_clear(&mysql->net, (command != COM_QUIT));

开始两方net->pkt_nr皆为0, 命令发送后客户端端为1,服务端开始发送分包,分包的pkt_nr的依次递增,客户端的net->pkt_nr也随之增加.

c ——0—–> s 0 query

c <—-1——s 2 resultset

c <—-2——s 3 resultset

解包的细节

my_net_read负责解包,首先读取4个字节,判断packet number是否等于net->pkt_nr然后再次读取packet_number长度的包体。

伪代码如下:

remain=4

for(i = 0; i < 2; i++) {

//数据是否读完

while (remain>0) {

length = read(fd, net->buff, remain)

remain = remain - length

}

//第一次

if (i=0) {

remain = uint3korr(net->buff+net->where_b);

}

}

网络层优化

总结

这份面试题几乎包含了他在一年内遇到的所有面试题以及答案,甚至包括面试中的细节对话以及语录,可谓是细节到极致,甚至简历优化和怎么投简历更容易得到面试机会也包括在内!也包括教你怎么去获得一些大厂,比如阿里,腾讯的内推名额!

某位名人说过成功是靠99%的汗水和1%的机遇得到的,而你想获得那1%的机遇你首先就得付出99%的汗水!你只有朝着你的目标一步一步坚持不懈的走下去你才能有机会获得成功!

成功只会留给那些有准备的人!

[外链图片转存中…(img-bjhfRFek-1714702226889)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 18
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值