0.前言
最近的项目里有用到Protocol Buffer,Protocol Buffer是Google公司开发的一种数据描述语言,类似于XML,是一种结构化数据的数据存储格式,可用于数据传输量较大的即时网络通信IM等场景。之所以使用它,是因为PB将信息序列化为二进制的格式,体积缩小了3倍,序列化速度比Json快了20-100倍,也必然会减少网络传输所需的时间。这么强大的的PB,当然要深入理解一下它为什么会如此高效。
1.信源编码和信道编码
信源编码:信源编码的目标就是使信源减少冗余,更加有效、经济地传输。
现代通信应用中常见的信源编码方式有:Huffman编码、算术编码、L-Z编码,这三种都是无损编码,另外还有一些采用压缩方式的有损编码。同时信源编码也根据,是否把一个传输单位编码为固定长度,区分为定长编码和变长编码。
信道编码:信道编码是为了对抗信道中的噪音和衰减,通过增加冗余来提高抗干扰能力以及纠错能力。信道编码的本质是降低误码率、增加通信的可靠性。
数字信号在传输中往往由于各种原因,使得在传送的数据流中产生误码,所以通过信道编码这一环节来避免码流传输中误码的发生。常用的处理技术有奇偶校验码、纠错码、信道交织编码等。
通过上面的简单介绍,我们首先给PB一个初步的定义,从通信角度来看,PB就是一种变长的无损的信源编码。接下来主要介绍PB在编码角度的性能优势。
2.PB对整数的编码优化
先考虑最简单的情况,PB如何对一个整数值进行编码。
一般情况下我们是将一个int值看作4字节,也就是所谓的定长编码。PB考虑到现实情况中,数值较大的数比数值较小的数更少地被使用这一事实,采用了变长编码,即如果一个数能够用1个字节来表示,那就用一个字节来表示。如数值1就会被编码为0000 0001,而不是把它编码为0000 0000 0000 0000 0000 0000 0000 0001。但是也由此产生一个问题,每个整数的编码长度可能不一样,如何区分边界呢?
PB将每个字节拿出最高位的1比特来作为边界的标记。如果这个标记等于1,则表示还没有到最后一个字节,否则表示到了最后一个字节。虽然造成了比特的1/8的浪费,一个很大的数将可能使用5个字节来表示。但是由于数值较小的数通常可以用1、2个字节来表示,和定长的4个字节编码相比,仍然有很大的优化。
边界问题解决之后,内容部分填入整数的补码即可。举例:
- //0000 0001表示整数1
- //1010 1100 0000 0010将两字节的最高位去掉后是 010 1100 000 0010
- //PB采用低字节优先,即后面的字节要放在高位,于是拼在一起的结果为0000010 0101100
- //最终表示的是300这个整数值
3. PB对于Key-Value对中key的优化
对于一个对象,里面包含多个变量,如何编码呢?比如一个类的定义为:
- class Test{
- String studentName;
- String studentId;
- String studentSchool;
- int age;
- }
如何使用Json,那么传输的格式可能如下:
- {"studentName":"SEU_Calvin","studentId":"150884","studentSchool":"东南大学","age":"18"}
- 1:"SEU_Calvin",2:"150884",3:"东南大学",4:"18"
对方本身保留了一份编号文档,于是就可以反序列化了。这些编号本身也可以用上面对整数的编码优化方式进行编码。
这里有个问题,如果接受端直接按照某种约定顺序解码,发送端就不需要传1、2、3、4这些编号了,可以吗?其实不可以的,因为如果存在某些变量值缺省了,就必须在相应字段填入默认的缺省值,这样反而浪费空间。或者加入变量之间的间隔符,显然也是不合理的。
4. PB对于Key-Value对中Value的优化
4.1 Value为整数
如果Value是整数,那么直接使用前面提到的2中对整数的编码优化即可。即大多数整数只占一两个字节。
4.2 Value为字符串
但是如果Value是一个很长的字符串,每个字节都拿出1个比特来区分边界就太浪费空间了,而且字符串本身就是一个一个字节的,被打乱后也会影响解码效率。因此,PB将Value长度信息的指示可以放在Key和Value之间。(长度本身也是一个整数,就用前面那种方法进行编码即可),在解码Value时,解析长度就可以知道Value值到哪里结束。
但是也因此产生一个问题,比如4.1中整数这种情况,Value中已经自带了结束标识符,那我们就不需要Value的长度指示信息了呀。因此PB引入了Type类型,即提前告诉接收端Value的类型。PB将这个Type信息放在了Key中的最后3个比特中。根据这个Type,即可让接受端注意或者忽略Value的长度字段。Value的类型在PB中称为wire_type。主要有以下几种:
- //wire type=0
- //表示这个Value是一个变长整数,比如int32, int64, uint32, uint64, sint32, sint64, bool, enum
- //wire type=1
- //表示这个Value是一个64位的定长数,比如fixed64, sfixed64, double
- //wire type=2
- //表示string, bytes等,这些Value的长度需要置于Key后面
- //wire type=3
- //表示groups中的Start Group,就是有一组,3表示接下来的Value是第一组
- //wire type=4
- //表示groups中的End Group
- //wire type=5
- //表示32位固定长度的fixed32, sfixed32, float等
5. 举例介绍
上面讲了那么多,我们用两个例子来说明一下。
例1
比如08 ac 02这三个字节。
08二进制为0000 1000,去除最高位为 0001 000。
最后3个比特为Type类型,000表示wire type=0,前面的0001表示这是编号为1的变量。
后面的ac02,写成二进制为10101100 00000010,去掉最高位分隔符为0101100 0000010,因为低字节优先,于是串起来为0000010 0101100 = 300。
最终,08ac 02这三个字节解码为编号为1的变量值为整数300。
例2
比如12 07 74 65 73 74 69 6e 67这几个字节。
12的二进制为0001 0010,最高位0表示这是最后一个字节,去除最高位后是0010 010,最后3个比特010表示wire type=2,前四位0010表示这是编号为2的变量。
因为wire type=2,表示Value是String、bytes等变长流。接下来要解码Value的长度。
07的二进制为0000 0111,最高位为0,表示这是最后一个字节,去除最高位后是000 0111(7),表示Value的长度为7,也就是后面的7个字节:74 65 73 74 69 6e 67。
这7个字节如果是String,那么根据ASCII码可解码为“testing”。
最终,1207 74 65 73 74 69 6e 67这几个字节解码为编号为2的变量值为字符串“testing”。
6. 总结PB的体积压缩优势
根据5中的例子,08 ac 02 12 07 74 65 73 74 69 6e 67这几个字节即可等价于Json中的
{"testInt":"300","testString":"testing"}
可以看出,Json使用了40个左右的字节,而PB只使用了12个字节,这也就解释了为什么PB将信息序列化为二进制后,体积缩小了3倍,也因此减少了数据网络传输的时间。
7. 总结PB的序列化速度优势
至于 为什么序列化速度提高了 20-100 倍 ,这里简单提一下,以 XML 的解包过程为例, XML 首先需要将得到的字符串转换为 XML 文档对象的结构模型,之后再从结构模型中读取指定节点的字符串,最后再将这个字符串指定为某个对象的变量值。这个过程非常复杂,其中转换文档对象结构模型的过程,通常需要完成词法文法分析等大量消耗 CPU 的复杂计算。而 PB 只需要 简单地将一个二进制序列,按照指定的格式 读取赋值到某个对象的变量值即可。因此 Json 的序列化速度非常快。