1. 程式人生 > >MySQL之——協議分析(下篇)

MySQL之——協議分析(下篇)

MySQL協議分析

議程
協議頭
協議型別
網路協議相關函式
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_read

     if (net->buff[net->where_b + 3] != (uchar) net->pkt_nr)

傳送包

sql/net_serv.c:my_net_write

   int3store(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_command
   net_new_transaction(net);
sql/client.c:cli_advanced_command

   net_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=4
for(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指定的行資料釋放掉.