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的信息。
因此,我们需求变更时,尽量保存旧的字段不要删除,做到只增不减的方式来兼容旧接口。

这里字段序号是唯一标识字段的关键。