MySQL对你们来讲,都应该很熟悉了,从大学里的课程到实际工做中数据的存储查询,不少时候都须要用到数据库,不少人也写过与数据库交互的程序,在Java中你可能一开始会使用原生mysql-connector-java来进行操做,后来你会接触到Hibernate,Mybatis等ORM框架,其实它们底层也是基于mysql-connector-java,但不少时候咱们并不清楚程序是怎么跟数据库具体交互的,好比执行一个SQL查询,程序是如何从MySQL中获取数据的呢?今天就让咱们来看看最基础的MySQL网络协议分析。html
阅读本文以前你须要对网络协议须要有基本的了解,好比两台机子之间的数据是如何通讯的,硬件层能够暂时不需了解,但网络层和传输层的协议要有必定的理解,好比IP数据包,TCP/IP协议,UDP协议等相关概念,有了这些基础,有利于你阅读本文。java
在历史悠久的时代,数据库只做为单机存储,也不怎么须要与程序进行交互的时候的首,它的网络通讯并非那么重要,但随着时代的发展,数据库再也不只是单纯的做为一个数据的仓库了,它须要提供与外界的交互,好比远程链接,程序操做数据库等,这时候一份规范的网络通讯的协议就很是重要了,好比它是如何校验权限,如何解析SQL语句,如何返回执行结果都须要用到相应的协议,不少时候咱们并不须要接触这些内容,由于它太底层了,咱们直接使用把它们封装好的第三方包就能够了,为何还要去学习它的网络协议呢?确实对于一开始学习编程的人来讲,这有点操之过急,反而有时候会拔苗助长,但当你对这一方面有了必定的了解以后,你便会火烧眉毛得想去探索更深层的奥秘,去了解并学习咱们日常用的第三方类库是怎么去实现,明白它的底层原理,甚至对一些莫名其妙的bug也不会再惧怕。mysql
分析协议,咱们首先要了解如何与数据库链接,说到MySQL链接方式,你们忽然可能有点懵,其实它一直伴随着咱们,好比咱们第一次装数据库完成后执行的第一次登陆,好比你没有设置密码:算法
mysql -uroot
复制代码
这是最基本的一种数据库链接方式,那么MySQL链接方式到底有几种呢?到MySQL5.7为止,总共有五种,分别是TCP/IP,TLS/SSL,Unix Sockets,Shared Memory,Named pipes,下面咱们就来看看这五种的区别:sql
方式 | 默认开启 | 支持系统 | 只支持本机 | 如何开启 | 参数配置 |
---|---|---|---|---|---|
TCP/IP | 是 | 全部系统 | 否 | --skip-networking=yes/no. | --port --bind-address |
TLS/SSL | 是 | 全部系统(基于TCP/IP)之上 | 否 | --ssl=yes/no. | --ssl-* options |
Unix Sockets | 是 | 类Unix系统 | 是 | 设置--socket=<empty> 来关闭. | --socket=socket path |
Shared Memory | 否 | Windows系统 | 是 | --shared-memory=on/off. | --shared-memory-base-name=<name> |
Named pipes | 否 | Windows系统 | 否 | --enable-named-pipe=on/off. | --socket=<name> |
从上表中咱们能够清晰看出每种链接方式的区别,接下里我会具体说明几种链接是怎么操做的,因为个人机子是Mac OS系统,这里只模拟非Windows系统下的三种方式,由于这三种方式都是默认开启的,咱们不须要进行任何配置:数据库
mysql -uroot
复制代码
若你在本机使用这种方式链接MySQL数据库的话,它默认会使用Unix Sockets。编程
mysql --protocol=tcp -uroot
mysql -P3306 -h127.0.0.1 -uroot
复制代码
链接的时候咱们指定链接协议,或者指定相应的IP及端口,咱们的链接方式就变成了TCP/IP方式。缓存
mysql --protocol=tcp -uroot --ssl=on
mysql -P3306 -h127.0.0.1 -uroot --ssl=on
复制代码
上表说过,TLS/SSL是基于TCP/IP的,因此咱们只需再指定打开ssl配置便可。安全
而后咱们能够经过如下语句来查询目前数据库的链接状况:bash
SELECT DISTINCT connection_type from performance_schema.threads where connection_type is not null
复制代码
那么咱们如何选择链接方式呢?我的总结了如下几个原则:
通讯中最重要的就是数据,那么程序是如何和MySQL Server进行通讯,并交互数据的呢?好比如何验证帐户,发送查询语句,返回执行结果等,我先画一个流程图来模拟一下整个过程,帮助你们理解:
整个过程相对来讲仍是比较清晰的,咱们对链接请求和断开请求不须要过度关心,只须要了解这一点就能够了,重要的是其余几点,那么在这几步中,数据是怎么进行交互的呢?
其实主要就是两步,Client将执行命令编码成Server要求的格式传输给Server端执行,Server端将执行结果传输给Client端,Client端再根据相应的数据包格式解析得到所需的数据。
虽然网络中的数据是用字节传输的,但它背后的数据源都是有类型的数据,MySQL协议也有基本的数据类型,比如Java中的8种基本数据类型,但MySQL协议中简单的多,它只有两种基本数据类型,分别为Integer(整型),String(字符串),下面咱们就来看看这两种类型。
首先Integer在MySQL协议中有两种编码方式,分别为FixedLengthInteger和LengthEncodedInteger ,其中前者用于存储无符号定长整数,实际中使用的很少,这里着重讲一下后者。
使用LengthEncodedInteger编码的整数可能会使用1, 3, 4, 或者9 个字节,具体使用字节取决于数值的大小,下表是不一样的数据长度的整数所使用的字节数:
最小值(包含) | 最大值(不包含) | 存储方式 |
---|---|---|
0 | 251 | 1个字节 |
251 | 2^16 | 3个字节(0xFC + 2个字节具体数据) |
2^16 | 2^24 | 4个字节(0xFD + 3个字节具体数据) |
2^24 | 2^64 | 9个字节(0xFE + 8个字节具体数据) |
举个简单的例子,好比1024的编码为:
0xFC 0x00 0x04
复制代码
其中0x表明16进制,实际数据传输中并无该标识,第一位表明这是一个251~2^16之间的数值,因此后面两位为数值具体的值,这里使用的是小端字节序,MySQL默认使用的也是这种编码次序,因此这里1024是0x00 0x04,字节序相关知识能够参考:理解字节序,到这里你们应该对这种编码格式有了必定的了解了,下面咱们就来看看String。
String的编码格式相对Integer来讲会复杂一点,主要有如下几种:
总的来讲String的编码格式种类相对比较多,不一样方式之间的区别也比较大,若要深入理解还需从实际的例子里去学习,后续文章中我会写几个demo带你们一块儿去探索。
数据包格式也主要分为两种,一种是Server端向Client端发送的数据包格式,另外一种则是Client向Server端发送的数据包。
Server向Client发送的数据包有两个原则:
每一个包的基本格式:
Type | Name | Description |
---|---|---|
int<3> | payload_length(包数据长度) | 具体数据包的内容长度,从出去头部四个字节后开始的内容 |
int<1> | sequence_id(包序列id) | 每一个包的序列id,总数据内容大于16MB时须要用,从0开始,依次增长,新的命令执行会重载为0 |
string | payload(具体数据) | 包中除去头部后的具体数据内容 |
举个列子:
例子 | 解释 |
---|
01 00 00 00 01| <li>payload_length: 1</li> <li>sequence_id: 0x00</li><li>payload: 0x01</li>
复制代码
如果数据内容大于或者等于2^24-1个字节,将会拆分发送,举个例子,好比发送16 777 215 (2^24-1) 字节的内容,则会按一下这种方式发送
ff ff ff 00 ...
00 00 00 01
复制代码
第一个数据包满载,第二个数据包是一个空数据包(一种临界状况)。
Client向Server端发送的格式相对来讲就简单一点了
Type | Name | Description |
---|---|---|
int<1> | 执行命令 | 执行的操做,好比切换数据库,查询表等操做 |
string | 参数 | 命令相应的参数 |
命令列表(摘抄自胡桃夹子的博客):
类型值 | 命令 | 功能 |
---|---|---|
0x00 | COM_SLEEP | (内部线程状态) |
0x01 | COM_QUIT | 关闭链接 |
0x02 | COM_INIT_DB | 切换数据库 |
0x03 | COM_QUERY | SQL查询请求 |
0x04 | COM_FIELD_LIST | 获取数据表字段信息 |
0x05 | COM_CREATE_DB | 建立数据库 |
0x06 | COM_DROP_DB | 删除数据库 |
0x07 | COM_REFRESH | 清除缓存 |
0x08 | COM_SHUTDOWN | 中止服务器 |
0x09 | COM_STATISTICS | 获取服务器统计信息 |
0x0A | COM_PROCESS_INFO | 获取当前链接的列表 |
0x0B | COM_CONNECT | (内部线程状态) |
0x0C | COM_PROCESS_KILL | 中断某个链接 |
0x0D | COM_DEBUG | 保存服务器调试信息 |
0x0E | COM_PING | 测试连通性 |
0x0F | COM_TIME | (内部线程状态) |
0x10 | COM_DELAYED_INSERT | (内部线程状态) |
0x11 | COM_CHANGE_USER | 从新登录(不断链接) |
0x12 | COM_BINLOG_DUMP | 获取二进制日志信息 |
0x13 | COM_TABLE_DUMP | 获取数据表结构信息 |
0x14 | COM_CONNECT_OUT | (内部线程状态) |
0x15 | COM_REGISTER_SLAVE | 从服务器向主服务器进行注册 |
0x16 | COM_STMT_PREPARE | 预处理SQL语句 |
0x17 | COM_STMT_EXECUTE | 执行预处理语句 |
0x18 | COM_STMT_SEND_LONG_DATA | 发送BLOB类型的数据 |
0x19 | COM_STMT_CLOSE | 销毁预处理语句 |
0x1A | COM_STMT_RESET | 清除预处理语句参数缓存 |
0x1B | COM_SET_OPTION | 设置语句选项 |
0x1C | COM_STMT_FETCH | 获取预处理语句的执行结果 |
这里距一个常见的的例子,好比切换数据库:
use godpan
复制代码
相应的报文格式则为:
0x02 0x67 0x6f 0x64 0x70 0x61 0x6e
复制代码
其中0x02表明切换数据库命令,后面的字节则为godpan的16进制表达。
有了以上的基础,咱们基本知道的与MySQL通讯之间的方式以及数据格式,那么与其通讯间到底有哪几种数据包呢?接下去的内容是创建在MySQL4.1版本之后,以前版本的数据包类型这里再也不论述。
这里主要分为两个阶段,第一个阶段是数据库帐户认证阶段,第二个阶段则是执行具体命令阶段,咱们先来看看前者。
这个阶段就是咱们日常所说的登陆,主要步骤以下:
这里咱们来看一看上面的Handshake packet和Auth packet,OK packet和ERR packet放在另外一个阶段写。
Handshake packet是由Server向Client发送的初始化包,由于全部从Server向Client端发送的包都是同样的格式,因此前面的四个字节是包头,前三位表明Handshake packet具体内容的数据,另外包序列号为0,很显然这个包内容小于16MB,下面是Handshake packet具体内容的格式:
相对包内容的位置 | 长度(字节) | 名称 | 描述 |
---|---|---|---|
0 | 1 | 协议版本 | 协议版本的版本号,一般为10(0x0A) |
1 | len = strlen (server_version) + 1 | 数据库版本 | 使用前面的NullTerminatedString格式编码,长度为数据库版本字符串的长度加上标示结束的的一个字节 |
len + 1 | 4 | 线程ID | 这次链接MySQL Server启动的线程ID |
len + 5 | 8 + 1(0x00表示结束) | 挑战随机数(第一部分) | 用于后续帐户密码验证 |
len + 14 | 2 | 协议协商 | 用于与客户端协商通信方式 |
len + 16 | 1 | 编码格式 | 标识数据库目前的编码方式 |
len + 17 | 2 | 服务器状态 | 用于表示服务器状态,好比是不是事务模式或者自动提交模式 |
len + 19 | 13 | 保留字节 | 将来可能会用到,预留字节 |
len + 32 | 12 + 1(0x00表示结束) | 挑战随机数(第二部分) | 用于后续帐户密码验证 |
上表就是整个Handshake packet的这个包结构,属性的含义以及规范都有相应的说明,下面是我本机解析的某次链接数据库的Handshake packet包,仅供参考:
{protocolVersion=10, serverVersion='5.7.13', threadId=4055, scramble=[49, 97, 80, 3, 35, 118, 45, 15, 5, 118, 9, 11, 124, 93, 93, 5, 31, 47, 111, 109, 0, 0, 0, 0, 0], serverCapabilities=65535, serverLanguage=33, serverStatus=2}
复制代码
Auth packet是由Client向Server发送的认证包,用于验证数据库帐户登陆,相应内容的格式:
相对包内容的位置 | 长度(字节) | 名称 | 描述 |
---|---|---|---|
0 | 4 | 协议协商 | 用于与服务端协商通信方式 |
4 | 4 | 消息最长长度 | 客户端能够发送或接收的最长长度,0表示不作任何限制 |
8 | 1 | 字符编码 | 客服端字符编码方式 |
9 | 23 | 保留字节 | 将来可能会用到,预留字节,用0代替 |
32 |不定| 认证字符串 | 主要有三部份内容<br> <li>用户名:NullTerminatedString格式编码</li><li>加密后的密码:LengthEncodedString格式编码</li><li>数据库名称(可选):NullTerminatedString格式编码</li>
复制代码
这部份内容是由客户端本身生成,因此说若是咱们若是要写一个程序链接数据库,那么这个包就得按照这个格式,否则服务端将会没法识别。
在咱们正确链接数据库后,咱们就要执行相应的命令了,好比切换数据库,执行CRUD操做等,这个阶段主要分为两步,Client发送命令(上文已经给出,下面再也不讨论),Server端接收命令执行相应的操做,咱们主要关心Server端向咱们发送数据包,可分为4类和一个最基础的报文结构Data Field:
Data Field是Server回应包里的一个核心,主要是数据的一种编码结构,跟我以前讲的LengthEncodedInteger和LengthEncodedString很相似,也主要分为三个部分
最小数据长度(包含)|最大数据长度(不包含)|数据长度|格式 ---|---|---| 1 |251| 1个字节|1字节 + 具体数据 251 |2^16| 2个字节 | 0xFC + 2个字节数据长度 + 具体数据 2^16 |2^24| 4个字节 | 0xFD + 4个字节数据长度 + 具体数据 2^24 |2^64| 8个字节 | 0xFE + 8个字节数据长度 + 具体数据 NULL | NULL | 0个字节 | 0xFB
要注意的一点是若是出现0xFB(251)开头说明这个数据对应的是MySQL中的NULL。
普通的OK包(PREPARE_OK包后面会讲到)会在如下几种状况下产生,由Server发送给相应的接收方:
OK 包的主要结构:
相对包内容的位置 | 长度(字节) | 名称 | 描述 |
---|---|---|---|
0 | 1 | 包头标识 | 0x00 表明这是一个OK 包 |
1 | rows_len | 影响行数 | 相应操做影响的行数,好比一个Update操做的记录是5条,那么这个值就为5 |
1 + rows_len | id_len | 自增id | 插入一条记录时,若是是自增id的话,返回的id值 |
1 + rows_len + id_len | 2 | 服务器状态 | 用于表示服务器状态,好比是不是事务模式或者自动提交模式 |
3 + rows_len + id_len | 2 | 警告数 | 上次命令引发的警告数 |
5 + rows_len + id_len | msg_len | 额外信息 | 这次操做的一些额外信息 |
下面是我本机解析的某次正确链接数据库后的OK packet包,仅供参考:
OK{affectedRows=0, insertId=0, serverStatus=2, message='....'}
复制代码
顾名思义Error 包就是当出现错误的时候返回的信息,好比帐户验证不经过,查询命令不合法,非空字段未指定值等相关操做,Server端都会向Client端发送Error 包。
Error 包的主要结构:
相对包内容的位置 | 长度(字节) | 名称 | 描述 |
---|---|---|---|
0 | 1 | 包头标识 | 0xFF 表明这是一个Error 包 |
1 | 2 | 错误代码 | 该错误的相应错误代码 |
3 | 1 | 标识位 | SQL执行状态标识位,用'#'进行标识 |
4 | 5 | 执行状态 | SQL的具体执行状态 |
9 | msg_len | 错误信息 | 具体的错误信息 |
好比咱们如今已经链接了数据库,执行
use test_database;
复制代码
可是咱们数据库中并无test_database这个数据库,咱们将会获得相应的错误信息,下面是我本机解析的Error packet包,仅供参考:
Error{errno=1046, sqlState='3D000', message='No database selected'}
复制代码
EOF Packet是用于标识某个阶段数据结束的标志包,会在一下几种状况中产生:
EOF 包的主要结构:
相对包内容的位置 | 长度(字节) | 名称 | 描述 |
---|---|---|---|
0 | 1 | 包头标识 | 0xFE 表明这是一个EOF 包 |
1 | 2 | 警告数 | 上次命令引发的警告数 |
3 | 2 | 服务器状态 |
这里要注意的一点,咱们上面分析了Data Field的结构,发现它是用0xFE做为长度须要8个字节编码值得标识头,因此咱们在判断一个包是不是EOF 包的时候,须要下面两个条件:
Result Set包产生于咱们每次数据库执行须要返回结果集的时候,Server端发送给咱们的包,好比日常的SELECT,SHOW等命令,Result Set包相对比较复杂,主要包含如下五个方面:
内容 | 含义 |
---|---|
Result Set Header | 返回数据的列数量 |
Field | 返回数据的列信息(多个) |
EOF | 列结束 |
Row Data | 行数据(多个) |
EOF | 数据结束 |
咱们逐个来分析,首先咱们来看Result Set Header。
Result Set Header表示返回数据的列数量以及一些额外的信息,其主要结构为:
长度 | 含义 |
---|---|
1-9字节 | 数据的列数量(LengthEncodedInteger编码格式) |
1-9字节 | 额外信息(LengthEncodedInteger编码格式) |
Field表示Result Set中数据列的具体信息,可出现屡次,具体次数取决于Result Set Header中数据的列数量,它的主要结构为:
长度 | 含义 |
---|---|
4 | 一般为ASCIIz字符串def |
n | 数据库名称(Data Field) |
n | 假如查询指定了表别名,就是表别名(Data Field) |
n | 原始的表名(Data Field) |
n | 假如查询指定了列别名,就是列别名(Data Field) |
n | 原始的列名(Data Field) |
1 | 标识位,一般为12,表示接下去的12个字节是具体的field内容 |
2 | field的编码 |
4 | field的长度 |
1 | field的类型 |
2 | field的标识 |
2 | field值的的小数点精度 |
2 | 预留字节 |
n | 可选元素,若是存在,则表示该field的默认值 |
其中field的类型与标识具体定义和对应变量含义可参考这篇文章:MySQL协议分析
这里的EOF包是标识这列信息的结束,具体结构信息参考上面的EOF包解释。
Row Data含着的是咱们须要获取的数据,一个Result Set包里面包含着多个Row Data结构(获得的数据可能多行),每一个Row Data中包含着多个字段值,它们之间没有间隔,好比咱们如今查询到的数据为(id: 1, name: godpan) 那么Row Data内容为(1,godpan),这两个值是连在一块儿的,对应的值都用LengthEncodedString编码。
等待Row Data发送完以后,Server最后会向Client端发送一个EOF包,标识全部的行数据已经发送完毕。
PREPARE_OK包产生在Client端向Server发送预处理SQL语句,Server进行正确回应的时候,你们写写Java的时候确定用过PreparedStatement,这里PreparedStatement的功能就是进行SQL的预处理,预处理的优势比较多,好比效率高,防SQL注入等,有兴趣的同窗能够本身去学习下。下面是PREPARE_OK包的结构:
长度 | 含义 |
---|---|
1 | 0x00(标识是一个OK包) |
4 | statement_handler_id(预处理语句id) |
2 | number of columns in result set(结果集中列的数量) |
2 | number of parameters in query(查询语句中参数的数量) |
1 | 0x00 (填充值) |
2 | 警告数 |
好比我如今执执行下面的语句:
PreparedStatement ps = connection.prepareStatement("SELECT * FROM `godpan_fans` where id=?");
ps.setInteger(1, 1);
ps.executeQuery();
复制代码
获得下面的PREPARE_OK包,仅供参考:
PSOK{statementId=1, columns=5, parameters=1}
复制代码
若是上面的columns大于0,以及parameters大于0,则将有额外的两个包传输,分别是columns的信息以及parameters的信息,对应信息结构:
内容 | 含义 |
---|---|
Field | columns信息(多个) |
EOF | columns信息结束 |
Field | parameters(多个) |
EOF | parameters结束 |
到此整个PREPARE_OK包发送完毕。
这个包跟上面提到的Row Data包有什么差异呢?主要有两点:
后面我会分别解释这两点,咱们先来看看它的结构:
相对包内容的位置 | 长度(字节) | 名称 | 描述 |
---|---|---|---|
0 | 1 | 包头标识 | 0x00 |
1 | (col_count+7+2)/8 | Null Bit Map | 前两位为预留字节,主要用于区别与其余的几种包(OK,ERROR,EOF),在MySQL 5以后这两个字节都为0X00,其中col_count为列的数量 |
(col_count+7+2)/8 + 1 | n | column values | 具体的列值,重复屡次,根据值类型编码 |
如今咱们来看一下它的两个特色,首先咱们来看它是如何来定义NULL的,首先咱们看到他的结构中有一个Null Bit Map,除去两个标识位,真正用于标识数据信息的就是(col_count+7)/8位字节,这里我先给出结论,后面再给你们具体分析:
参数个数 | 长度(字节) | 具体值范围 | 描述 |
---|---|---|---|
1-8 | 1 | -1, 2^n组合 | 1 = 2^0表示第一个参数为NULL,3 = 2^0 + 2^1表示第一个和第二参数为NULL... |
上面给出了标识NULL的基本算法,原则是哪一个参数(次序为n)为NULL,则Null Bit Map相应的值加上2^n,8个参数为一个周期,以此类推。
接着咱们来看一下第二点,是如何用具体值类型来对相应的值进行编码的,这里主要分为三类,基本数据类型,时间类型,字符串类型;
Execute包顾名思义是一个执行包,它是由Client端发送到Server端的,但它和普通的命令又有点不一样,它主要是用来执行预处理语句,并会携带相应参数,具体结构以下:
长度 | 含义 |
---|---|
1 | COM_EXECUTE(标识是一个Execute包) |
4 | 预处理语句id |
1 | 游标类型 |
4 | 预留字节 |
0 | 接下去的内容只有在有参数的状况下 |
(param_count+7)/8 | null_bit_map(描述参数中NULL的状况) |
1 | 参数绑定状况 |
n*2 | 参数类型(依次存储) |
n | 参数具体值(非NULL)(依次存储,使用Row Data Binary方式编码) |
Execute包从Client端发送到Server端后可能会获得如下几个结果:
咱们须要根据包的不一样类型来进行不一样的处理。
本篇文章主要讲述了MySQL的链接方式,通讯过程及协议,以及传输包的基本格式和相关传输包的类型,内容相对来讲,比较多也比较复杂,我也是将近三周才写完,但整体按照我自学的思路走,不会太绕,有些点可能须要细心思考下,写的有误的地方也但愿你们能指正,但愿对你们有所帮助,后面可能会写几个实例和你们一块儿学习。