以前已经发了三篇有关Protocol Buffer的技术博客,其中第一篇介绍了Protocol Buffer的语言规范,然后两篇则分别基于C++和Java给出了一些相对比较实用而又简单的示例。因为近期工做压力很大,所以对因而否继续写本篇博客也确实让我纠结了几天。但往往想到善终如始则无败事这句话时,最终的决定仍是既然开始了,就要尽本身最大的努力去作,而不要留有丝毫的遗憾。
该篇Blog的内容将彻底取自于Google的官方文档,只是为一些相对难以理解的技术点加入了适当的注解。但因技术能力有限,如解释有误,欢迎指正。
这是一篇让你对Protocol Buffer知其然亦知其因此然的文档,即使你在并不了解这其中的技术细节和处理机制的状况下,仍然可以在你的应用程序中正常的使用Protocol Buffer,然而我相信,经过对这些细节和机制的深刻了解,不只可让你更好的使用和驾驭Protocol Buffer,并且还能深深地感觉到Google工程师的智慧和高超的编程技艺,所以在我看来,深刻的研习对咱们编程能力的提升和思路的拓宽都是大有裨益的。不积跬步无以至千里。
1、简单消息编码布局:
让咱们先看一下下面的消息定义示例:
message Test1 {
required int32 a = 1;
}
假设咱们在应用程序中将字段a的值设置为150(十进制),此后再将该对象序列化到Binary文件中,你能够看到文件的数据为:
08 96 01
这3个字节的含义又是什么呢?它们又是按照什么样的编码规则生成的呢?让咱们拭目以待。
2、Base 128 Varints:
在理解Protocol Buffer的编码规则以前,你首先须要了解varints。varints是一种使用一个或多个字节表示整型数据的方法。其中数值自己越小,其所占用的字节数越少。
在varint中,除了最后一个字节以外的每一个字节中都包含一个msb(most significant bit)设置(使用最高位),这意味着其后的字节是否和当前字节一块儿来表示同一个整型数值。而字节中的其他七位将用于存储数据自己。由此咱们能够简单的解释一下Base 128,一般而言,整数数值都是由字节表示,其中每一个字节为8位,即Base 256。然而在Protocol Buffer的编码中,最高位成为了msb,只有后面的7位存储实际的数据,所以咱们称其为Base 128(2的7次方)。
好比数字1,它自己只占用一个字节便可表示,因此它的msb没有被设置,如:
0000 0001
再好比十进制数字300,它的编码后表示形式为:
1010 1100 0000 0010
对于Protocol Buffer而言又是如何将上面的字节布局还原成300呢?这里咱们须要作的第一步是drop掉每一个字节的msb。从上例中能够看出第一个字节(1010 1100)的msb(最高位)被设置为1,这说明后面的字节将连同该字节表示同一个数值,而第二个字节(0000 0010)的msb为0,所以该字节将为表示该数值的最后一个字节了,后面若是还有其余的字节数据,将表示其余的数据。
1010 1100 0000 0010
-> 010 1100 000 0010
上例中的第二行已经将第一行中每个字节的msb去除。因为Protocol Buffer是按照Little Endian的方式进行数据布局的,所以咱们这里须要将两个字节的位置进行翻转。
010 1100 000 0010
-> 000 0010 010 1100 //翻转第一行的两个字节
-> 100101100 //将翻转后的两个字节直接链接并去除高位0
-> 256 + 32 + 8 + 4 = 300 //将上一行的二进制数据换算成十进制,其值为300
3、消息结构:
Protocol Buffer中的消息都是由一系列的键值对构成的。每一个消息的二进制版本都是使用标签号做为key,而每个字段的名字和类型均是在解码的过程当中根据目标类型(反序列化后的对象类型)进行配对的。在进行消息编码时,key/value被链接成字节流。在解码时,解析器能够直接跳过不识别的字段,这样就能够保证新老版本消息定义在新老程序之间的兼容性,从而有效的避免了使用older消息格式的older程序在解析newer程序发来的newer消息时,一旦遇到未知(新添加的)字段时而引起的解析和对象初始化的错误。最后,咱们介绍一下字段标号和字段类型是如何进行编码的。下面先列出Protocol Buffer能够支持的字段类型。编程
Type | Meaning | Used For |
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 | Start group | groups (deprecated) |
4 | End group | groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
因为在编码后每个字段的key都是varint类型,key的值是由字段标号和字段类型合成编码所得,其公式以下:
field_number << 3 | field_type
由此看出,key的最后3个bits用于存储字段的类型信息。那么在使用该编码时,Protocol Buffer所支持的字段类型将不会超过8种。这里咱们能够进一步计算出Protocol Buffer在一个消息中能够支持的字段数量为2的29次方减一。如今咱们再来回顾一下以前给出的Test1消息被序列化后的第一个字节08的由来。
0000 1000
-> 000 1000 //drop掉msb(最高位)
最低的3位表示字段类型,即0为varint。咱们再将结果右移3位( >> 3),此时获得的结果为1,即字段a在消息Test1中的标签号。经过这样的结果,Protocol Buffer的解码器能够获悉当前字段的标签号是1,其后所跟随数据的类型为varint。如今咱们能够继续利用上面讲到的知识分析出后两个字节(96 01)的由来。
96 01 = 1001 0110 0000 0001
-> 001 0110 000 0001 //drop两个字节的msb
-> 000 0001 001 0110 //翻转高低字节
-> 10010110 //去掉最高位中没用的0
-> 128 + 16 + 4 + 2 = 150
4、更多的值类型:
1. 有符号整型
如前所述,类型0表示varint,其中包含int32/int64/uint32/uint64/sint32/sint64/bool/enum。在实际使用中,若是当前字段能够表示为负数,那么对于int32/int64和sint32/sint64而言,它们在进行编码时将存在着较大的差异。若是使用int32/int64表示一个负数,该字段的值不管是-1仍是-2147483648,其编码后长度将始终为10个字节,就如同对待一个很大的无符号整型同样。反之,若是使用的是sint32/sint64,Protocol Buffer将会采用ZigZag编码方式,其编码后的结果将会更加高效。
这里简单讲述一下ZigZag编码,该编码会将有符号整型映射为无符号整型,以便绝对值较小的负数仍然能够有较小的varint编码值,如-1。下面是ZigZag对照表:数组
Signed Original | Encoded As |
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
2147483647 | 4294967294 |
-2147483648 | 4294967295 |
其公式为:
(n << 1) ^ (n >> 31) //sint32
(n << 1> ^ (n >> 63) //sint64
须要补充说明的是,Protocol Buffer在实现上述位移操做时均采用的算术位移,所以对于(n >> 31)和(n >> 63)而言,若是n为负值位移后的结果就是-1,不然就是0。
注:简单解释一下C语言中的算术位移和逻辑位移。他们的左移操做都是相同的,即低位补0,高位直接移除。不一样的是右移操做,逻辑位移比较简单,高位所有补0。而算术位移则须要视当前值的符号位而定,补进的位和符号位相同,即正数全补0,负数全补1。换句话说,算术位移右移时要保证符号位的一致性。在C语言中,若是使用 int变量位移时就是算术位移,uint变量位移时是逻辑位移。
2. Non-varint数值型
double/fixed64始终都占用8个字节,float/fixed32始终占用4个字节。
3. Strings
其类型值为2,key信息以后是字节数组的长度信息,最后在紧随指定长度的实际数据值信息。如:
message Test2 {
required string b = 2;
}
如今咱们设置b的值为"testing"。其编码后数据以下:
12 07 74 65 73 74 69 6E 67
第一个字节0x12表示key,经过解码能够获得字段类型2和字段标号2。第二个字节07表示testing的长度。后面7个红色高亮的字节则表示testing。
5、嵌入消息:
这里是一个包含嵌入消息的消息定义。
message Test3 {
required Test1 c = 3;
}
此时咱们先将Test1的a字段值设置为150,其编码结果以下:
1A 03 08 96 01
从上面的结果能够看出08 96 01和以前直接编码Test1时是彻底一致的,只是在前面增长了key(字段类型 + 标号)和长度信息。新增信息的解码方式和含义与前面的Strings彻底相同,这里再也不重复解释了。
6、Packed Repeated Fields:
Protocol Buffer从2.1.0版本开始引入了[pack = true]的字段级别选项。若是设置该选项,那么元素数量为0的repeated字段将不会被编码,不然数组中的全部元素会被编码成一个单一的key/value形式。毕竟数组中的每个元素都具备相同的字段类型和标号。该编码形式,对包含较小值的整型元素而言,优化后的编码结果能够节省更多的空间。如:
message Test4 {
repeated int32 d = 4 [pack=true];
}
这里咱们假设d字段包含3个元素,值分别为3,270,86942。编码结果以下:
22 //key (字段标号4,类型为2)
06 //数据中全部元素所占用的字节数量
03 //第一个元素(varint 3)
8E 02 //第二个元素(varint 270)
9E A7 05 //第三个元素(varint 86942)
7、字段顺序:
在.proto文件中定义消息的字段标号时,能够是不连续的,可是若是将其定义为连续递增的数值,将得到更好的编码和解码性能。
结束语:
本篇博客是Protocol Buffer技术详解系列的最后一篇博客,同时该系列博客又将是开源学习之旅系列主题中的第一个系列,但愿从此可以借此平台与你们进行更多的技术交流,共同提升。若有意见或问题,欢迎留言。布局