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

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个字节编码相比,仍然有很大的优化。

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

//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"}

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

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。主要有以下几种:

//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的序列化速度非常快。
 

  • 7
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值