相應的,對於一個length encoded integer,我們可以通過判斷第一個byte的值來轉成相應的integer。
string包括:
fixed length string,固定長度string在mysql中,如果client或server要發送數據,它需要將數據按照(2 ** 24 - 1)拆分成packet,給每一個packet添加header,然後再以此發送。
對於一個packet,格式如下:
3 payload length 1 sequence id string[len] payload
前面3個字節表明的是該packet的長度,每個packet最大不超過16MB。第4個字節表明的是該packet的序列號,從0開始,對於多個packet依次遞增,等到下一個新的命令發送數據的時候才重置為0。前面4個字節組成了一個packet的header,後面就是該packet實際的數據。
因為一個packet最大能發送的數據位16MB,所以如果需要發送大於16MB的數據,就需要拆分成多個packet進行發送。
通常,server會回給client三種類型的packet
OK Packet,操作成功要實現proxy,首先需要解決的就是登陸問題,包括proxy模擬server處理client的登陸,proxy模擬client登陸server。
為了簡單,mixer只支持username + password的方式進行登陸,這應該也是最通用的登陸方式。同時不支持ssl以及compression。
一個完整的登陸流程如下:
client首先connect到server這裡,不得不說實現登陸協議的時候踩過的一個很大的坑,因為我使用的是HandshakeV10協議,在文檔裡面,協議有這樣的規定:
if capabilities & CLIENT_SECURE_CONNECTION { string[$len] auth-plugin-data-part-2 ($len=MAX(13, length of auth-plugin-data - 8)) }
如果根據文檔的說明,算出來auth-plugin-data-part-2的長度是13,因為auth-plugin-data的長度是20。但是,實際情況是,auth-plugin-data-part-2的長度應該為12,第13位一直為0。只有這樣,我們才能根據salt算出正確的加密密碼。這一點,在mysql-proxy官方的文檔,以及多個msyql client driver上面,Wireshark的分析中都是如此,在go-sql-driver中,作者都直接寫了如下的注釋:
// second part of the password cipher [12? bytes] // The documentation is ambiguous about the length. // The official Python library uses the fixed length 12 // which is not documented but seems to work.
可想而知,這個坑有多坑爹。至少我開始是栽在上面了。加密老是不對。
搞定了登陸,剩下的就是mysql的命令支持,mixer只實現了基本的命令。主要集中在text protocol以及prepared statment裡面。
最基本的ping實現,用來檢查mysql是否存活。
雖然叫init db,其實壓根干的事情就跟use db一樣,用來切換使用db的。
可以算是最重要的一個命令,我們在命令行使用的多數mysql語句,都是通過該命令發送的。
在COM_QUERY中,mixer主要支持了select,update,insert,delete,replace等基本的操作語句,同時支持begin,commit,rollback事物操作,還支持set names和set autocommit。
COM_QUERY有4中返回packet
OK Packet這裡重點說明一下text resultset,因為它包含的就是我們最常用的select的結果集。
一個text resultset,包括如下幾個包:
一個以length encoded integer編碼的column-count packet對於一個row packet的裡面的數據,我們通過如下方式獲取:
如果值為NULL,那麼就是0xfbCOM_STMT_族協議就是通常的prepared statement,當我在atlas群裡面說支持prepared statement的時候,很多人以為我支持的是在COM_QUERY中使用的prepare,execute和deallocate prepare這組語句。其實這兩個還是很有區別的。
為什麼我不現在不想支持COM_QUERY的prepare,主要在於這種prepare需要進行變量設置,mixer在後端跟server是維護的一個連接池,所以對於client設置的變量,proxy維護起來特別麻煩,並且每次跟server使用新的連接的時候,還需要將所有的變量重設,這增大了復雜度。所以我不支持變量的設置,這點看cobar也是如此。既然不支持變量,所以COM_QUERY的prepare我也不會支持了。
COM_STMT_*這組命令,主要用在各個語言的client driver中,所以我覺得只支持這種的prepare就夠了。
對於COM_STMT_EXECUTE的返回結果,因為prepare的語句可能是select,所以會返回binary resultset,binary resultset組成跟前面text resultset差不多,唯一需要注意的就是row packet采用的是binary row packet。
對於每一個binary row packet,第一個byte為0,後面緊跟著一個null bitmap,然後才是實際的數據。
在binary row packet中,使用null bitmap來表明該行某一列的數據為NULL。null bitmap長度通過 (column-count + 7 + 2) / 8計算得到,而對於每列數據,如果為NULL,那麼它在null bitmap中的位置通過如下方式計算:
NULL-bitmap-byte = ((field-pos + offset) / 8) NULL-bitmap-bit = ((field-pos + offset) % 8)
offset在binary resultset中為2,field-pos為該列的位置。
對於實際非NULL數據,則是根據每列定義的數據類型來獲取,譬如如果type為MYSQL_TYPE_LONGLONG,那麼該數據值的長度就是8字節,如果type為MYSQL_TYPE_STRING,那麼該數據值就是一個length encoded string。
我通過Wireshark分析了一些mysql protocol,主要在這裡,這裡不得不強烈推薦wireshark,它讓我在學習mysql protocol過程中事半功倍。
mixer的代碼在這裡https://github.com/siddontang/mixer,歡迎反饋。