from : http://hi.baidu.com/yancncen/blog/item/b6e0ad38031b06cbd4622547.html

聯系作者,請加qq:1-1-3-8-5-7-1-5-4

thrift里的c++ lib支持很多種協議,不過鑒于有些協議在別的語言的thrift lib并沒有得到支持,如php只支持binaryprotocol協議。所以,建議交互都走binaryprotocol協議,在此也只分析binaryprotocol的協議。

廢話不說了,直入正題

binaryprotocol協議分析:

使用工具:
strace、tcpdump、vi、thrift及其源碼

(1)0-4個字節:
協議版本|消息類型 (對于TBinaryProtocol.cpp而言協議版本為0x80010000和消息類型進行或運算)
其中消息類型包括:call == 1, reply == 2, excetpion == 3

(2)4-8字節:
請求的函數名稱的長度,這里假設為functionNameLen


(3)8-functionNameLen字節{functionNameLen為請求函數名的長度}:
請求的函數名稱名稱

(4)functionNameLen-functionNameLen+4個字節:(4個字節)
請求的序列號
(貌似生成的代碼里面,不管是哪個請求序列號都為0)


如果function沒有參數:
最后一個字節為0

參數具有以下形式:{類型}{序號}{值}, 詳細請看下面的解釋

如果function有參數:
(5)functionNameLen+4-functionNameLen+5個字節:(1個字節)
參數1的類型, 包括以下類型:

參數類型表:
T_STOP       = 0,
T_VOID       = 1,
T_BOOL       = 2,
T_BYTE       = 3,
T_I08        = 3,
T_I16        = 6,
T_I32        = 8,
T_U64        = 9,
T_I64        = 10,
T_DOUBLE     = 4,
T_STRING     = 11,
T_UTF7       = 11,
T_STRUCT     = 12,
T_MAP        = 13,
T_SET        = 14,
T_LIST       = 15,
T_UTF8       = 16,
T_UTF16      = 17

(6)functionNameLen+5-functionNameLen+7個字節:(2個字節)
參數序號,取決于你定義的idl文件中參數所定義的序號


(7)接下來的n個字節,取決于參數的類型:

a)對于以下具有固定長度的簡單數據類型的參數:
T_STOP       = 0, n==1
T_VOID       = 1,   n==1
T_BOOL       = 2, n==1
T_BYTE       = 3, n==1
T_I08        = 3, n==1
T_I16        = 6, n==2
T_I32        = 8, n==4
T_U64        = 9, n==8 ? (雖然給出了定義,但是TBinaryProtocol.cpp并沒有實現,
     所以,如果要使用該數據類型的必須注意, 應該使用string類型來代替它
     unsigned long long 類型就需要特別注意了)
T_I64        = 10, n==8
T_DOUBLE     = 4, n==8   (double在目標機器上必須是8個字節的,符合IEC-559標準的浮點數,貌似對我們沒有影響)
這n個字節就是參數的值。


以下的數據類型被我歸為復合數據類型:

b)對于String,這n個字節包含以下內容:
前面4個字節:字符串的長度stringLen
接下來的stringLen個字節:字符串的內容

c)對于struct,這n個字節包含以下內容:
   假設這個結構體包含m個字段:(為了便于說明問題,下面所說的字節偏移是相對于struct內部結構而言的)
   0-1字節:字段的數據類型
   1-3字節:字段序號,取決于你定義的idl文件中參數所定義的序號

   接下來的k個字節:goto (7)
   以此類推,直至m個字段
   其實,struct的字段和函數參數具有一樣的編碼方式

d)對于set,這n個字節包含以下內容:(為了便于說明問題,下面所說的字節偏移是相對于set內部結構而言的)
   0-1字節:set里面的元素的數據類型
   1-5字節:元素個數
   假設元素的數據類型的長度為k個字節,那么接下來每k個字節作為一個元素值,至于元素值的分析,請goto(7)
   注意,這里和函數參數/struct的區別在于,這里不存在元素的序號值

