博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
MySQL 通讯协议
阅读量:5285 次
发布时间:2019-06-14

本文共 13604 字,大约阅读时间需要 45 分钟。

Client/Server 通讯协议用于客户端链接、代理、主备复制等,支持 SSL、压缩,在链接阶段进行认证,在执行命令时可以支持 Prepared Statements 以及 Stored Procedures 。

当打算编写数据库代理、中间件、对 MySQL 数据包进行审核时,都需要了解底层的通信协议。在本文中,主要介绍 MySQL 通讯协议相关的内容。

简介

服务器启动后,会使用 TCP 监听一个本地端口,当客户端的连接请求到达时,就会执行三段握手以及 MySQL 的权限验证;验证成功后,客户端开始发送请求,服务器会以响应的报文格式返回数据;当客户端发送完成后,会发送一个特殊的报文,告知服务器已结束会话。

MySQL 定义了几种包类型,A) 客户端->服务器,登录时的 auth 包、执行 SQL 时的 CMD 包;B) 服务器->客户端,登录时的握手包、数据包、数据流结束包、成功包(OK Packet)、错误信息包。

协议定义了基本的数据类型,如 int、string 等;数据的传送格式等。

协议

MySQL 的客户端与服务器之间支持多种通讯方式,最广泛使用的是 TCP 通讯;另外,还支持命名管道和共享内存,而 TCP 就是最通用的一种方式,在此仅介绍 TCP 方式。

在 C/S 之间,实际采用的是一种类似半双工式的模式收发数据,即在一个 TCP 链路上,客户端发出请求数据后,只有在接收完所有的服务端响应数据以后才能发下一次请求,中间不能发其它数据,需要有很强的顺序性要求。

MySQL 客户端与服务器的交互主要分为两个阶段,分别为握手认证阶段和命令执行阶段,详细来说一次正常的过程如下:

1. 三次握手建立 TCP 连接。2. 建立 MySQL 连接,也就是认证阶段。    服务端 -> 客户端:发送握手初始化包 (Handshake Initialization Packet)。    客户端 -> 服务端:发送验证包 (Client Authentication Packet)。    服务端 -> 客户端:认证结果消息。3. 认证通过之后,客户端开始与服务端之间交互,也就是命令执行阶段。    客户端 -> 服务端:发送命令包 (Command Packet)。    服务端 -> 客户端:发送回应包 (OK Packet, or Error Packet, or Result Set Packet)。4. 断开 MySQL 连接。    客户端 -> 服务器:发送退出命令包。5. 四次握手断开 TCP 连接。

从服务器发往客户端的数据包有四种:数据包、数据结束包、成功报告包以及错误消息包。Result Set Packet 首先发送包头+列包+EOF包+行包+EOF包。

报文格式

所有的包有统一的格式,并通过函数 my_net_write()@sql/net_serv.cc 写入 buffer 等待发送。

+-------------------+--------------+---------------------------------------------------+|      3 Bytes      |    1 Byte    |                   N Bytes                         |+-------------------+--------------+---------------------------------------------------+|<= length of msg =>|<= sequence =>|<==================== data =======================>||<============= header ===========>|<==================== body =======================>|

MySQL 报文格式如上,消息头包含了 A) 报文长度,标记当前请求的实际数据长度,以字节为单位;B) 序号,为了保证交互时报文的顺序,每次客户端发起请求时,序号值都会从 0 开始计算。

消息体用于存放报文的具体数据,长度由消息头中的长度值决定。

单个报文的最大长度为 (2^24-1)Bytes ,也即 (16M-1)Bytes,对于包长为 (2^24-1)Bytes 也会拆为两个包发送。这是因为最初没有考虑 16M 的限制,从而没有预留任何字段来标志这个包的数据不完整,所以只好把长度为 (2^24-1) 的包视做不完整的包,直到后面收到一个长度小于 (2^24-1) 的包,然后拼起来。

这也意味着最后一个包的长度有可能是 0。

基本类型

接下来介绍一下报文中的数据类型,也就是不同的数据类型在报文中的表现形式。

整型值

