本文由 GodPan 發表在 ScalaCool 團隊部落格。
MySQL對大家來說,都應該很熟悉了,從大學裡的課程到實際工作中資料的儲存查詢,很多時候都需要用到資料庫,很多人也寫過與資料庫互動的程式,在Java中你可能一開始會使用原生mysql-connector-java來進行操作,後來你會接觸到Hibernate,Mybatis等ORM框架,其實它們底層也是基於mysql-connector-java,但很多時候我們並不清楚程式是怎麼跟資料庫具體互動的,比如執行一個SQL查詢,程式是如何從MySQL中獲取資料的呢?今天就讓我們來看看最基礎的MySQL網路協議分析。
引言
閱讀本文之前你需要對網路協議需要有基本的瞭解,比如兩臺機子之間的資料是如何通訊的,硬體層可以暫時不需瞭解,但網路層和傳輸層的協議要有一定的理解,比如IP資料包,TCP/IP協議,UDP協議等相關概念,有了這些基礎,有利於你閱讀本文。
背景
在歷史悠久的時代,資料庫只作為單機儲存,也不怎麼需要與程式進行互動的時候的首,它的網路通訊並不是那麼重要,但隨著時代的發展,資料庫不再只是單純的作為一個資料的倉庫了,它需要提供與外界的互動,比如遠端連線,程式運算元據庫等,這時候一份規範的網路通訊的協議就非常重要了,比如它是如何校驗許可權,如何解析SQL語句,如何返回執行結果都需要用到相應的協議,很多時候我們並不需要接觸這些內容,因為它太底層了,我們直接使用把它們封裝好的第三方包就可以了,為什麼還要去學習它的網路協議呢?確實對於一開始學習程式設計的人來說,這有點操之過急,反而有時候會適得其反,但當你對這一方面有了一定的瞭解之後,你便會迫不及待得想去探索更深層的奧祕,去了解並學習我們平常用的第三方類庫是怎麼去實現,明白它的底層原理,甚至對一些莫名其妙的bug也不會再害怕。
MySQL連線方式
分析協議,我們首先要了解如何與資料庫連線,說到MySQL連線方式,大家突然可能有點懵,其實它一直伴隨著我們,比如我們第一次裝資料庫完成後執行的第一次登入,比如你沒有設定密碼:
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系統下的三種方式,因為這三種方式都是預設開啟的,我們不需要進行任何配置:
1.Unix Sockets:
mysql -uroot
複製程式碼
若你在本機使用這種方式連線MySQL資料庫的話,它預設會使用Unix Sockets。
2.TCP/IP:
mysql --protocol=tcp -uroot
mysql -P3306 -h127.0.0.1 -uroot
複製程式碼
連線的時候我們指定連線協議,或者指定相應的IP及埠,我們的連線方式就變成了TCP/IP方式。
3.TLS/SSL:
mysql --protocol=tcp -uroot --ssl=on
mysql -P3306 -h127.0.0.1 -uroot --ssl=on
複製程式碼
上表說過,TLS/SSL是基於TCP/IP的,所以我們只需再指定開啟ssl配置即可。
然後我們可以通過以下語句來查詢目前資料庫的連線情況:
SELECT DISTINCT connection_type from performance_schema.threads where connection_type is not null
複製程式碼
那麼我們如何選擇連線方式呢?個人總結了以下幾個原則:
- 若是你能確定程式和資料庫在同一臺機子(類Unix系統)上,推薦使用Unix Sockets,因為它效率更高;
- 若資料庫分佈在不同的機子上,且能確保連線安全或者安全性要求不是那麼高,推薦使用TCP/IP,反之使用TLS/SSL;
MySQL資料包
通訊中最重要的就是資料,那麼程式是如何和MySQL Server進行通訊,並互動資料的呢?比如如何驗證賬戶,傳送查詢語句,返回執行結果等,我先畫一個流程圖來模擬一下整個過程,幫助大家理解:
![mysql-process](https://user-gold-cdn.xitu.io/2017/11/28/160017dc3d1d8550?w=560&h=485&f=png&s=34592)
整個過程相對來說還是比較清晰的,我們對連線請求和斷開請求不需要過分關心,只需要瞭解這一點就可以了,重要的是其他幾點,那麼在這幾步中,資料是怎麼進行互動的呢?
其實主要就是兩步,Client將執行命令編碼成Server要求的格式傳輸給Server端執行,Server端將執行結果傳輸給Client端,Client端再根據相應的資料包格式解析獲得所需的資料。
1.基本資料型別
雖然網路中的資料是用位元組傳輸的,但它背後的資料來源都是有型別的資料,MySQL協議也有基本的資料型別,好比Java中的8種基本資料型別,但MySQL協議中簡單的多,它只有兩種基本資料型別,分別為Integer(整型),String(字串),下面我們就來看看這兩種型別。
Integer(整型)
首先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(字串)
String的編碼格式相對Integer來說會複雜一點,主要有以下幾種:
- FixedLengthString(定長方式):需先知道String的長度,MySQL中的一個例子就是ERR_Packet包(後續會講到)就使用了這種編碼方式,因為它的長度固定,用5個位元組儲存所有資料。
- NullTerminatedString(Null結尾方式): 字串以遇到Null作為結束標誌,相應的位元組為00。
- VariableLengthString(動態計算字串長度方式): 字串的長度取決於其他變數計算而定,比如一個字串由Integer + Value組成,我們通過計算Integer的值來獲取Value的具體的長度。
- LengthEncodedString(指定字串長度方式): 與VariableLengthString原理相似,是它的一種特殊情況,具體例子就是我上條舉的這個例子。
- RestOfPacketString(包末端字串方式):一個包末端的字串,可根據包的總長度金和當前位置得到字串的長度,實際中並不常用。
總的來說String的編碼格式種類相對比較多,不同方式之間的區別也比較大,若要深刻理解還需從實際的例子裡去學習,後續文章中我會寫幾個demo帶大家一起去探索。
2.基本資料包格式
資料包格式也主要分為兩種,一種是Server端向Client端傳送的資料包格式,另一種則是Client向Server端傳送的資料包。
Server to Client
Server向Client傳送的資料包有兩個原則:
- 每個資料包大小不能超過2^24位元組(16MB);
- 每個資料包前都需要加上資料包資訊;
每個包的基本格式:
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 to Server
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版本以後,之前版本的資料包型別這裡不再論述。
這裡主要分為兩個階段,第一個階段是資料庫賬戶認證階段,第二個階段則是執行具體命令階段,我們先來看看前者。
資料庫賬戶認證階段
這個階段就是我們平常所說的登入,主要步驟如下:
- 1.Client與Server進行連線
- 2.Server向Client傳送Handshake packet
- 3.Client與Server傳送Auth packet
- 4.Server向Client傳送OK packet或者ERR packet
這裡我們來看一看上面的Handshake packet和Auth packet,OK packet和ERR packet放在另一個階段寫。
Handshake 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
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:包資料的一個基礎結構;
- OK包(包括PREPARE_OK):Server端傳送正確處理資訊的包,包頭標識為0x00;
- Error包: Server端傳送錯誤資訊的包,包頭標識為0xFF;
- EOF包:用於Server向Client傳送結束包,包頭標識為0xFE;
- Result Set包:用於Server向Client傳送的查詢結果包;
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 包
普通的OK包(PREPARE_OK包後面會講到)會在以下幾種情況下產生,由Server傳送給相應的接收方:
- COM_PING: 連線或者測試資料庫
- COM_QUERY: 不需要查詢結果集的操作,比如INSERT, UPDATE, or ALTER TABLE
- COM_REFRESH: 資料重新整理
- COM_REGISTER_SLAVE: 註冊從伺服器
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 包
顧名思義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 Packet是用於標識某個階段資料結束的標誌包,會在一下幾種情況中產生:
- 結果集中欄位資訊結束的時候;
- 結果集中列資訊結束的時候;
- 伺服器確認停止服務的時候;
- 客戶端傳送COM_SET_OPTION and COM_DEBUG命令後,伺服器回應的時候;
- 伺服器請求使用MySQL4.1版本之前的認證方式的時候;
EOF 包的主要結構:
相對包內容的位置 | 長度(位元組) | 名稱 | 描述 |
---|---|---|---|
0 | 1 | 包頭標識 | 0xFE 代表這是一個EOF 包 |
1 | 2 | 警告數 | 上次命令引起的警告數 |
3 | 2 | 伺服器狀態 |
這裡要注意的一點,我們上面分析了Data Field的結構,發現它是用0xFE作為長度需要8個位元組編碼值得標識頭,所以我們在判斷一個包是否是EOF 包的時候,需要下面兩個條件:
- 標識頭(第一個位元組)為0xFE;
- 包的總長度小於9個位元組;
Result Set包
Result Set包產生於我們每次資料庫執行需要返回結果集的時候,Server端傳送給我們的包,比如平常的SELECT,SHOW等命令,Result Set包相對比較複雜,主要包含以下五個方面:
內容 | 含義 |
---|---|
Result Set Header | 返回資料的列數量 |
Field | 返回資料的列資訊(多個) |
EOF | 列結束 |
Row Data | 行資料(多個) |
EOF | 資料結束 |
我們逐個來分析,首先我們來看Result Set Header。
Result Set Header
Result Set Header表示返回資料的列數量以及一些額外的資訊,其主要結構為:
長度 | 含義 |
---|---|
1-9位元組 | 資料的列數量(LengthEncodedInteger編碼格式) |
1-9位元組 | 額外資訊(LengthEncodedInteger編碼格式) |
Field
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包是標識這列資訊的結束,具體結構資訊參考上面的EOF包解釋。
Row Data
Row Data含著的是我們需要獲取的資料,一個Result Set包裡面包含著多個Row Data結構(得到的資料可能多行),每個Row Data中包含著多個欄位值,它們之間沒有間隔,比如我們現在查詢到的資料為(id: 1, name: godpan) 那麼Row Data內容為(1,godpan),這兩個值是連在一起的,對應的值都用LengthEncodedString編碼。
EOF 包
等待Row Data傳送完之後,Server最後會向Client端傳送一個EOF包,標識所有的行資料已經傳送完畢。
PREPARE_OK包
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 Binary
這個包跟上面提到的Row Data包有什麼差別呢?主要有兩點:
- 用不同的方式定義NULL;
- 資料編碼不再單純的使用LengthEncodedString,而是根據資料型別的不同進行相應的編碼;
後面我會分別解釋這兩點,我們先來看看它的結構:
相對包內容的位置 | 長度(位元組) | 名稱 | 描述 |
---|---|---|---|
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個引數為一個週期,以此類推。
接著我們來看一下第二點,是如何用具體值型別來對相應的值進行編碼的,這裡主要分為三類,基本資料型別,時間型別,字串型別;
- 基本資料型別:比如TINYINT使用一個位元組編碼,FLOAT使用四個位元組,DOUBLE使用8個位元組等;
- 時間型別:使用類似LengthEncodedString的編碼方式編碼,具體可參考MySQL_PROTOCOL;
- 字串類:不屬於上面兩類的都屬於字串型別,使用普通的LengthEncodedString;
Execute包
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端後可能會得到以下幾個結果:
- OK包
- ERROR包
- Result Set包(可能多個)
我們需要根據包的不同型別來進行不同的處理。
總結
本篇文章主要講述了MySQL的連線方式,通訊過程及協議,以及傳輸包的基本格式和相關傳輸包的型別,內容相對來說,比較多也比較複雜,我也是將近三週才寫完,但總體按照我自學的思路走,不會太繞,有些點可能需要細心思考下,寫的有誤的地方也希望大家能指正,希望對大家有所幫助,後面可能會寫幾個例項和大家一起學習。