Protocol Buffer编码
ProtoBuf数据类型
一个简单的Message
让我们看一下下面这个简单的Message的定义:
1 |
|
在一个应用中,你创建了一个名为Test1的Message,并且设置a为150。然后你将这个Message序列化后得到输出流如下三个字节:
1 |
|
如此小的数字表示,但这些都意味着什么 呢
在protobuf中message是一系列的键值对,上面三个字节中的08(000 1000)表示了message中的数据类型和关键字编号,其中,数据类型是此字节编码的后三位,即0(000),然后根据下面的表格确定类型;关键字(上述例子中的a)的编号是此字节右移三位,即1(000 0001)。此时就容易有一个疑问了,那a是怎么表示的?这就是protobuf的另一个优点了——安全,如果消息被截取是没办法知道消息代表了什么意思,只有拥有.proto文件才能对应上关键字。
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 |
上面例子中的96 01就是代表着我们给a设置的值——150,下面介绍了protobuf中message的数据类型是如何编码的。
Varints
Varint从名字就能看出点什么var(可变的) int,有点MySQL中varchar那种感觉,这种类型是使用一个或多个字节去序列化integer的方法,越小的数字会占用越小的字节。
变长的类型需要解决的一个问题是确定编码的边界,varints为了解决这个问题,为每个字节都设置了一个标志位,如果下一个字节还是我的就是1,如果下一个字节不是我的了就是0。每个字节的低七位用来以二进制补码来存储一组数据,采用小端字节序(这里有篇文章介绍了字节序 [https://www.ruanyifeng.com/blog/2016/11/byte-order.html]),本排前面的字节在后面。举个例子300的编码
300的补码为
1 |
|
根据低七位为用来存储数据,则可分解为:
1 |
|
再根据是小端字节序,转化后为
1 |
|
再根据每个字节都需要设置一个标志位,下一个字节还是我的就是1,不是我的就是0,则转化后为:
1 |
|
这就是300的varints的字节编码,一共占用了两个字节。如我们所了解,例如Java中的int是固定长度,占用4个字节的,而varints表示的数字越小占用的空间也就越小,但是当足够大的时候,varints是比固定长度需要的空间多的。但是我们用的数的范围一般较小。
Signed Integers
在计算机中,整数类型都是用补码来存储的,varints也一样,补码的计算是原码所有位取反,然后再加1,例如int32 的-64的原码为
1 |
|
-64的补码为
1 |
|
占用4个字节,如果转换为varints的话,则需要占用5个字节,不论负数的数值多大,都会稳定的占用5个字节(实际上protobuf中,会稳定占用10个字节,因为是按照long来编码,是为了int32改为in64的时候仍可以兼容
为了解决这个不够高效的问题,最后引入了Zigzag编码,这个编码的主要做的是将有符号数,通过一个公式转成无符号数,然后再根据varints编码进行处理。这个公式如下:
1 |
|
Signed Original | Encoded As |
---|---|
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
2147483647 | 4294967294 |
-2147483648 | 4294967295 |
Non-varint Numbers
Non-varint就比较简单了,当算出来数据类型为1的时候,就直接需要一个64位大小的数据块,数据类型为5的时候,需要一个32位大小的数据块。这两种情况也是和上面一样采用小端字节序
String(字符串)
先看一个带有string的Message,并且设置b的值为”testing”
1 |
|
编码后为:
1 |
|
字符串的编码和之前不同的是,第一个字节依旧代表类型和关键字编号,第二个字节表示后面有多少位是这个字段的,即编码方式为:key + length + content
12 ( 0001 0010),后三位 010 为 数据类型为 2,0001 0010 右移三位为 0000 0010,即关键字编号为2。length为7则后面跟着7个字节是这个字段的,即我们设置的值
Embedded Messages(嵌入式Message)
同样的先来看个带有嵌入式的Message,设置值为上面的Test1
1 |
|
编码后为:
1 |
|
先来看一下数据类型1a(0001 1010)后三位010为2,跟String一样,03代表长度,那08 96 01就很眼熟了,就是开头说的Test1。
Packed Repeated Fields(打包重复元素)
在protobuf2.1.0引入了一个[packed=true]的东西,如下面的例子
1 |
|
在不使用[packed=true]的时候,repeated的字段会被编译成关键字编号一样的多个键值对,并且不是连续的,也没有什么顺序;在解析时,元素之间的顺序会被保留下来,但是其他字段的顺序会丢失。在protobuf2.1.0中引入了[packed=true],protobuf3.0中已经默认使用这个东西,当用上这个东西的时候,这个重复字段会被单独打包到一个键值对中,并且数据类型为2。就如上面那个Test4例子,当给重复字段赋值为3,270和86942后,编码后为:
1 |
|
Field Order(字段顺序)
在.proto文件中字段编码可以随便写,对Message的序列化顺序是没有任何影响的。当Message被序列化的时候,对于字段是没有保证的顺序。序列化的顺序是一个实现细节,将来任何特定的实现细节都可以被更改,因此protobuf解析器必须能够以任何顺序解析字段。