RPC-Thrift协议
一、序列化协议
Thrift可以让你选择客户端与服务端之间传输通信协议的类别,在传输协议上总体上划分为文本(text)和二进制(binary)传输协议, 为节约带宽,提供传输效率,一般情况下使用二进制类型的传输协议为多数,但有时会还是会使用基于文本类型的协议,这需要根据项目/产品中的实际需求(例如:调试的时候)。
序列化协议类型:
TBinaryProtocol:二进制编码格式进行数据传输。
TCompactProtocol:高效密集型的二进制序列化协议,使用Variable-Length Quantity (VLQ) 编码对数据进行压缩。
TJSONProtocol:使用JSON的数据编码协议进行数据传输。
TSimpleJSONProtocol:这种节约只提供JSON只写的协议,适用于通过脚本语言解析。
TTupleProtocol(继承自TCompactProtocol)
TDebugProtocol:在开发的过程中帮助开发人员调试用的,以文本的形式展现方便阅读。
RPC框架中一般使用 TCompactProtocol。
二、传输层
Thrift中所有的传输层协议的基类是TTransport。另外,需要说明的一点是,thrift是基于TCP协议的。
传输协议类型:
TSocket:使用堵塞式I/O进行传输,也是最常见的模式。
TFramedTransport:使用非阻塞方式,以frame为单位进行传输,类似于Java中的NIO。
TFileTransport:以文件形式进行传输,虽然这种方式不提供Java的实现,但是实现起来非常简单。
TMemoryTransport:使用内存I/O,如Java中的ByteArrayOutputStream实现。
TZlibTransport:使用执行zlib压缩,与其他传输方式联合使用,不提供Java的实现。
TNonblockingTransport:使用非阻塞方式,用于构建异步客户端。
RPC框架中一般使用 TFramedTransport。
三、Thrift的序列化和反序列化方式
步骤:
使用IDL创建thrift接口定义文件;
将thrift的定义文件转换为对应语言的源代码;
选择相应的protocol,进行序列化和反序列化;
四、TCompactProtocol与TBinaryProtocol的原理和区别
Thrift二进制序列化协议中,默认为TBinaryProtocol,关于TCompactProtocol的说明,为高效密集型的二进制序列化(varint)。
那么TCompactProtocol相对于TBinaryProtocol是怎样做到高效密集的呢?TCompactProtocol是否一定比TBinaryProtocol高效?
我们以比较常用的i32类型为例,来解释一下两种方式各自的原理:
TBinaryProtocol
处理i32整型数据类型时,定义的是4个字节的数组,32位的长度正好可以保存到这4个字节组当中。如果我们分别以n1~n32来表示第1位到第32位,那么这个数组的数据结构应该为以下结构:
i32out[0] {n1 ~ n8 }
i32out[1] {n9 ~ n16}
i32out[2] {n17 ~ n24}
i32out[3] {n25 ~ n32}
这样的实现很简单.
对于其它类型,比如i16,也是类似的原理,不过是以2个字节的数组保存,在此不再说明了。
因为我们架构中使用的是TCompactProtocol,所以我们需要重点了解一下该协议的序列化方式。
TCompactProtocol
在处理i32整型数据类型时,与TBinaryProtocol完全不同,采用的是1~5个字节组来保存。依然以n1~n32来表示第1位到第32位,数据结构应该为以下结构:
i32out[0] {1 , 0 , 0 , 0 , n1 ~ n4}
i32out[1] {1 , n5 ~ n11}
i32out[2] {1 , n12 ~ n18}
i32out[3] {1 , n19 ~ n25}
i32out[4] {0 , n26 ~ n32}
这是一种极端情况,5个字节全部占满。
很显然,这样做比TBinaryProtocol复杂得多,而且还多了1个字节,并没有达到密集的目的。那是不是说明TBinaryProtocol更好?
先不急着下结论,举个具体一点的例子来说明两种实现的区别。
假如我们需要序列化一个十进制数值'10',那么它的二进制表示方式应该为'1010',只用了4位,但i32会在前面自动补0,
则是:'00000000000000000000000000001010',那么如果使用TBinaryProtocol方式来保存,则应该为以下结构:
i32out[0] {0 , 0 , 0 , 0 , 0 , 0 , 0 , 0}
i32out[1] {0 , 0 , 0 , 0 , 0 , 0 , 0 , 0}
i32out[2] {0 , 0 , 0 , 0 , 0 , 0 , 0 , 0}
i32out[3] {0 , 0 , 0 , 0 , 1 , 0 , 1 , 0}
这样有什么问题呢?那就是大量补足的0占用了宝贵空间。
接着我们再来看看TCompactProtocol会怎样保存'10'这个数值:
i32out[0] {0 , 0 , 0 , 0 , 1 , 0 , 1 , 0}
TCompactProtocol只用了1个字节,而TBinaryProtocol依然用了4个字节。这就是为什么说TCompactProtocol高效密集型的二进制序列化的原因。
TCompactProtocol的保存规则
TCompactProtocol每个字节的第1位是状态位,第2位到第8位保存具体的数据.
这有别于TBinaryProtocol的1到8位全部保存具体数据。这也是为什么极端情况下TCompactProtocol比TBinaryProtocol多占1个字节的原因。
TCompactProtocol的字节中第1位状态位的意思是标记此字节后是否还有数据。1为有数据,0为没有数据.
为了更容易理解,我们再举一个例子,用TCompactProtocol来序列化十进制数值'300',二进制应该为'100101100',用TCompactProtocol方式来保存则为如下结构:
i32out[0] {1 , 0 , 0 , 0 , 0 , 0 , 1 , 0}
i32out[1] {0 , 0 , 1 , 0 , 1 , 1 , 0 , 0}
这里将'100101100'拆分为了2部分,'10'和'0101100',在i32out[0]中保存了'10',并在第1位记为'1'来表示后面还有数据,第7位和第8位保存'10',不足的几位(第2位到第6位)补0;
所以i32out[0]为'10000010',在i32out[1]中保存了后面的'0101100',并在第1位记为'0'来表示后面没有了,则第1位为'0',所以i32out[1]为'00101100'。
TCompactProtocol以这样的原理来达到压缩的目的。
thrift 文件:
namespace java mmxf.thrift;
struct Pair {
1: required string key
2: required string value
}
"1", "2" 这些数字标识符究竟有何含义? 它在序列化机制中究竟扮演什么样的角色?
thrift官网描述:thrift的向后兼容性(Version)借助属性标识(数字编号id + 属性类型type)来实现, 可以理解为在序列化后(属性数据存储由 field_name:field_value => id+type:field_value)。
所以,id很重要,一旦id顺序混淆或者有变化,value值与name的对应也会变换,name在其中并没有映射作用。
注意:RPC服务数据发送方(生产者)和读取方(消费者)如果同样的字段name,id不同,获取到的value是不一致的,会出现不同name的value互换的奇怪现象。
数据交换格式分类
当前的数据交换格式可以分为如下几类:
1. 自解析型
序列化的数据包含完整的结构,包含了field名称和value值. 比如xml/json/java serizable,百度的mcpack/compack, 都属于此类. 即调整不同属性的顺序对序列化/反序列化不影响。
2. 半解析型
序列化的数据,丢弃了部分信息,比如field名称,但引入了index(常常是id+type的方式)来对应具体属性和值。这方面的代表有google protobuf,thrift也属于此类。
3. 无解析型
传说中百度的infpack实现,就是借助该种方式来实现,丢弃了很多有效信息,性能/压缩比最好,不过向后兼容需要开发做一定的工作,详情不知。