概述
1.protocol buffer编码背景
Protocol Buffer(PB)是google 的一种数据交换的格式,它独立于语言,独立于平台。可以理解为一种信源编码方式,就是将待传输的信源符号经过某种变换,转换成码流进行传输的这个变换过程。信源编码可分为两类:有损编码与无损编码,PB属于无损编码,在无损编码中,又分为定长编码和变长编码,定长编码就是一个符号变换后的码字的比特长度是固定的,比如ASCII、Unicode都是定长编码,码字是8比特,16比特。变长编码则是将信源符号映射为不同的码字长度,比如Huffman编码,PB编码。
PB也可以看作是一种协议,主要用于对象序列化。那么,如何记录一个对象的变量值呢?目前典型的格式有XML和JSON。这两种方式都有两个共同特点,即自描述特性以及文本描述。自描述是指变量名也包含在格式中。而PB不包含变量名本身,同时采用二进制编码,通信底层的协议一般均为二进制,具有解析速度快、占用空间小的优点,缺点是缺乏可读性了。PB编码充分利用二进制编码和可变长度的特点,节约对象的存储空间。
2.protocol buffer的二进制编码格式
2.1 对比定长编码和文本结构编码,了解protocol buffer的编码格式是如何影响到编码之后的文件大小。
整数的编码
int型定长编码:4个字节
PD编码(变长编码):pd为每个整数编码后还是整数个字节,但字节个数可能不同。将每个字节拿出1比特最高位的那个比特MSB(Most Significant Bit)来作为边界的标记(编码是否为最后一个字节),1表示还没有到最后一个字节,0表示到了最后一个字节。
0xxx xxxx表示某个整数编码后的结果是单个字节,因为MSB=0;
1xxx xxxx 0xxx xxxx表示某个整数编码后的结果是2个字节,因为前一个字节的MSB=1(编码结果未结束),后一个字节的MSB=0;
同理,三个字节、四个字节都用这种方法来表示边界.
举例:
0000 0001
表示整数1;
1010 1100 0000 0010
表示两个字节的结果;
将两字节的MSB去掉为:0101100 0000010,
PB对于多个字节的情况采用低字节优先,即后面的字节要放在高位,于是拼在一起的结果为:00000100101100表示300这个整数值。(其实就是将数字的二进制补码的每7位分为一组, 低7位先输出,编码在前面,在输出下一组,依次类推)
对象的编码
当对象包含多个变量是,以一个类的定义为例:
message Test1{
IntFlag= 150;
StringFlag="testing";
}
利用输出流序列化这些信息,当我们打开这个序列化文件的时候可以看到一共利用3个bytes表示的这个Message:08 96 01
PD序列编码主要有以下协议措施减少存储:
1)序号key替代变量名:
PB采用了“编号+对应变量值”的这种形式来序列化。因为编号肯定是唯一的,所以这种形式其实就是一系列Key-Value对,Key就是编号,Value就是编号对应变量的值。
分析:采用编号替代json等文本格式中的变量名可以省很大空间(实际应用中变量名可能很长);之所以保留编号,而没有直接取消按值顺序传递,因为某些值可能为空,没必要传递过去(缺省值),就只需要传递一部分即可。而1、2、3、4这些编号都不记录的话,就必须所有的都传递过去,反而并不节省空间。(因为key为整数,对小整数的pd编码是很节约空间的,一般需要1个字节)
2) 1bit定结束边界:
关于key的编码:对于key(小整数)我们采用上面的整数编码,当整数编码较大需要多个bytes时,采用整数编码提到的,将每个字节拿出1比特最高位的那个比特MSB(Most Significant Bit)来作为边界的标记。
3) 3bit标记value类型;
关于value的编码:Value也面临和Key一样的问题,首先也需要知道Value的结果有多长,是不是也采用类似的方法呢,这样就会有些难办。比如Value如果是一个字符串,可能很长,每个字节都拿出一比特来这么弄,浪费且不说,而且字符串本身就是一个一个字节的,完全被打乱了,解码的时候速度会降低。所以Value值最好一整个的放在一起。
最简单的一种思路是,关于Value长度的指示可以放在Key和Value之间。因为长度本身也是一个整数,就用前面那种方法进行编码即可,在解码时,先得到Key,然后后面跟着Value的长度,解析得到Value长度后,再解析Value值。
能不能更加节省呢?PB更加高明之处就在于此。通过观察可以知道,在程序设计时,很多变量都是一个整数(int,int64等等),对于整数,无需长度指示,按照整数编码即可。但不指示的话,怎么知道后面是个整数呢?
PB于是把Key增加了3个bit,记录后面的Value的类型wire_type(3位可以表示的类型有限,对类型粗分类),key的最终表示为:(Key << 3) | wire_type
类型 | 意义 | 用途 |
---|---|---|
0 | Varint(整型变量) | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimited(长度确定) | string, bytes, embedded messages, packed repeated fields |
3 | group开始 | group(已经启用) |
4 | group结束 | group(已经弃用) |
5 | 32-bit | fixed32, sfixed32, float |
以上面的序列编码08 96 01
为例:
因为第一个字节是Key的一个整型变量(08),去除MSB为:0001000
从这个数字的低三位我们取出value的类型(0),然后向右边移动三位可以得到它的编号(1)。所以现在我们知道tag为1,接下来的数是一个varint,接下来用varint译码方法(比如整型译码,不需保存value长度)进行译码:
96 01 = 1001 0110 0000 0001
→ 000 0001 ++ 001 0110 (丢弃MSB,按照7bits进行反转,低字节优先编码)
→ 10010110
→ 2 + 4 + 16 + 128 = 150
这样,08 96 01这三个字节就表示第一个变量值为整数150。
另一个例子:12 07 74 65 73 74 69 6e 67
第一个字节12
去除MSB为:0010010
, 后三位010表示wire type=2,前四位0010表示第2个变量。
因为wire type=2,表示Value是string, bytes等Length-delimited,接下来的数记录了Value的长度。
07的二进制:0000 0111,因为MSB=0,所以是最后一个字节,其值为0000111,即为7,表示Value的长度为7,也就是后面的7个字节:74 65 73 74 69 6e 67
这7个字节假如是string,则为“testing”(ASCII码)
于是知道,传递的是第二个变量,且值为“testing”。
如果上面的例子串起来:08 96 01 12 07 74 65 73 74 69 6e 67
就表示对象的第一个整数值为150,第二个变量的字符串为“testing”。
假如用JSON的话,就类似于这样:
{"IntFlag":"150","StringFlag":"testing"}
其中,IntFlag和StringFlag假定是类的变量名,可以看出,JSON使用了40个左右的字节,而PB使用了12个字节,如果这个对象被反复传递(大多数程序一般都是这样),则总体开销很小。
主要参考链接:
https://blog.csdn.net/sylar_d/article/details/51346290
https://www.cnblogs.com/apoptoxin/p/5713980.html
最后
以上就是强健太阳为你收集整理的protocol buffer编码格式分析的全部内容,希望文章能够帮你解决protocol buffer编码格式分析所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复