工作生活

RPC-Thrift协议

2019-07-03  本文已影响0人  CleverApe

一、序列化协议

       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实现,就是借助该种方式来实现,丢弃了很多有效信息,性能/压缩比最好,不过向后兼容需要开发做一定的工作,详情不知。

上一篇下一篇

猜你喜欢

热点阅读