MySQL 报文中整型值分别有 1、2、3、4、8 字节长度,使用小字节序传输。

二进制数据

也就是 Length Coded Binary,其数据长度不固定,长度值由数据前的 1-9 个字节决定,其中长度值所占的字节数不定,字节数由第 1 个字节决定,如下:

第一个字节值    后续字节数  长度值说明    0-250            0   第一个字节值即为数据的真实长度      251            0   空数据,数据的真实长度为零      252            2   后续额外2个字节标识了数据的真实长度      253            3   后续额外3个字节标识了数据的真实长度      254            8   后续额外8个字节标识了数据的真实长度

字符串

根据是否以 NULL 结尾,分为了有两种形式:

  • 以 NULL 结尾,Null-Terminated String
    字符串长度不固定,当遇到 'NULL'(0x00) 字符时结束。
  • 长度编码,Length Coded String
    字符串长度不固定,无 'NULL'(0x00) 结束符,编码方式与上面的二进制数据相同。

客户端请求报文

也就是从客户端发送到服务端的请求命令。

+-------------------+------------------------------------------------------------------+|      1 Bytes      |                             N Bytes                              |+-------------------+------------------------------------------------------------------+|<==== command ====>|<============================ arguments =========================>|

客户端向服务端发送的请求,其中第一个字节用于标识当前请求消息的类型,这也就定义了请求的种类,其中包括了:切换数据库 COM_INIT_DB(0x02)、查询命令 COM_QUERY(0x03) 等。

命令的宏定义在 include/mysql_com.h 文件中,该命令会在 dispatch_command() 中根据不同的命令进入不同代码处理逻辑。

报文中的参数内容是用户在 MySQL 客户端输入的命令,不包括每行命令结尾的 ';' 分号,采用的是非 NULL 结尾的字符串表示方法。

例如:当在 MySQL 客户端中执行 use mysql; 命令时,发送的请求报文数据会是下面的样子:

0x02 0x6d 0x79 0x73 0x71 0x6c

0x02 为请求类型值 COM_INIT_DB,后面的 0x6d 0x79 0x73 0x71 0x6c 为 ASCII 字符 mysql 。

错误码

也就是当发生了错误之后,服务端发送给客户端的报文。

MySQL 的错误包含了三部分:A) MySQL 特定的错误码,数字类型,不通用;B) SQLSTATE,为 5 个字符的字符串,采用 ANSI SQL 和 ODBC 的标准;C) 错误信息。

对于错误报文的格式可以参照参考文件,其中第二字节表示由 MySQL 定义的错误编码,服务器状态实际是 ANSI SQL 对应的编码,两者并非一一对应。

在 MySQL 中可以通过 perror ERROR 查看;详细的文档,可以参考官方文档  。

抓包分析

可以通过 tcpdump 捕获包并保存在文件中,然后通过 Wireshark 打开文件,查看网络包的内容,相对来说比较方便。可以通过 tcpdump -D 查看支持的网卡接口,通过 -i 指定接口,在此使用 lo

注意,tcpdump 不能捕获 unix socket,链接时不能使用 -S /tmp/mysql.sock 或者 -h localhost 参数,应当使用 -h 127.1 。

可以将 tcpdump 的包输出到 stdout 或者通过 -w 保存到文件,然后用 Wireshark 分析。

----- 将抓包的数据保存到文件# tcpdump -i lo port 3306 -w filename----- 当然,也可以打印到终端,然后处理数据# tcpdump -i lo port 3306 -nn -X -q# tcpdump -i any -s 0 -l -w - dst port 3306 | strings | grep -iE 'select|update'

认证协议

认证稍微有点复杂,单独拉出来。

MySQL 的用户管理模块信息存储在系统表 mysql.user 中,其中包括了授权用户的基本信息以及一些权限信息。在登陆时,只会用到 host、user、passwd 三个字段,也就是说登陆认证需要 host+user 关联,当然可以使用 * 通配符。

服务器在收到新的连接请求时,会调用 login_connection() 作身份验证,先根据 IP 做 ACL 检查,然后才进入用户名密码验证阶段。

