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

聯(lián)系作者,請加qq:1-1-3-8-5-7-1-5-4

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

廢話不說了,直入正題

binaryprotocol協(xié)議分析:

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

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

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


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

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


如果function沒有參數(shù):
最后一個(gè)字節(jié)為0

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

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

參數(shù)類型表:
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個(gè)字節(jié):(2個(gè)字節(jié))
參數(shù)序號,取決于你定義的idl文件中參數(shù)所定義的序號


(7)接下來的n個(gè)字節(jié),取決于參數(shù)的類型:

a)對于以下具有固定長度的簡單數(shù)據(jù)類型的參數(shù):
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并沒有實(shí)現(xiàn),
     所以,如果要使用該數(shù)據(jù)類型的必須注意, 應(yīng)該使用string類型來代替它
     unsigned long long 類型就需要特別注意了)
T_I64        = 10, n==8
T_DOUBLE     = 4, n==8   (double在目標(biāo)機(jī)器上必須是8個(gè)字節(jié)的,符合IEC-559標(biāo)準(zhǔn)的浮點(diǎn)數(shù),貌似對我們沒有影響)
這n個(gè)字節(jié)就是參數(shù)的值。


以下的數(shù)據(jù)類型被我歸為復(fù)合數(shù)據(jù)類型:

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

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

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

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

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

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

總結(jié):
(1)關(guān)于定義idl的一些總結(jié),盡量避免定義過于復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。

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

雖然沒有遞歸調(diào)用來封/解包, 如果定義太過于復(fù)雜的數(shù)據(jù)結(jié)構(gòu)也會(huì)隨之產(chǎn)生多重循環(huán),看下面的例子
假設(shè)定義以下一個(gè)數(shù)據(jù)結(jié)構(gòu):
map< string, list< set<string> > >,thrift將會(huì)產(chǎn)生類似于以下的循環(huán)來進(jìn)行封/解包:
foreach (key in map)
{
foreach ( set in map[key] )
{
    foreach (string in set )
      encode()/decode();  
}
}

假設(shè)map,list,set的元素各有100個(gè),這將是一個(gè)嚴(yán)重影響性能的地方,應(yīng)該避免。

(2)建議使用了unsigned long long的字段使用string類型,而不是u64類型,因?yàn)槟壳暗膖hrift不支持。

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

至于為何不提供某些字節(jié)來標(biāo)識(shí)整個(gè)數(shù)據(jù)包的長度,是因?yàn)閠hrift的binaryprotocol協(xié)議需要支持復(fù)雜數(shù)據(jù)類型,像set,list,map,而這些復(fù)合數(shù)據(jù)類型的大小是難以確定的。
為了支持標(biāo)識(shí)整個(gè)數(shù)據(jù)包長度,封包前需要知道set,list,map的總體大小,那么就需要遍歷set,list,map的大小,這是相當(dāng)不劃算,會(huì)增加運(yùn)算邏輯,而且還會(huì)導(dǎo)致協(xié)議變得很復(fù)雜。


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

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

注意,原來字段的序號標(biāo)識(shí)一定不能被改變,1:username string, 不能改成 5:username string。
此時(shí),如果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)
}

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

這里字段序號是唯一標(biāo)識(shí)字段的關(guān)鍵。