Android开发——Protocol Buffer效率之高的原理介绍

xiaoxiao2021-02-28  9

0.前言

最近的项目里有用到Protocol BufferProtocol BufferGoogle公司开发的一种数据描述语言,类似于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个字节来表示。但是由于数值较小的数通常可以用12个字节来表示,和定长的4个字节编码相比,仍然有很大的优化。

边界问题解决之后,内容部分填入整数的补码即可。举例:

[java] view plain copy //0000 0001表示整数1  //1010 1100 0000 0010将两字节的最高位去掉后是 010 1100 000 0010  //PB采用低字节优先,即后面的字节要放在高位,于是拼在一起的结果为0000010 0101100  //最终表示的是300这个整数值  

 

3.  PB对于Key-Value对中key的优化

对于一个对象,里面包含多个变量,如何编码呢?比如一个类的定义为:

[java] view plain copy class Test{      String studentName;      String studentId;      String studentSchool;      int age;  }  

如何使用Json,那么传输的格式可能如下:

[html] view plain copy {"studentName":"SEU_Calvin","studentId":"150884","studentSchool":"东南大学","age":"18"}   PB认为"studentName"、"studentId"、"studentSchool"等这些 变量名不应该包含在传输消息中 ,因为编解码、传输这些信息也需要资源。PB为了 节省空间 ,在通信双方都保持一份文档,记录了"studentName"、"studentId"的编号,比如上述四个变量名字分别编号为1、2、3、4。于是在序列化的时候,只需要传输下面的信息:

[html] view plain copy 1:"SEU_Calvin",2:"150884",3:"东南大学",4:"18"  

对方本身保留了一份编号文档,于是就可以反序列化了。这些编号本身也可以用上面对整数的编码优化方式进行编码。 

这里有个问题,如果接受端直接按照某种约定顺序解码,发送端就不需要传1234这些编号了,可以吗?其实不可以的,因为如果存在某些变量值缺省了,就必须在相应字段填入默认的缺省值,这样反而浪费空间。或者加入变量之间的间隔符,显然也是不合理的。

 

4.  PB对于Key-Value对中Value的优化

4.1  Value为整数

如果Value是整数,那么直接使用前面提到的2中对整数的编码优化即可。即大多数整数只占一两个字节。

4.2  Value为字符串

但是如果Value是一个很长的字符串,每个字节都拿出1个比特来区分边界就太浪费空间了,而且字符串本身就是一个一个字节的,被打乱后也会影响解码效率。因此,PBValue长度信息的指示可以放在KeyValue之间。(长度本身也是一个整数,就用前面那种方法进行编码即可),在解码Value时,解析长度就可以知道Value值到哪里结束。

 

但是也因此产生一个问题,比如4.1中整数这种情况,Value中已经自带了结束标识符,那我们就不需要Value的长度指示信息了呀。因此PB引入了Type类型,即提前告诉接收端Value的类型PB将这个Type信息放在了Key中的最后3个比特中。根据这个Type,即可让接受端注意或者忽略Value的长度字段。Value的类型在PB中称为wire_type。主要有以下几种:

[java] view plain copy //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,表示ValueStringbytes等变长流。接下来要解码Value的长度。

07的二进制为0000 0111,最高位为0,表示这是最后一个字节,去除最高位后是000 01117),表示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 的序列化速度非常快。
转载请注明原文地址: https://www.6miu.com/read-2350246.html

最新回复(0)