简介

protocol buffer这种优良的编码方式,到底底层是怎么工作的呢?为什么它能够实现高效疾速的数据传输呢?这所有都要从它的编码方式说起。

定义一个简略的message

咱们晓得protocol buffer的主体就是message,接下来咱们从一个简略的message登程,具体解说protobuf中的编码方式。

比方上面的一个非常简单的音讯对象:

message Student {  optional int32 age = 1;}

在下面的例子中,咱们定义了一个Student音讯对象,并给他定义了一个名叫age的字段,并给它设置一个值叫做22。而后应用protobuf将其进行序列化,这么大的一个对象,对其序列化之后的字节如下所示:

08 96 00

很简略,应用三个字节就能够示意一个messag对象,数据量十分小。

那么这三个字节到底示意什么意思呢?一起来看看吧 。

Base 128 Varints

在解释下面的三个字节的含意之前,咱们须要理解一个varints的概念。

什么叫Varints呢?就是序列化整数的时候,占用的空间大小是不一样的,小的整数占用的空间小,大的整数占用的空间大,这样不必固定一个具体的长度,能够缩小数据的长度,然而会带来解析的复杂度。

那么怎么晓得这个数据到底须要几个byte呢?在protobuf中,每个byte的最高位是一个判断位,如果这个位被置位1,则示意前面一个byte和该byte是一起的,示意同一个数,如果这个位被置位0,则示意前面一个byte和该byte没有关系,数据到这个byte就完结了。

举个例子,一个byte是8位,如果示意的是整数1,那么能够用上面的byte来示意:

0000 0001

如果一个byte装不下的整数,那么就须要应用多个byte来进行连贯操作,比方上面的数据表示的是300:

1010 1100 0000 0010

为什么是300呢?首先看第一个byte,它的首位是1,示意前面还有一个byte。再看第二个byte,它的首位是0,示意到此就完结了。咱们把判断位去掉,变成上面的数字:

010 1100 000 0010

这时候还不能计算数据的值,因为在protobuf中,byte的位数是反过来的,所以咱们须要把下面的两个byte替换一下地位:

000 0010 010 1100 

也就是:

10 010 1100 

=256 + 32 + 8 + 4 = 300

音讯体的构造

从message的定义能够晓得,protobuf中的音讯体的构造是key=value的模式,其中的key就是message中定义的字段的整数值1,2,3,4等。而value就是真正对其设置的值。

当一个音讯被编码之后,这些key和value会被连贯在一起,组成一个byte stream。当要对其进行解析的时候,须要定位到key和value的具体长度,所以在key中须要蕴含两局部,第一个局部就是字段在proto文件中的值,第二个局部就是value局部占用的长度大小。

只有通过这两个局部的值联合起来,解析器才可能正确的对字段进行解析。

key的这种格局,被称为 wire types,有哪些 wire types呢?咱们看一下:

类型含意应用场景
0Varintint32, int64, uint32, uint64, sint32, sint64, bool, enum
164-bitfixed64, sfixed64, double
2Length-delimitedstring, bytes, embedded messages, packed repeated fields
3Start groupgroups (deprecated)
4End groupgroups (deprecated)
532-bitfixed32, sfixed32, float

能够看到除了3,4两种类型之外,其余的类型能够分为三类,一类是固定长度的类型,如1,5,他们别离是64位和32位的数字。

第二类是0,示意Varint,这是一种可变类型,用来示意通用的数字类型,bool类型和枚举类型。第三类2,示意长度辨别的类型,这种类型通常用来示意字符串,字节数字等。

所有的key都是一个varint类型,它的值是:(field_number << 3) | wire_type ,也就是说key的最初三个位,用来存储wire类型。

下面咱们例子中的key的值是08,用二进制示意:

000 1000

最初三位是0,示意是一个Varint类型,将08右移三位,失去1,示意key示意的字段是1这个字段,也就是age。

而后咱们看下剩下的局部96 00,换成二进制是:

96 00 = 1001 0110  0000 0000

依据Varint的定义,第一位示意的是连贯位,示意第二个字节的内容和第一个字节的内容是一起的。对于Varint来说,须要将低位的字节和高位的字节进行替换,如下:

1001 0110  0000 0000 去掉最高位的1 :001 0110  0000 0000  替换低位字节和高位字节:0000 0000  001 0110 

下面的值是16 + 4 + 2 = 22

这样咱们就失去了值为1的key,对应的value是22。

符号整数

咱们晓得有两种示意符号整数的形式,一种是规范的int类型:int32 和 int64,一种是带符号的int类型:sint32 和 sint64。

这两种类型的区别在于对应负整数的示意上。对于int32和int64来说,所有的负整数都是以十个字节来示意的,所以占用的空间会比拟大,不适宜用来示意负整数。

如果应用sint32 和 sint64,那么应用的编码方式是ZigZag,对于负整数来说更加无效。

ZigZag将带符号的整数和无符号的整数进行映射,对于每个n来说,将会应用上面的公式来编码:

(n << 1) ^ (n >> 31)

对于sint64来说就是:

(n << 1) ^ (n >> 64)

举个例子:

符号整数编码后果
00
-11
12
-23
21474836474294967294
-21474836484294967295

# 字符串

字符串的wire类型是2,阐明它的值是一个varint编码的长度。举个例子:

 message Student {  optional string name = 2;}

上咱们给Student定义了第二个属性name,如果给name赋值 "testing" ,那么失去的编码是:

12 07 [74 65 73 74 69 6e 67]

中括号的编码就是"testing"的UTF8示意。

0x12 能够这样解析:

 0x12→ 0001 0010  (binary representation)→ 00010 010  (regroup bits)→ field_number = 2, wire_type = 2

0x12示意字段2的类型是2,前面跟着的07就示意后续byte字节的长度了。

嵌套的音讯

音讯中能够嵌套音讯,咱们看一个例子:

message Teacher {  optional Student s = 3;}

如果咱们把s的age字段设置为22,就和第一个例子一样,那么下面的编码就是:

 1a 03 08 96 00

能够看到前面的三个字节和第一个例子是一样的。后面两个字节的判断形式和字符串是一值的,这样就不再多讲。

总结

好了,protobuf的根本编码规定和实现曾经讲完了。听起来是不是很微妙?

本文已收录于 http://www.flydean.com/03-protobuf-encoding/

最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!

欢送关注我的公众号:「程序那些事」,懂技术,更懂你!