mixer: mysql協議分析

來源:互聯網
上載者:User

綜述

要實現一個mysql proxy,首先需要做的就是理解並實現mysql通訊協議。這樣才能通過proxy架起client到server之間的橋樑。

mixer的mysql協議實現主要參考mysql官方的internal manual,並用Wireshark同時進行驗證。在實現的過程中,當然踩了很多坑,這裡記錄一下,算是對協議分析的一個總結。

需要注意的是,mixer並沒有支援所有的mysql協議,譬如備份,預存程序等,主要在於精力有限,同時也為了實現簡單。

資料類型

mysql協議只有兩種基本的資料類型,integer和string。

integer

integer包括fixed length integer和length encoded integer兩種,對於length encoded integer,用的地方比較多,這裡詳細說明一下。

對於一個integer,我們按照如下的方式將其轉成length encoded integer:

  • 如果value < 251,使用1 byte
  • 如果value >= 251 同時 value < 2 ** 16,使用fc + 2 byte
  • 如果value >= 2 ** 16 同時 value < 2 ** 24,使用fd + 3 byte
  • 如果value >= 2 ** 24 同時 value < 2 ** 64,使用fe + 8 byte

相應的,對於一個length encoded integer,我們可以通過判斷第一個byte的值來轉成相應的integer。

string

string包括:

  • fixed length string,固定長度string
  • null terminated string,以null結尾的string
  • variable length string,通過另一個值決定長度的string
  • length encoded string,通過起始length encoded integer決定長度的string
  • rest of packet string,從當前位置到包結尾的string
Packet

在mysql中,如果client或server要發送資料,它需要將資料按照(2 ** 24 - 1)拆分成packet,給每一個packet添加header,然後再以此發送。

對於一個packet,格式如下:

3              payload length1              sequence idstring[len]    payload

前面3個位元組表明的是該packet的長度,每個packet最大不超過16MB。第4個位元組表明的是該packet的序號,從0開始,對於多個packet依次遞增,等到下一個新的命令發送資料的時候才重設為0。前面4個位元組組成了一個packet的header,後面就是該packet實際的資料。

因為一個packet最大能發送的資料位元16MB,所以如果需要發送大於16MB的資料,就需要拆分成多個packet進行發送。

通常,server會回給client三種類型的packet

  • OK Packet,操作成功
  • Err Packet,操作失敗
  • EOF Packet,end of file
登陸互動

要實現proxy,首先需要解決的就是登陸問題,包括proxy類比server處理client的登陸,proxy類比client登陸server。

為了簡單,mixer只支援username + password的方式進行登陸,這應該也是最通用的登陸方式。同時不支援ssl以及compression。

一個完整的登陸流程如下:

  • client首先connect到server
  • server發送initial handshake packet,包括支援的capability,一個用於加密的隨機salt等
  • client返回handshake結果,包括自己支援的capability,以及用salt加密的密碼
  • server驗證,如果成功,返回ok packet,否則返回err packet並關閉串連

這裡,不得不說實現登陸協議的時候踩過的一個很大的坑,因為我使用的是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.

可想而知,這個坑有多坑爹。至少我開始是栽在上面了。加密老是不對。

Command

搞定了登陸,剩下的就是mysql的命令支援,mixer只實現了基本的命令。主要集中在text protocol以及prepared statment裡面。

COM_PING

最基本的ping實現,用來檢查mysql是否存活。

COM_INIT_DB

雖然叫init db,其實壓根乾的事情就跟use db一樣,用來切換使用db的。

COM_QUERY

可以算是最重要的一個命令,我們在命令列使用的多數mysql語句,都是通過該命令發送的。

在COM_QUERY中,mixer主要支援了select,update,insert,delete,replace等基本的動作陳述式,同時支援begin,commit,rollback事物操作,還支援set names和set autocommit。

COM_QUERY有4中返回packet

  • OK Packet
  • Err Packet
  • Local In File(不支援)
  • Text Resultset

這裡重點說明一下text resultset,因為它包含的就是我們最常用的select的結果集。

一個text resultset,包括如下幾個包:

  • 一個以length encoded integer編碼的column-count packet
  • column-count個column定義packet
  • eof packet
  • 一個或者多個row packet,每個row packet有column-count個資料
  • eof packet或者err packet

對於一個row packet的裡面的資料,我們通過如下方式擷取:

  • 如果值為NULL,那麼就是0xfb
  • 否則,任何值都是用length encoded string表示
COM_STMT_*

COM_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,歡迎反饋。

相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.