Protobuf介绍

gRPC系列文章(二)

Table of Contents

Prorobuf是Google开发的一种跨语言、跨平台、可扩展的用于序列化数据协议。Protobuf为gRPC提供序列化方案,那为什么茫茫人海中要用Protobuf呢?这篇文章将来回答这些问题。

为什么用Protobuf?

Protobuf事实上是一种序列化方案,它做的事情与Json、XML都是一样的。但是Json和XML是以人的视角来设计的,因此需要更复杂的表达模式,而Protobuf则是面向机器的,可读性几乎为零,但是带来的极大的性能提升1

上面展示了一个合法的Json数据,潜在的影响传输效率的问题包括:

  • Key的低效性:
    • Key作为索引占用空间过大,比如"id"可以用"1"代替;
    • 在数组中key会重复出现;
  • 以字符串形式传输,比如一个数字123,用int8表示仅占用1byte空间,而用字符串方式时则需要占用3byte空间;
  • 额外的控制符号,如"["、"{"等;
  • 结构复杂,解析Json字符串需要CPU资源。

Protobuf的改进包括:

  • 在proto文件规定字段编号,在序列化中使用编号代替字段名;
  • 数据以二进制方式传输,不存在int转字符串的问题;
  • 无额外控制符号;
  • 数据格式简单,不需要太多的CPU资源。

编码

Varint

Varint是一种可变长的编码方式,但不需要指定数据长度,那么是如何知道数据什么时候结束呢?它征用了最高位作为数据结束标识,称为MSB(在下图中红色区域),如果MSB为1则表示数据还没有结束,如果为0则表示数据结束。

数字31659的Varint编码过程

Varint编码方式适用于小数据的变长编码,标识位的长度根据数据量的变化而变化。用Varint编码方式编码1bytes数据需要2bytes空间,编码8bytes数据需要10bytes空间,在这种场景下:

  • 如果采用定长8bytes编码方式(int64),不足8bytes的数据也会占用8bytes空间,换句话说小数据不需要这么大空间也必须占用8bytes空间;
  • 如果使用length表示长度的变长编码方式,表示8bytes数据需要1bytes标识位,共计9bytes空间。

看起来像是使用length表示长度的变长编码方式更优,但事实上Varint表示越小数据占用空间越小,而且在一般场景中小数的使用率更高。综上,Varint编码对于不大的数据具有良好的压缩效果,但对于大数字和负数效果都不尽如人意。

ZigZag

负数的符号位(最高位)是1,这就导致了int64(-1)都要使用8bytes数据传输,这种机制对Varint编码方式并不友好。Protobuf针对负数的特性引入了sint32sint64代替int32int64,这里sint32sint64都是借助ZigZag编码实现了对负数的优化2

ZigZag编码用int映射为uint的方法解决了负数数字过大的问题,映射表如下所示:

ZigZag编码数字 表示数字
0 0
1 -1
2 1
3 -2
4 2
... ...
4294967294 2147483647
4294967295 -2147483648

ZigZag方案把负数的最高位问题均摊了,正数在编码后比原来的大一倍,但是换来的是数值较大的负数占用尽可能小的空间。总之,在负数较多时考虑使用sint32或者sint64,否则还是使用int32int64

结构

Protobuf在底层的主要结构就"T-V"和"T-L-V"两种,其中T是Tag,L是Length,V是Value,"T-L-V"用于表示变长的数据,比如数组、嵌套结构等,"T-V"既可以用于表示定长数据,也可以表示变长数据。下面的图片列举出了几个比较有代表性的结构。

典型结构

Tag指定了field_number和wire_type。field_number代表数据Key,需要在proto文件中指定。wire_type则是表示数据类型,下面列出了目前支持的全部类型3

| wire_type | Value长度 | 编码方式 | 储存方式 | 典型数据结构 | | - | - | - | - | | 0 | 1-10 bytes | T-V | Varint | int32, uint32, sint32, int64, uint64, sint64, bool, enum | | 1 | 8 bytes | T-V | 64-bit | fixed64, sfixed64, double | | 2 | L bytes | T-L-V | Length Delimited | strings, bytes, embedded message, packed repeated fields | | 5 | 4 bytes | T-V | 32-bit | fixed32, sfixed32, float |

wire_type目前占用3 bits空间(表示范围为0-7),field_number使用varint编码,默认空间为4 bits(表示范围为0-15),如果编号超过15那么Tag的长度会自动+1 byte,所以对于频繁使用的字段field_number最好设置在0-15之间。

Length指定了Value的长度,其本身也是可变长结构,使用Varint进行编码。

  1. https://zhuanlan.zhihu.com/p/149821222

  2. https://izualzhy.cn/protobuf-encode-varint-and-zigzag

  3. wire_type值为2和3的情况目前已弃用,所以不列在表格中。