serialize_protobuf

7 min read

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编码后的字节码。

我们换个复杂一点数来看这个过程:

image

这是编码过程,解码就是反向的,先看每一个字节的第一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/可以看到如下效果。

image

小数

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)

image

列号在optional的时候非常重要,是表示数据的重要手段,下面是个例子0x08是第一列varints编码,0x10就是第二列了。

image

image

复杂例子

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} }