e)對于list,這n個字節包含以下內容:
和set類似,這里就不重復累贅了。
值得注意的一點是,thrift白皮書提到了“list<type> An ordered list of elements.”,實際上,list在映射到stl的list時并不是經過排序的list。

f)對于map,這n個字節包含以下內容:(為了便于說明問題,下面所說的字節偏移是相對于map內部結構而言的)
0-1字節:key的數據類型。注意,可能是復合數據類型
1-2字節:value的數據類型。
假設key的數據類型的長度為k個字節,value的數據類型的長度為v個字節
那么接下來每k+v個字節作為一個key-value值,
至于key的值的分析和value的值的分析,請goto(7)

總結:
(1)關于定義idl的一些總結,盡量避免定義過于復雜的數據結構。

從上面的協議分析來看,復合數據類型的存在著一個遞歸包含的關系。
不過thrift在生成封/解包的代碼里,并沒有出現遞歸調用來封/解包,而是采用了循環嵌套循環的方式來生成代碼,
這種做法避免了頻繁遞歸調用封/解包函數,可提高封/解包的效率。同時帶來的問題就是生成的代碼量的急劇膨脹。

雖然沒有遞歸調用來封/解包, 如果定義太過于復雜的數據結構也會隨之產生多重循環,看下面的例子
假設定義以下一個數據結構:
map< string, list< set<string> > >,thrift將會產生類似于以下的循環來進行封/解包:
foreach (key in map)
{
foreach ( set in map[key] )
{
    foreach (string in set )
      encode()/decode();  
}
}

假設map,list,set的元素各有100個,這將是一個嚴重影響性能的地方,應該避免。

(2)建議使用了unsigned long long的字段使用string類型,而不是u64類型,因為目前的thrift不支持。

(3)在網絡IO層一個潛在的瓶頸
由于thrift的binaryprotocol協議的包頭沒有任何的字節描述了整個網絡包的長度的信息。
所以thrift的binaryprotocol協議在解包的時候是每次都只能采用從socket讀取一個變量的類型接著讀取變量的值出來這樣的解釋方式。
這種解包的方式可能引起的潛在問題:
當請求的client數量非常多,交互的數據量也非常多(這里可能是交互了很多字段,或者使用了太多復合數據類型)時,tcp/ip協議棧的緩沖區可能會被塞滿了
還沒有被處理的數據,就會嚴重影響服務質量。

至于為何不提供某些字節來標識整個數據包的長度,是因為thrift的binaryprotocol協議需要支持復雜數據類型,像set,list,map,而這些復合數據類型的大小是難以確定的。
為了支持標識整個數據包長度,封包前需要知道set,list,map的總體大小,那么就需要遍歷set,list,map的大小,這是相當不劃算,會增加運算邏輯,而且還會導致協議變得很復雜。


(4)如何做到兼容舊接口
當我們的server更新接口的同時,還需要保證舊client能夠和新server交互,那么在定義IDL時就需要特別注意。
假設,我們定義以下一個這個一個結構體來交互用戶信息。
struct user
{
1:username string,
2:password string,
3:sex i16,
}

當我們需求變更時,假設以下兩種情況:
a)需要新增字段, age來表示年齡
struct user
{
1:username string,
2:password string,
3:sex i16,
4:age i32,
}

注意,原來字段的序號標識一定不能被改變,1:username string, 不能改成 5:username string。
此時,如果server是新的,client是舊的,并不影響client的工作,client從server那邊收到的包里包含了age的信息,只是沒有decode出來而已。

b)刪除字段sex,新增字段age
struct user
{
1:username string,
2:password string,
//3:sex i16,
4:age i32, (注意,為了保證a)所定義的client能夠和b)的server交互,這里的字段序號必須定義為4)
}

此時,如果server是新的,client是a)所生成的也并不影響和b)的server交互,因為client從server那邊收到的包雖然沒有包含sex的信息,但是client并不會崩潰,只是缺少了sex的信息。
因此,我們需求變更時,盡量保存舊的字段不要刪除,做到只增不減的方式來兼容舊接口。

這里字段序號是唯一標識字段的關鍵。