其中报文的格式如下。

auth protocol

MySQL 认证采用经典的 CHAP 协议,即挑战握手认证协议,在 native_password_authenticate()函数的注释中简单介绍了该协议的执行过程:

1. the server sends the random scramble to the client.2. client sends the encrypted password back to the server.3. the server checks the password.

random scramble 在 4.1 之前的版本中是 8 字节整数,在 4.1 以及后续版本是 20 字节整数,该值是通过 create_random_string() 函数生成。

根据版本不同,分为了两类。

4.0版本之前

基本流程如下:

  1. 服务器发送随机字符串 (scramble) 给客户端。可以参考 create_random_string() 的生成方法。
  2. 客户端把用户明文密码加密一下,然后再将其与服务器发送的随机字符串加密一下,然后变成了新的 scramble_buff 发送给服务端。可以参考 scramble() 函数的实现。
  3. 服务端将 mysql.user.password 中的值加上原始随机字符串进行加密,如果加密后的值和客户端发送过来的内容一样,则验证成功。

需要注意的是:真正意义上的密码是明文密码的加密 hash 值; 如果有人知道了这个用户的 password 哈希值,而不用知道原始明文密码,实际上他就能直接登录服务器。

4.1 以后版本

数据库中保存的密码是用 SHA1(SHA1(password)) 加密的,其流程为:

  1. 服务器发送随机字符串 (scramble) 给客户端。
  2. 客户端作如下计算,然后客户端将 token 发送给服务端。

    stage1_hash = SHA1(明文密码)

    token = SHA1(scramble + SHA1(stage1_hash)) XOR stage1_hash

  3. 服务端作如下计算,比对 SHA1(stage1_hash) 和 mysql.user.password 是否相同。

    stage1_hash = token XOR SHA1(scramble + mysql.user.password)

这里实际用到了异或的自反性: A XOR B XOR B = A ,对于给定的数 A,用同样的运算因子 B 作两次异或运算后仍得到 A 本身。对于当前情况的话,实际的计算过程如下。

token = SHA1(scramble + SHA1(SHA1(password))) XOR SHA1(password)         // 客户端返回的值      = PASSWORD XOR SHA1(password)stage1_hash = token XOR SHA1(scramble + mysql.user.password) = token XOR PASSWORD            = [PASSWORD XOR SHA1(password)] XOR PASSWORD            = SHA1(password)

因此,校验时,只需要 SHA1(stage1_hash) 与 mysql.user.password 比较一下即可。

这次没上一个版本的缺陷了. 有了 mysql.user.password 和 scramble 也不能获得 token,因为没法获得 stage1_hash

但是如果用户的 mysql.user.password 泄露,并且可以在网络上截取的一次完整验证数据,从而可以反解出 stage1_hash 的值。而该值是不变的,因此下次连接获取了新的 scramble 后,自己加密一下 token 仍然可以链接到服务器。

源码分析

接下来分别介绍客户端、服务端的程序。

客户端

对于 mysql 客户端,源码保存在 client/mysql.cc 文件中,下面是 main() 函数的主要执行流程。

main() |-sql_connect() | |-sql_real_connect() |   |-mysql_init()                             # 调用MySQL初始化 |   |-mysql_options()                          # 设置链接选项 |   |-mysql_real_connect()                     # sql-common/client.c |     |-connect_sync_or_async()                # 通过该函数尝试链接 |     | |-my_connect()                         # 实际通过该函数建立链接 |     |-cli_safe_read()                        # 等待第一个handshake包 |     |-run_plugin_auth()                      # 通过插件实现认证 | |-put_info()                                   # 打印客户端的欢迎信息 |-read_and_execute()                           # 开始等待输入、执行SQL

客户端最终会调用 mysql_real_connect(),实际调用的是 cli_mysql_real_connect(),通过该函数建立链接,其中认证方式可以通过 run_plugin_auth() 时用插件实现。

然后,会输出一系列的欢迎信息,并通过 read_and_execute() 执行 SQL 命令。

在 MySQL 客户端执行时,并非所有的命令都是需要发送到服务端的,其中有一个数组定义了常见的命令。

