protobuf
pb是一种常见的序列化方式,pb的序列化效率较高,比json格式的数据size小很多,是很多rpc系统常用的格式。但是pb需要发送端和接收端协商好数据的格式,不像json是可以自解释的。
本文来说一下pb是如何进行序列化的,文章内容全部参考https://developers.google.com/protocol-buffers/docs/encoding
数据压缩
数字类型
无符号整数
对于常见的数字类型像int long等,在数据传输的时候都存在一个问题,那就是数字往往不大,或者说绝对值不大,但是因为类型是int,那就必须传输4字节,但是实际上绝大多数时候可能都是0-100范围的数,一个byte就能表示却要4个byte的大小来传输。
Varints
就是pb中对于数字的编码方式,我们先来看无符号的数字。其编码方式如下
- 1 将数字表示成2进制,例如int值5表示成2进制就是00...101(前面是29个0,最后101)
- 2 将前面连续的0去掉,使得剩下的bit数是7的倍数,每7个bit作为一组。5的话就剩下 0000101 这1组数据。
- 3 pb是按照小端的方式排列数据的,需要将低位放在前面,所以2得到多组的话,需要翻转一下组的顺序。例子5因为只有一组就不用翻转了。
- 4 将每组前面加一个bit1,最后一组则加bit0,凑成8bit也就是1字节。该bit表示这个字节是不是本数据的结束,0是,1不是结束。00000101就是5经过varints编码后的字节码。
我们换个复杂一点数来看这个过程:
这是编码过程,解码就是反向的,先看每一个字节的第一bit,直到到是0的那个字节才是结束,然后把有效的这些字节,第一bit都去掉然后反转下顺序拼起来成一个数就行了。
有符号整数
负数是不能按照上面的编码方式的因为,第一位是符号位是1,这样去掉前面0这一步就删不掉任何东西了。考虑到负数的场景一般都是些绝对值比较小的数。所以进行映射,将负数区间映射到正数范围。
- 0映射0
- 正数n映射为2n (例如1=>2, 2=>4, 2147483647=>4294967294)
- 负数-n映射为2n-1 (例如-1=>1, -2=>3, -2147483648=>4294967295)
映射完之后就成了无符号数了,再使用上面的编码就可以了。
对于同一组字节码,pb文件如果声明的是int32
(pb中int32是指无符号整数)和sint32
,解析出的数字是不一样的。
在https://yura415.github.io/js-protobuf-encode-decode/
可以看到如下效果。
小数
float与double因为编码方式和精度的问题,无法进行压缩,所以按照原长度进行传输。
字符串 数组(list) 嵌入结构等
这里数据都采用Length-delimited
编码,因为他们都是有长度的,所以先说明下长度,然后用分别表示每个元素,以string为例。
07 74 65 73 74 69 6e 67
第一个字节就是长度为7,接下来就取7个字节,作为字符串的数据部分。
整体结构
上面是从细节上解释了各个数据类型采用何种编码。对于pb来说,还有个很重要的就是对于字段的描述文件.proto
文件,格式如下。
message Test1 {
required sint32 a = 1;
optional int32 b = 2;
}
required和optional是pb2的语法,在pb3中都是optional的。require表示必须提供,optional则是可以为空数据,因为字段可以是optional的,所以就必须有个序号来表示自己这段数据是第几个字段,也就是等号后面的1和2。
在编码的时候,需要在数据编码前面放置一个字节来表示他是第几个字段,和使用哪种编码,其中前五个bit表示第几列(也就表示一般情况下列数不能超过32列),后3bit表示使用哪种编码
例如:0x08 (00001 000) 表示接下来是第一列数据,使用0号编码方式(也就是varints)
列号在optional的时候非常重要,是表示数据的重要手段,下面是个例子0x08是第一列varints编码,0x10就是第二列了。
复杂例子
message Test1 {
optional int32 a = 1;
}
message Test3 {
optional Test1 c = 3;
}
数据为
1a 03 08 96 01
解析:
- 1a=>00011 010是第3列,采用2号编码(Length-delimited)。
- 03 表示长度是数据总长度是3个字节。
- 08 96 01 是Test1所占的3个字节,然后按照Test1的规则解析
- 08=>00001 000是第一列,采用0号编码(varints)
- 96 01 => 按照varints解析出150
- 最终解析出
{c: {a: 150} }