本文由 GodPan 發表在 ScalaCool 團隊博客。html
MySQL對你們來講,都應該很熟悉了,從大學裏的課程到實際工做中數據的存儲查詢,不少時候都須要用到數據庫,不少人也寫過與數據庫交互的程序,在Java中你可能一開始會使用原生mysql-connector-java來進行操做,後來你會接觸到Hibernate,Mybatis等ORM框架,其實它們底層也是基於mysql-connector-java,但不少時候咱們並不清楚程序是怎麼跟數據庫具體交互的,好比執行一個SQL查詢,程序是如何從MySQL中獲取數據的呢?今天就讓咱們來看看最基礎的MySQL網絡協議分析。java
閱讀本文以前你須要對網絡協議須要有基本的瞭解,好比兩臺機子之間的數據是如何通訊的,硬件層能夠暫時不需瞭解,但網絡層和傳輸層的協議要有必定的理解,好比IP數據包,TCP/IP協議,UDP協議等相關概念,有了這些基礎,有利於你閱讀本文。mysql
在歷史悠久的時代,數據庫只做爲單機存儲,也不怎麼須要與程序進行交互的時候的首,它的網絡通訊並非那麼重要,但隨着時代的發展,數據庫再也不只是單純的做爲一個數據的倉庫了,它須要提供與外界的交互,好比遠程鏈接,程序操做數據庫等,這時候一份規範的網絡通訊的協議就很是重要了,好比它是如何校驗權限,如何解析SQL語句,如何返回執行結果都須要用到相應的協議,不少時候咱們並不須要接觸這些內容,由於它太底層了,咱們直接使用把它們封裝好的第三方包就能夠了,爲何還要去學習它的網絡協議呢?確實對於一開始學習編程的人來講,這有點操之過急,反而有時候會拔苗助長,但當你對這一方面有了必定的瞭解以後,你便會火燒眉毛得想去探索更深層的奧祕,去了解並學習咱們日常用的第三方類庫是怎麼去實現,明白它的底層原理,甚至對一些莫名其妙的bug也不會再懼怕。算法
分析協議,咱們首先要了解如何與數據庫鏈接,說到MySQL鏈接方式,你們忽然可能有點懵,其實它一直伴隨着咱們,好比咱們第一次裝數據庫完成後執行的第一次登陸,好比你沒有設置密碼:sql
mysql -uroot
複製代碼
這是最基本的一種數據庫鏈接方式,那麼MySQL鏈接方式到底有幾種呢?到MySQL5.7爲止,總共有五種,分別是TCP/IP,TLS/SSL,Unix Sockets,Shared Memory,Named pipes,下面咱們就來看看這五種的區別:數據庫
方式 | 默認開啓 | 支持系統 | 只支持本機 | 如何開啓 | 參數配置 |
---|---|---|---|---|---|
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的鏈接方式,通訊過程及協議,以及傳輸包的基本格式和相關傳輸包的類型,內容相對來講,比較多也比較複雜,我也是將近三週才寫完,但整體按照我自學的思路走,不會太繞,有些點可能須要細心思考下,寫的有誤的地方也但願你們能指正,但願對你們有所幫助,後面可能會寫幾個實例和你們一塊兒學習。