static COMMANDS commands[] = { { "?", '?', com_help, 1, "Synonym for `help'." }, { "clear", 'c', com_clear, 0, "Clear the current input statement."}, ... ... };

每次读取一行都会通过 find_command() 函数进行检测,如果满足对应的命令,且对应的函数变量非空,则直接执行,如 clear,此时不需要输入分号即可;如果没有找到,则必须要等待输入分号。

int read_and_execute(bool interactive) { while (!aborted) { if (!interactive) { // 是否为交互模式 ... ... // 非交互模式,直接执行 } else { // 交互模式 char *prompt = ...; // 首先会设置提示符 line = readline(prompt); // 从命令行读取 if ( ... && (com= find_command(line))) { // 从commands[]中查找 (*com->func)(&glob_buffer,line); // 如果是help、edit等指令,则直接执行 } add_line(...); // 常见的SQL,最终在此执行 } } } int com_go(String *buffer,char *line) { timer=start_timer(); // 设置时间 error= mysql_real_query_for_lazy(buffer->ptr(),buffer->length()); // 执行查询SQL do { // 获取结果 } while(!(err= mysql_next_result(&mysql))); }

在 add_line() 函数中,最终会调用 com_go() 函数,该函数是执行的主要函数,会最终调用 MySQL API 执行相应的 SQL、返回结果、输出时间等统计信息。

服务端

服务端通过 network_init() 执行一系列初始化之后,会阻塞在 handle_connections_sockets() 函数的 select()/poll() 函数处。

对于 one_thread_per_connection 这种方式,会新建一个线程执行 handle_one_connection() 。

handle_one_connection() |-thd_prepare_connection()   |-login_connection()     |-check_connection()       |-acl_authenticate()

源码内容如下。

/* sql/sql_connect.cc */int check_connection(THD *thd) { if (!thd->main_security_ctx.host) { // 通过TCP/IP连接,或者本地用-h 127.1 if (acl_check_host(...)) // 检查hostname } else { // 使用unix sock连接,不会进行检测 ... ... } return acl_authenticate(thd, connect_errors, 0) } /* sql/sql_acl.cc */ bool acl_authenticate(THD *thd, uint connect_errors, uint com_change_user_pkt_len) { if (command == COM_CHANGE_USER) { } else { do_auth_once() // 执行认证模式 } }

在 acl_check_host() 会检查两个对象,一个是 hash 表 acl_check_hosts;另一个是动态数组 acl_wild_hosts 。这2个对象是在启动的时候,通过 init_check_host() 从 mysq.users 表里读出并加载的,其中 acl_wild_hosts 用于存放有统配符的主机,acl_check_hosts 存放具体的主机。

最终会调用 acl_authenticate() 这是主要的认证函数。

插件实现

MySQL 的认证授权采用插件实现。

默认采用 mysql_native_password 插件,也就是使用 native_password_auth_client() 作用户端的认证,实际有效的函数是 scramble()

上述的函数通过用户输入的 password、服务器返回的 scramble 生成 reply,返回给服务端;可以通过 password('string') 查看加密后的密文。

以 plugin/auth/ 目录下的插件为例,在启动服务器时,可添加 --plugin-load=auth_test_plugin.so参数自动加载相应的授权插件。

----- 获得foobar的加密格式mysql> select password('foobar');----- 旧的加密格式mysql> select old_password('foobar');----- 默认方式mysql> create user 'foobar2'@'localhost' identified via mysql_native_password using 'xxx';----- 也可以动态加载mysql> install plugin test_plugin_server soname 'auth_test_plugin.so';----- 查看当前支持的插件mysql> select * from information_schema.plugins where plugin_type='authentication';mysql> create user 'foobar'@'localhost' identified with test_plugin_server;mysql> SET PASSWORD FOR 'foobar'@'localhost'=PASSWORD('new_password');mysql> DROP USER 'foobar'@'localhost';mysql> FLUSH PRIVILEGES;mysql> SELECT host, user, password, plugin FROM mysql.user;

在 plugin 目录下有很多 auth 插件可供参考,详细可参考官网  。

总结

在如下列举客户端与服务端的详细交互过程,其中客户端代码在 client 目录下。

### Client(mysql)  ###                       ### Server(mysqld) ###----------------------------------------     --------------------------------------------------main()                                       mysqld_main() |-sql_connect()                              |-init_ssl() | |-sql_real_connect() {for(;;)}             |-network_init() |   |-mysql_init()                           |-handle_connections_sockets() |   |-init_connection_options()                |-select()/poll() |   |-mysql_real_connect()                     | |     |-cli_mysql_real_connect()               | |       |-socket()                             | |       |-vio_new()                            | |       |-vio_socket_connect()                 | |       | |-mysql_socket_connect()             | |       |   |-connect()                        | |       |   |                                  | |       |   |        [Socket Connect]          | |       |   |>>==========>>==========>>======>>| |       |                                      |-accept() |       |-vio_keepalive()                      |-vio_new() |       |-my_net_set_read_timeout()            |-my_net_init() |       |-my_net_set_write_timeout()           |-create_new_thread() |       |-vio_io_wait()                          |-handle_one_connection()    {新线程} |       |                                          |-thd_prepare_connection() {for(;;)} |       |                                          | |-login_connection() |       |                                          |   |-check_connection() |       |                                          |     |-acl_check_host() |       |                                          |     |-vio_keepalive() |       |                                          |     |-acl_authenticate() |       |                                          |       |-do_auth_once() |       |                                          |       | |-native_password_authenticate()  {插件实现} |       |                                          |       |   |-create_random_string() |       |                                          |       |   |-send_server_handshake_packet() |       |                                          |       |   | |       |              [Handshake Initialization]  |       |   | |       |<<==<<==========<<==========<<==========<<==========<<| |       |-cli_safe_read()                          |       |   |-my_net_read() |       |-run_plugin_auth()                        |       |   | |       | |-native_password_auth_client()          |       |   | |       |   |-scramble()                           |       |   | |       |     |-my_net_write()                     |       |   | |       |     |                                    |       |   | |       |     |            [Client Authentication] |       |   | |       |     |>>==========>>==========>>==========>>========>>| |       |                                          |       |   |-check_scramble() |       |                                          |       |-mysql_change_db() |       |                                          |       |-my_ok() |       |                      [OK]                |       | |       |<<==========<<==========<<==========<<==========<<| |       |-cli_safe_read()                          | |                                                  | |                                                  | |                                                  | |                                                  | |-put_info() {Welcome Info}                        | |-read_and_execute() [for(;;)]                     |                                                    |-thd_is_connection_alive()  [while()]                                                    |-do_command()

参考

关于 MySQL的认证流程,包括客户端和服务器端,可以参考本地 ;详细的协议介绍可以参考 ,或者 ,或者保存的本地资料  。

MySQL 的认证授权可以采用插件,在 plugin 目录下有很多 auth 插件可供参考,具体可以参考官网的  。

转载于:https://www.cnblogs.com/qiumingcheng/p/10655142.html

你可能感兴趣的文章
【WP7】页面之间数据交互
查看>>
C++中的unique函数
查看>>
小白学数据分析----->流失分析设计
查看>>
FontAwesome 奥森图标的学习
查看>>
request response cookie session
查看>>
spring
查看>>
开源cms
查看>>
指针与引用
查看>>
NMON记录服务器各项性能数据
查看>>
Xitrum学习笔记05 - 模板引擎
查看>>
JavaBase 变量,数据类型和运算符
查看>>
Android Audio Focus的应用(requestAudioFocus)
查看>>
1django 视图与网址
查看>>
实现如下语法的功能:var a = (5).plus(3).minus(6); //2
查看>>
MFC添加背景图片
查看>>
未找到arm-linux-gcc解决办法
查看>>
统计Xcode项目代码行数
查看>>
认识 service worker
查看>>
第五次团队作业:项目展示
查看>>
WIN10更新后,应用报“不能在此路径中使用此配置节。如果在父级别上锁定了该节,便会出现这种情况”...
查看>>