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中对于数字的编码方式,我们先来看无符号的数字。其编码方式如下
我们换个复杂一点数来看这个过程:
这是编码过程,解码就是反向的,先看每一个字节的第一bit,直到到是0的那个字节才是结束,然后把有效的这些字节,第一bit都去掉然后反转下顺序拼起来成一个数就行了。
负数是不能按照上面的编码方式的因为,第一位是符号位是1,这样去掉前面0这一步就删不掉任何东西了。考虑到负数的场景一般都是些绝对值比较小的数。所以进行映射,将负数区间映射到正数范围。
映射完之后就成了无符号数了,再使用上面的编码就可以了。
对于同一组字节码,pb文件如果声明的是int32
(pb中int32是指无符号整数)和sint32
,解析出的数字是不一样的。
在https://yura415.github.io/js-protobuf-encode-decode/
可以看到如下效果。
float与double因为编码方式和精度的问题,无法进行压缩,所以按照原长度进行传输。
这里数据都采用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
解析:
{c: {a: 150} }