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指定的行資料釋放掉.