1、protobuf格式

protobuf的message中有很多字段,每个字段的格式为: 

修饰符 字段类型 字段名 = 域号;

       在序列化时,protobuf按照TLV的格式序列化每一个字段,T即Tag,也叫Key;V是该字段对应的值value;L是Value的长度,如果一个字段是整形,这个L部分会省略。 

       Key的序列化格式是按照message中字段后面的域号与字段类型来转换。转换公式如下:

(field_number << 3) | wire_type 

上面的field_number就是域号, wire_type与字段的类型有关,

2、protobuf编码 

write_type编码方式type存储方式
0Varint(负数使用Zigzag辅助)int32、int64、uint32、uint64、sint32、sint64、bool、enumT-V
164-bitfixed、sfixed64、doubleT-V
2Length-delimistring、bytes、embedded、messages、packed repeated fieldsT-L-V
3(弃用)Start groupGroups(deprecated)弃用
4(弃用)End groupGroups(deprecated)弃用
532-bitfixed32、sfixed32、floatT-V

如:required string name=1;这里的name字段的域号为1,在protobuf中规定:

  • 如果域号在[1,15]范围内,会使用一个字节表示Key;
  • 如果域号大于等于16,会使用两个字节表示Key;

key编码完成后,该字节的第一个比特位表示后一个字节是否与当前字节有关系,即:

  • 如果第一个比特位为1,表示有关,即连续两个字节都是Key的编码;
  • 如果第一个比特位为0,表示Key的编码只有当前一个字节,后面的字节是Length或者Value;

注意:protobuf中的域号定义要小于2048 ,原因为,最大的域号即2个字节16个比特位表示key,去掉位移的三位,还剩下13位,再去掉两个字节开头的第一个用来表示是否存在关系的比特位,即16-3-2=11,最后只有11位参与计算,二进制计算后2^11== 2048 ,所以域号不得超过2048

了解了以上的那些,我们看看,上述我们编写的案例,算法是如何实现的呢?

varint编码

上述我们的案例中,出现了int32类型,对应的压缩算法为varint,我们看下age=300,这个值是如何序列化的

      可以看出来,我们首先将300转为二进制,结果为100101100,由于当前是int32,所以不足32位,高位全部补0,即为00000000000000000000000100101100,接着第二步,从低位到高位取7位,8位是一个字节,当前的最高位为标志位,如果下一个字节内还有非0得数值(即有意义存在),则最高位补1,如果没有最高位补0,当最高位为0后,压缩存储结束,从age=300,我们可以看出来,取7位则是0101100,由于后一个字节中还存在值,所以最高位补1,则为10101100,而下一个字节则从第8位(低位到高位)开始,继续获取7个字节,则为0000010,由于后续的一个字节中,不存在有意义的值,则最高位补0,代表后续不存在有意义的值了,不需要继续压缩,则为00000010,也就是说原本32个比特位的数值,现在只有16个比特位,4个字节压缩到了2个字节。

字符串压缩

       在Protobuf中存储字符串格式,使用的T-L-V存储方式,标识符Tag采用Varint编码,字节长度Length采用Varint编码,string类型字段值采用UTF-8编码方式存储,所以tag得值为1 <<3 | 2 =10,L的值存储为00000011,即为3,而V的存储,把每一个字符按照UTF-8的编码后的字节流数组,分别为77 105 99,而在Protobuf编码后的字节流则是按照如图的顺序,所以打印出来的结果如上的10 3 77 105 99 16 -84 2

3、protobuf格式

(1)protobuf消息格式

message xxx {
  // 字段规则:required -> 字段只能也必须出现 1 次
  // 字段规则:optional -> 字段可出现 0 次或1次
  // 字段规则:repeated -> 字段可出现任意多次(包括 0)
  // 类型:int32、int64、sint32、sint64、string、32-bit ....
  // 字段编号:0 ~ 536870911(除去 19000 到 19999 之间的数字)
  字段规则 类型 名称 = 字段编号;
}

(2)编译方法

// $SRC_DIR: .proto 所在的源目录
// --cpp_out: 生成 c++ 代码
// $DST_DIR: 生成代码的目标目录
// xxx.proto: 要针对哪个 proto 文件生成接口代码

protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/xxx.proto

4、总结

基于Protobuf序列化原理分析,为了有效降低序列化后数据量的大小,可以采用以下措施:

  (1)字段标识号(Field_Number)尽量只使用1-15,且不要跳动使用 Tag是需要占字节空间的。如果Field_Number>16时,Field_Number的编码就会占用2个字节,那么Tag在编码时就会占用更多的字节;如果将字段标识号定义为连续递增的数值,将获得更好的编码和解码性能

  (2)若需要使用的字段值出现负数,请使用sint32/sint64,不要使用int32/int64。 采用sint32/sint64数据类型表示负数时,会先采用Zigzag编码再采用Varint编码,从而更加有效压缩数据

  (3)对于repeated字段,尽量增加packed=true修饰 增加packed=true修饰,repeated字段会采用连续数据存储方式,即T - L - V - V -V方式

 

参考: