共计 3031 个字符,预计需要花费 8 分钟才能阅读完成。
简介
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 呢?咱们看一下:
类型 | 含意 | 应用场景 |
---|---|---|
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 |
能够看到除了 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)
举个例子:
符号整数 | 编码后果 |
---|---|
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
2147483647 | 4294967294 |
-2147483648 | 4294967295 |
# 字符串
字符串的 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/
最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!
欢送关注我的公众号:「程序那些事」, 懂技术,更懂你!