protobuf编码方式

645 阅读4分钟

最近看到一个关于序列化的面试题,就去看了一些protobuf的序列化实现方式,这里总结一下,主要内容就来自于官网。protobuf的基本特性这里就不详细介绍了,我关心的其实主要是下面几点问题;

  • 基本类型的序列化
  • 嵌入类型的序列化
  • repeat的序列化
  • map的序列化

基本类型的序列化

protobuf的的序列化是基于idl的,发送方和接受方都保存有idl。因此protobuf的序列化方式更加安全(没有idl很难解析),效率也更高(不需要记录key的名字)。在二进制字节编码中,只需要保留field的序号,长度(可选)和值即可,如图所示:

key+(len)+value | key+(len)+value | key+(len)+value | key+(len)+value ...

key的编码

key是由序号左移3位,与类型标志位按位或之后的结果,

类型 含义 适用范围
0 varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 长度有限类型 string、bytes、嵌入类型、重复类型
5 32-bit fixed32, sfixed32, float

比如官网的例子,08 96 01中第一位标识key的编码

08是key的部分
→ 1 <<3 | 0
也就是0000 1000

key的index的编码方式也是varint。可以看下后面的代码实现

value的编码

然后是value的编码,这里其实要分3类, 一类是整形都是采用了varint+zigzag编码对应0;第二类是浮点数,就是采用常规的编码对应1、5;第三类是有限长度的编码对应2

varint

首先说明下varint的编码,思路很简单,很多数字的编码采用的是固定长度的字符串编码完成,比如典型的int64就是8位,int32就是4位。 但是大多数场景下传递的数字都很小。varint的思路就是用最高位bit标识是否有后续字符,这样在数字较小的情况下可以用更小的位数完成数字的表达。 比如1只用一个字节标识0000 0001,300需要2个字符1010 1100 0000 0010标识。

96 01是value的部分,实际上是150
96 01 = 1001 0110  0000 0001
→ 000 0001  ++  001 0110
→ 10010110
→ 128 + 16 + 4 + 2 = 150

可以看出varint的比较擅长标识小数字,这样导致一个问题是如果是补码形势的负数很难表示,因此protobuf采用另外一种ZigZag的编码方式将所有的负数都映射成大于0的数来表示,经过这样的转换绝对值小的数需要用的字符编码长度也更小。

n = (n << 1) ^ (n >> 31)  // 32位编码
n = (n << 1) ^ (n >> 63)  // 64位编码

浮点数

其次说明下浮点数 浮点数比较简单就是常规的编码模式,因此protobuf实际上没有对浮点数进行压缩。

string的编码

最后说优先有限的编码 比如说官网给的例子,如果是对字符串的编码

message Test2 {
  optional string b = 2;
}

对于testing编码后结果为 12 07 74 65 73 74 69 6e 67 12是2 << 3 |2 之后的结果 07标识字符串的长度(长度的编码也是varint编码) 74 65 73 74 69 6e 67是testing的字符编码

对于byte的编码和字符串是相同的,

除此之外如果是一个嵌套类型的话其实也是一样,比如说

message Test3 {
  optional Test1 c = 3;
}

嵌套类型的编码

如果内置的test1的结构体是150的话

1a         // key (3<<3 | 2)
03         // 后面value的长度是3
08 96 01   // 是test1的编码,上文介绍过

repeat类型的编码

其次对于repeat类型也是一样的,

message Test4 {
  repeated int32 d = 4 [packed=true];
}

如果d是3、270和86942的编码,实际上会被编辑成如下形势:

22        // key (4<<3 | 2)
06        // 后面6个字节都是value
03        // 3的varint编码
8E 02     // 270的varint编码
9E A7 05  // 86942的varint编码

map的编码

实际上map的编码也是一样的map在protobuf的里面相当于是嵌入和结构体加上map

message Test5 {
  map[string]string e = 5
}
等同于
message Test5 {
  repeat Test6 e = 5 
}
message Test6 {
  string key = 1
  striing value = 2
}