mysql long类型_怒肝两个月MySQL源码,我总结出这篇2W字的MySQL协议详解(超硬核干货)!!...
點擊上方藍色“冰河技術”,關注并選擇“設為星標”
持之以恒,貴在堅持,每天進步一點點!
作者個人研發的在高并發場景下,提供的簡單、穩定、可擴展的延遲消息隊列框架,具有精準的定時任務和延遲隊列處理功能。自開源半年多以來,已成功為十幾家中小型企業提供了精準定時調度方案,經受住了生產環境的考驗。為使更多童鞋受益,現給出開源框架地址:
https://github.com/sunshinelyz/mykit-delay
PS: 歡迎各位Star源碼,也可以pr你牛逼哄哄的代碼。? ? ??
寫在前面
最近,在開發一個分庫分表中間件,由于功能需求,需要分析MySQL協議,發現網上對于MySQL協議分析的文章大部分都過時了,原因是分析的MySQL版本太低了。怎么辦呢?于是乎,我便硬著頭皮開始啃MySQL源碼,經過兩個多月的整理,終于總結出這篇MySQL協議。
注:部分來自于互聯網,感謝數據庫大牛前輩們的默默付出!!
交互過程
MySQL客戶端與服務器的交互主要分為兩個階段:握手認證階段和命令執行階段。
握手認證階段
握手認證階段為客戶端與服務器建立連接后進行,交互過程如下:
- 服務器 -> 客戶端:握手初始化消息
- 客戶端 -> 服務器:登陸認證消息
- 服務器 -> 客戶端:認證結果消息
命令執行階段
客戶端認證成功后,會進入命令執行階段,交互過程如下:
- 客戶端 -> 服務器:執行命令消息
- 服務器 -> 客戶端:命令執行結果
MySQL客戶端與服務器的完整交互過程如下:
基本類型
整型值
MySQL報文中整型值分別有1、2、3、4、8字節長度,使用小字節序傳輸。
字符串(以NULL結尾)(Null-Terminated String)
字符串長度不固定,當遇到'NULL'(0x00)字符時結束。
二進制數據(長度編碼)(Length Coded Binary)
數據長度不固定,長度值由數據前的1-9個字節決定,其中長度值所占的字節數不定,字節數由第1個字節決定,如下表:
| 0-250 | 0 | 第一個字節值即為數據的真實長度 |
| 251 | 0 | 空數據,數據的真實長度為零 |
| 252 | 2 | 后續額外2個字節標識了數據的真實長度 |
| 253 | 3 | 后續額外3個字節標識了數據的真實長度 |
| 254 | 8 | 后續額外8個字節標識了數據的真實長度 |
字符串(長度編碼)(Length Coded String)
字符串長度不固定,無'NULL'(0x00)結束符,編碼方式與上面的 Length Coded Binary 相同。
報文結構
報文分為消息頭和消息體兩部分,其中消息頭占用固定的4個字節,消息體長度由消息頭中的長度字段決定,報文結構如下:
消息頭
報文長度
用于標記當前請求消息的實際數據長度值,以字節為單位,占用3個字節,最大值為 0xFFFFFF,即接近 16 MB 大小(比16MB少1個字節)。
序號
在一次完整的請求/響應交互過程中,用于保證消息順序的正確,每次客戶端發起請求時,序號值都會從0開始計算。
消息體
消息體用于存放請求的內容及響應的數據,長度由消息頭中的長度值決定。
報文類型
登陸認證交互報文
握手初始化報文(服務器 -> 客戶端)
服務協議版本號:該值由 PROTOCOL_VERSION 宏定義決定(參考MySQL源代碼/include/mysql_version.h頭文件定義)
服務版本信息:該值為字符串,由 MYSQL_SERVER_VERSION 宏定義決定(參考MySQL源代碼/include/mysql_version.h頭文件定義)
服務器線程ID:服務器為當前連接所創建的線程ID。
挑戰隨機數:MySQL數據庫用戶認證采用的是挑戰/應答的方式,服務器生成該挑戰數并發送給客戶端,由客戶端進行處理并返回相應結果,然后服務器檢查是否與預期的結果相同,從而完成用戶認證的過程。
服務器權能標志:用于與客戶端協商通訊方式,各標志位含義如下(參考MySQL源代碼/include/mysql_com.h中的宏定義):
| CLIENT_LONG_PASSWORD | 0x0001 | new more secure passwords |
| CLIENT_FOUND_ROWS | 0x0002 | Found instead of affected rows |
| CLIENT_LONG_FLAG | 0x0004 | Get all column flags |
| CLIENT_CONNECT_WITH_DB | 0x0008 | One can specify db on connect |
| CLIENT_NO_SCHEMA | 0x0010 | Do not allow database.table.column |
| CLIENT_COMPRESS | 0x0020 | Can use compression protocol |
| CLIENT_ODBC | 0x0040 | Odbc client |
| CLIENT_LOCAL_FILES | 0x0080 | Can use LOAD DATA LOCAL |
| CLIENT_IGNORE_SPACE | 0x0100 | Ignore spaces before '(' |
| CLIENT_PROTOCOL_41 | 0x0200 | New 4.1 protocol |
| CLIENT_INTERACTIVE | 0x0400 | This is an interactive client |
| CLIENT_SSL | 0x0800 | Switch to SSL after handshake |
| CLIENT_IGNORE_SIGPIPE | 0x1000 | IGNORE sigpipes |
| CLIENT_TRANSACTIONS | 0x2000 | Client knows about transactions |
| CLIENT_RESERVED | 0x4000 | Old flag for 4.1 protocol |
| CLIENT_SECURE_CONNECTION | 0x8000 | New 4.1 authentication |
| CLIENT_MULTI_STATEMENTS | 0x0001 0000 | Enable/disable multi-stmt support |
| CLIENT_MULTI_RESULTS | 0x0002 0000 | Enable/disable multi-results |
字符編碼:標識服務器所使用的字符集。
服務器狀態:狀態值定義如下(參考MySQL源代碼/include/mysql_com.h中的宏定義):
| SERVER_STATUS_IN_TRANS | 0x0001 |
| SERVER_STATUS_AUTOCOMMIT | 0x0002 |
| SERVER_STATUS_CURSOR_EXISTS | 0x0040 |
| SERVER_STATUS_LAST_ROW_SENT | 0x0080 |
| SERVER_STATUS_DB_DROPPED | 0x0100 |
| SERVER_STATUS_NO_BACKSLASH_ESCAPES | 0x0200 |
| SERVER_STATUS_METADATA_CHANGED | 0x0400 |
登陸認證報文(客戶端 -> 服務器)
MySQL 4.0 及之前的版本
MySQL 4.1 及之后的版本
客戶端權能標志:用于與客戶端協商通訊方式,標志位含義與握手初始化報文中的相同。客戶端收到服務器發來的初始化報文后,會對服務器發送的權能標志進行修改,保留自身所支持的功能,然后將權能標返回給服務器,從而保證服務器與客戶端通訊的兼容性。
最大消息長度:客戶端發送請求報文時所支持的最大消息長度值。
字符編碼:標識通訊過程中使用的字符編碼,與服務器在認證初始化報文中發送的相同。
用戶名:客戶端登陸用戶的用戶名稱。
挑戰認證數據:客戶端用戶密碼使用服務器發送的挑戰隨機數進行加密后,生成挑戰認證數據,然后返回給服務器,用于對用戶身份的認證。
數據庫名稱:當客戶端的權能標志位 CLIENT_CONNECT_WITH_DB 被置位時,該字段必須出現。
客戶端命令請求報文(客戶端 -> 服務器)
命令:用于標識當前請求消息的類型,例如切換數據庫(0x02)、查詢命令(0x03)等。命令值的取值范圍及說明如下表(參考MySQL源代碼/include/mysql_com.h頭文件中的定義):
| 0x00 | COM_SLEEP | (內部線程狀態) | (無) |
| 0x01 | COM_QUIT | 關閉連接 | mysql_close |
| 0x02 | COM_INIT_DB | 切換數據庫 | mysql_select_db |
| 0x03 | COM_QUERY | SQL查詢請求 | mysql_real_query |
| 0x04 | COM_FIELD_LIST | 獲取數據表字段信息 | mysql_list_fields |
| 0x05 | COM_CREATE_DB | 創建數據庫 | mysql_create_db |
| 0x06 | COM_DROP_DB | 刪除數據庫 | mysql_drop_db |
| 0x07 | COM_REFRESH | 清除緩存 | mysql_refresh |
| 0x08 | COM_SHUTDOWN | 停止服務器 | mysql_shutdown |
| 0x09 | COM_STATISTICS | 獲取服務器統計信息 | mysql_stat |
| 0x0A | COM_PROCESS_INFO | 獲取當前連接的列表 | mysql_list_processes |
| 0x0B | COM_CONNECT | (內部線程狀態) | (無) |
| 0x0C | COM_PROCESS_KILL | 中斷某個連接 | mysql_kill |
| 0x0D | COM_DEBUG | 保存服務器調試信息 | mysql_dump_debug_info |
| 0x0E | COM_PING | 測試連通性 | mysql_ping |
| 0x0F | COM_TIME | (內部線程狀態) | (無) |
| 0x10 | COM_DELAYED_INSERT | (內部線程狀態) | (無) |
| 0x11 | COM_CHANGE_USER | 重新登陸(不斷連接) | mysql_change_user |
| 0x12 | COM_BINLOG_DUMP | 獲取二進制日志信息 | (無) |
| 0x13 | COM_TABLE_DUMP | 獲取數據表結構信息 | (無) |
| 0x14 | COM_CONNECT_OUT | (內部線程狀態) | (無) |
| 0x15 | COM_REGISTER_SLAVE | 從服務器向主服務器進行注冊 | (無) |
| 0x16 | COM_STMT_PREPARE | 預處理SQL語句 | mysql_stmt_prepare |
| 0x17 | COM_STMT_EXECUTE | 執行預處理語句 | mysql_stmt_execute |
| 0x18 | COM_STMT_SEND_LONG_DATA | 發送BLOB類型的數據 | mysql_stmt_send_long_data |
| 0x19 | COM_STMT_CLOSE | 銷毀預處理語句 | mysql_stmt_close |
| 0x1A | COM_STMT_RESET | 清除預處理語句參數緩存 | mysql_stmt_reset |
| 0x1B | COM_SET_OPTION | 設置語句選項 | mysql_set_server_option |
| 0x1C | COM_STMT_FETCH | 獲取預處理語句的執行結果 | mysql_stmt_fetch |
參數:內容是用戶在MySQL客戶端輸入的命令(不包括每行命令結尾的";"分號)。另外這個字段的字符串不是以NULL字符結尾,而是通過消息頭中的長度值計算而來。
例如:當我們在MySQL客戶端中執行use hutaow;命令時(切換到hutaow數據庫),發送的請求報文數據會是下面的樣子:
0x02?0x68?0x75?0x74?0x61?0x6f?0x77其中,0x02為請求類型值COM_INIT_DB,后面的0x68 0x75 0x74 0x61 0x6f 0x77為ASCII字符hutaow。
COM_QUIT 消息報文
功能:關閉當前連接(客戶端退出),無參數。
COM_INIT_DB 消息報文
功能:切換數據庫,對應的SQL語句為USE。
| n | 數據庫名稱(字符串到達消息尾部時結束,無結束符) |
COM_QUERY 消息報文
功能:最常見的請求消息類型,當用戶執行SQL語句時發送該消息。
| n | SQL語句(字符串到達消息尾部時結束,無結束符) |
COM_FIELD_LIST 消息報文
功能:查詢某表的字段(列)信息,等同于SQL語句SHOW [FULL] FIELDS FROM ...。
| n | 表格名稱(Null-Terminated String) |
| n | 字段(列)名稱或通配符(可選) |
COM_CREATE_DB 消息報文
功能:創建數據庫,該消息已過時,而被SQL語句CREATE DATABASE代替。
| n | 數據庫名稱(字符串到達消息尾部時結束,無結束符) |
COM_DROP_DB 消息報文
功能:刪除數據庫,該消息已過時,而被SQL語句DROP DATABASE代替。
| n | 數據庫名稱(字符串到達消息尾部時結束,無結束符) |
COM_REFRESH 消息報文
功能:清除緩存,等同于SQL語句FLUSH,或是執行mysqladmin flush-foo命令時發送該消息。
| 1 | 清除緩存選項(位圖方式存儲,各標志位含義如下) |
| 0x01: REFRESH_GRANT | |
| 0x02: REFRESH_LOG | |
| 0x04: REFRESH_TABLES | |
| 0x08: REFRESH_HOSTS | |
| 0x10: REFRESH_STATUS | |
| 0x20: REFRESH_THREADS | |
| 0x40: REFRESH_SLAVE | |
| 0x80: REFRESH_MASTER |
COM_SHUTDOWN 消息報文
功能:停止MySQL服務。執行mysqladmin shutdown命令時發送該消息。
| 1 | 停止服務選項 |
| 0x00: SHUTDOWN_DEFAULT | |
| 0x01: SHUTDOWN_WAIT_CONNECTIONS | |
| 0x02: SHUTDOWN_WAIT_TRANSACTIONS | |
| 0x08: SHUTDOWN_WAIT_UPDATES | |
| 0x10: SHUTDOWN_WAIT_ALL_BUFFERS | |
| 0x11: SHUTDOWN_WAIT_CRITICAL_BUFFERS | |
| 0xFE: KILL_QUERY | |
| 0xFF: KILL_CONNECTION |
COM_STATISTICS 消息報文
功能:查看MySQL服務的統計信息(例如運行時間、每秒查詢次數等)。執行mysqladmin status命令時發送該消息,無參數。
COM_PROCESS_INFO 消息報文
功能:獲取當前活動的線程(連接)列表。等同于SQL語句SHOW PROCESSLIST,或是執行mysqladmin processlist命令時發送該消息,無參數。
COM_PROCESS_KILL 消息報文
功能:要求服務器中斷某個連接。等同于SQL語句KILL。
| 4 | 連接ID號(小字節序) |
COM_DEBUG 消息報文
功能:要求服務器將調試信息保存下來,保存的信息多少依賴于編譯選項設置(debug=no|yes|full)。執行mysqladmin debug命令時發送該消息,無參數。
COM_PING 消息報文
功能:該消息用來測試連通性,同時會將服務器的無效連接(超時)計數器清零。執行mysqladmin ping命令時發送該消息,無參數。
COM_CHANGE_USER 消息報文
功能:在不斷連接的情況下重新登陸,該操作會銷毀MySQL服務器端的會話上下文(包括臨時表、會話變量等)。有些連接池用這種方法實現清除會話上下文。
| n | 用戶名(字符串以NULL結尾) |
| n | 密碼(挑戰數) |
| MySQL 3.23 版本:Null-Terminated String(長度9字節) | |
| MySQL 4.1 版本:Length Coded String(長度1+21字節) | |
| n | 數據庫名稱(Null-Terminated String) |
| 2 | 字符編碼 |
COM_BINLOG_DUMP 消息報文
功能:該消息是備份連接時由從服務器向主服務器發送的最后一個請求,主服務器收到后,會響應一系列的報文,每個報文都包含一個二進制日志事件。如果主服務器出現故障時,會發送一個EOF報文。
| 4 | 二進制日志數據的起始位置(小字節序) |
| 4 | 二進制日志數據標志位(目前未使用,永遠為0x00) |
| 4 | 從服務器的服務器ID值(小字節序) |
| n | 二進制日志的文件名稱(可選,默認值為主服務器上第一個有效的文件名) |
COM_TABLE_DUMP 消息報文
功能:將數據表從主服務器復制到從服務器中,執行SQL語句LOAD TABLE ... FROM MASTER時發送該消息。目前該消息已過時,不再使用。
| n | 數據庫名稱(Length Coded String) |
| n | 數據表名稱(Length Coded String) |
COM_REGISTER_SLAVE 消息報文
功能:在從服務器report_host變量設置的情況下,當備份連接時向主服務器發送的注冊消息。
| 4 | 從服務器ID值(小字節序) |
| n | 主服務器IP地址(Length Coded String) |
| n | 主服務器用戶名(Length Coded String) |
| n | 主服務器密碼(Length Coded String) |
| 2 | 主服務器端口號 |
| 4 | 安全備份級別(由MySQL服務器rpl_recovery_rank變量設置,暫時未使用) |
| 4 | 主服務器ID值(值恒為0x00) |
COM_PREPARE 消息報文
功能:預處理SQL語句,使用帶有"?"占位符的SQL語句時發送該消息。
| n | 帶有"?"占位符的SQL語句(字符串到達消息尾部時結束,無結束符) |
COM_EXECUTE 消息報文
功能:執行預處理語句。
| 4 | 預處理語句的ID值 |
| 1 | 標志位 |
| 0x00: CURSOR_TYPE_NO_CURSOR | |
| 0x01: CURSOR_TYPE_READ_ONLY | |
| 0x02: CURSOR_TYPE_FOR_UPDATE | |
| 0x04: CURSOR_TYPE_SCROLLABLE | |
| 4 | 保留(值恒為0x01) |
| 如果參數數量大于0 | |
| n | 空位圖(Null-Bitmap,長度 = (參數數量 + 7) / 8 字節) |
| 1 | 參數分隔標志 |
| 如果參數分隔標志值為1 | |
| n | 每個參數的類型值(長度 = 參數數量 * 2 字節) |
| n | 每個參數的值 |
COM_LONG_DATA 消息報文
該消息報文有兩種形式,一種用于發送二進制數據,另一種用于發送文本數據。
功能:用于發送二進制(BLOB)類型的數據(調用mysql_stmt_send_long_data函數)。
| 4 | 預處理語句的ID值(小字節序) |
| 2 | 參數序號(小字節序) |
| n | 數據負載(數據到達消息尾部時結束,無結束符) |
功能:用于發送超長字符串類型的數據(調用mysql_send_long_data函數)
| 4 | 預處理語句的ID值(小字節序) |
| 2 | 參數序號(小字節序) |
| 2 | 數據類型(未使用) |
| n | 數據負載(數據到達消息尾部時結束,無結束符) |
COM_CLOSE_STMT 消息報文
功能:銷毀預處理語句。
| 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個字節的值,來區分響應報文的類型。
| 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 |
數值精度:該字段對DECIMAL和NUMERIC類型的數值字段有效,用于標識數值的精度(小數點位置)。
默認值:該字段用在數據表定義中,普通的查詢結果中不會出現。
附: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_readif?(net->buff[net->where_b?+?3]?!=?(uchar)?net->pkt_nr)發送包
sql/net_serv.c:my_net_writeint3store(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_commandnet_new_transaction(net);
sql/client.c:cli_advanced_commandnet_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=4for(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);
????}
}
網絡層優化
從ppt里可以看到,一個resultset packet由多個包組成,如果每次讀寫包都導致系統調用那肯定是不合理,常規優化方法:寫大包加預讀
NET->BUFF
每個包發送到網絡或者從網絡讀包都會先把數據包保存在net->buff里,待到net->buff滿了或者一次命令結束才會通過socket發出給對端.net->buff有個初始大小(net->max_packet),會隨讀取數據的增多而擴展.
VIO->READ_BUFFER
每次從網絡讀包,并不是按包的大小讀取,而是會盡量讀取2048個字節,這樣一個resultset包的讀取不會再引起多次的系統調用了.header packet讀取完畢后, 接下來的field,eof, row ?apcket讀取僅僅需要從vio-read_buffer拷貝指定字節的數據即可.
MYSQL API說明
api和MySQL客戶端都會使用sql/client.c這個文件,解包的過程都是使用sql/client.c:cli_read_query_result.
mysql_store_result來解析row packet,并把數據存儲到res->data里,此時所有數據都存內存里了.
mysql_fetch_row僅僅是使用內部的游標,遍歷result->data里的數據
if?(!res->data_cursor){
????DBUG_PRINT("info",("end?of?data"));
????DBUG_RETURN(res->current_row=(MYSQL_ROW)?NULL);
}
tmp?=?res->data_cursor->data;
res->data_cursor?=?res->data_cursor->next;
DBUG_RETURN(res->current_row=tmp);
mysql_free_result是把result->data指定的行數據釋放掉.
大部分參考:http://hutaow.com/blog/2013/11/06/mysql-protocol-analysis/
重磅福利
微信搜一搜【冰河技術】微信公眾號,關注這個有深度的程序員,每天閱讀超硬核技術干貨,公眾號內回復【PDF】有我準備的一線大廠面試資料和我原創的超硬核PDF技術文檔,以及我為大家精心準備的多套簡歷模板(不斷更新中),希望大家都能找到心儀的工作,學習是一條時而郁郁寡歡,時而開懷大笑的路,加油。如果你通過努力成功進入到了心儀的公司,一定不要懈怠放松,職場成長和新技術學習一樣,不進則退。如果有幸我們江湖再見!
另外,我開源的各個PDF,后續我都會持續更新和維護,感謝大家長期以來對冰河的支持!!
寫在最后
如果你覺得冰河寫的還不錯,請微信搜索并關注「?冰河技術?」微信公眾號,跟冰河學習高并發、分布式、微服務、大數據、互聯網和云原生技術,「?冰河技術?」微信公眾號更新了大量技術專題,每一篇技術文章干貨滿滿!不少讀者已經通過閱讀「?冰河技術?」微信公眾號文章,吊打面試官,成功跳槽到大廠;也有不少讀者實現了技術上的飛躍,成為公司的技術骨干!如果你也想像他們一樣提升自己的能力,實現技術能力的飛躍,進大廠,升職加薪,那就關注「?冰河技術?」微信公眾號吧,每天更新超硬核技術干貨,讓你對如何提升技術能力不再迷茫!
留言區
總結
以上是生活随笔為你收集整理的mysql long类型_怒肝两个月MySQL源码,我总结出这篇2W字的MySQL协议详解(超硬核干货)!!...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python3 字节码详解
- 下一篇: 十字链表c语言实验报告,矩阵加法(基于十