赞
踩
本系列准备介绍微服务框架的相关内容,以我目前在用的kite框架为栗子,也扩展一些业界常见的实现,主要包括一下的部分:
在进程中直接操作的都是数据结构和对象,但是内存中的数据结构和对象是不能直接进行网络传输的,必须将其转换为二进制的数据,把内存中的对象转变为二进制数据的过程称之为序列化,反之,将二进制数据还原为相应对象的过程就是反序列化。
常见的序列化技术有json、thrift、protobuf等。(这里解释一下,protobuf是单纯的序列化技术,但是thrift是一套完整的rpc解决方案,序列化只是其中的一小部分内容,这里说thrift只是为了称呼方便,指代的是thrift中的序列化部分。) 其中json主要用在web应用前后端交互的场景下,其可读性强,但是性能和压缩率比较差。在后端rpc中常用的序列化技术主要是thrift和protobuf。
上图为thrift的架构图,可以看到thrift protocol层支持BinaryProtocol、CompactProtocol、JsonProtocol、MultiplexedProtocol,其中最常用的为BinaryProtocol、CompactProtocol,但是这里的协议是通讯协议或者说rpc协议,序列化协议主要支持binary、compact、json,下面简单介绍下binary和compact。
thrift的二进制编码采用TLV编码格式实现。一个TLV的值由tag、length、value三个字段构成,其中tag表示数据的类型,length表示数据的长度,value表示数据的值。一般情况下,tag和length的长度是固定的,length是可选的,value的长度是可变的。具体的细节可见下表。
可以看到thrift用一个字节来标识具体的数据类型,两个字节来标识对应字段的编号。对于固定长度的数据类型,没有length字段,对于变长的数据类型,有四个字节的length字段来表示数据长度。
压缩二进制协议和二进制协议基本是一样的,都是采用TLV的编码格式,唯一区别的点在于压缩二进制协议对于整形会采用varint+zigtag的方式进行编码。
varint编码方式对每个字节都只用后7位表示实际数据,最高位来表示下一个字节是否还属于当前数据,最高位为1则下一个字节还是当前数值范围,最高位为0则下一个字节不属于当前数值范围。
采用该方式进行编码时,对于小整数会占用更少的字节,对于大整数会占用更多的字节。对于i32来说,可能会占用1-5个字节,对i64来说,可能会占用1-10个字节。
下面以300(i32)为例来演示varint是如何编码的。
# 300的二进制表示如下,占4个字节32位
00000000 00000000 00000001 00101100
# 300以varint表示如下
10101100 00000010
对于无符号的整数来说,只会采用varint来进行编码。对于有符号的整数,因为最高位用来表示符号,对于负数,最高位为1,其实是很大的整数,反而会消耗更多的字节来表示。所以对于有符号整数,先采用zigzag将其映射到无符号的整数再进行varint编码。zigtag编码如下:
i32:
1) if n >= 0, m=2*n
2) if n < 0, m=2*|n| - 1
3) 采用位运算实现: m=(n << 1) ^ (n >> 31)
i64:
1) if n >= 0, m=2*n
2) if n < 0, m=2*|n| - 1
3) 采用位运算实现: m=(n << 1) ^ (n >> 63)
protobuf的编码方式其实和thrift大同小异,同样采用了TLV的编码方式,对于整数采用varint和zigtag的方式。
当然区别也是有的。之前我们提到过,在thrift中会用一个字节来表示数据类型,两个字节来表示编号,这其实是非常浪费的。一方面thrift中的数据类型只有11种,另一方面绝大多数情况下我们定义的结构体中的字段都只有几个,编号不会很大。
所以protobuf对此进行了优化,tag占用一字节,其中较低的三位表示write type,代表数据类型,较高的五位表示编号,当编号大于16的时候编号会额外再占据一字节(关于protobuf如何判断编号是否额外占据一字节还不是很清楚,让我们暂时忽略细节<updatae, tag是使用varint来表示的,破案>)。
这里是另一篇关于pb动态解析的文章,有兴趣可以看一眼。
关于序列化技术,最受关注的有两点:性能和压缩率。
最后还有一点需要重复提到的是,protobuf是单纯的序列化技术,但是thrift是一套完整的rpc解决方案,很难去单独地使用其序列化技术,这可能也是在技术选型时比较重要的影响因素。
再次强调,在文章最开始的时候我们提到过,这里的协议是指通信双方约定好的通信的格式。
协议的制定对于通信至关重要,这关系到信息能否有效地传递。大多数的协议都是header + body的形式。
下图为thrift binary protocol的格式,是由header + body组成。
----------------------------------------------------------------------------------------
| header | body |
| magic | method name length | method name | sequence number | result |
| 4 | 4 | N length size | 4 | X |
----------------------------------------------------------------------------------------
header首先是一个4字节的magic字段,其中高16位为8001,低16位表示TMessageType。TMessageType目前有4种:
之后是4字节的方法名称长度,不定长的方法名称,4个字节的sequence num。
然后就是body部分。body部分就是用上面讲的序列化方式将请求或者响应序列化后的二进制数据。
我们看到这header并没有消息长度这个字段,这是因为thrift在设计时的面向流的理念。这样在序列化时不需要先计算出整个消息的长度,这在消息很大时对性能有很大的提升。那么header中没有length如何判断消息结束呢?对于结构体来说,会设置一个带有特殊stop type的字段来标识结构体的结束。
接下来,继续以thrift为例,介绍rpc框架的基础。
thrift是以层级的结构来构建rpc框架,并对不同层级进行了抽象,分为Ttransport、
Tprotocol、Tprocessor三层。对于服务端来说,还有Tserver,Tserver来管理前面的三层。
简单的示意图如下,更多详细内容会在kite的具体实现中介绍。
参考文档:
https://thrift.apache.org/static/files/thrift-20070401.pdf
如果觉得本文对您有帮助,可以请博主喝杯咖啡~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。