Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
protocol
优缺点
优点
- 支持向后兼容和向前兼容
- 序列化及反序列化速度快
- 生成数据体积小
- 有代码生成机制
- 支持多种编程语言
缺陷
- 二进制格式导致可读性差
protobuf 与 Thrift
Thrift 和 Protobuf 的最大不同,在于 Thrift 提供了完整的 RPC 支持,包含了 Server/Client ,而 Protobuf 只包括了 stub 的生成器和格式定义。
编码原理
Base 128 Varints 编码
varint 是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。
varint 中的每个字节(最后一个字节除外)都设置了最高有效位(msb),这一位表示还会有更多字节出现。每个字节的低 7 位用于以 7 位组的形式存储数字的二进制补码表示,最低有效组首位。
如果用不到 1 个字节,那么最高有效位设为 0 ,如下面这个例子,1 用一个字节就可以表示,所以 msb 为 0.
1 | 0000 0001 |
如果需要多个字节表示,msb 就应该设置为 1 。例如 300,如果用 Varint 表示的话:
1 | 1010 1100 0000 0010 |
Message Structure 编码
protocol buffer 中 message 是一系列键值对。message 的二进制版本只是使用字段号(field’s number 和 wire_type)作为 key。每个字段的名称和声明类型只能在解码端通过引用消息类型的定义(即 .proto 文件)来确定。这一点也是人们常常说的 protocol buffer 比 JSON,XML 安全一点的原因,如果没有数据结构描述 .proto 文件,拿到数据以后是无法解释成正常的数据的。

由于采用了 tag-value 的形式,所以 option 的 field 如果有,就存在在这个 message buffer 中,如果没有,就不会在这里,这一点也算是压缩了 message 的大小了。
当消息编码时,键和值被连接成一个字节流。当消息被解码时,解析器需要能够跳过它无法识别的字段。这样,可以将新字段添加到消息中,而不会破坏不知道它们的旧程序。这就是所谓的 “向后”兼容性。
为此,线性的格式消息中每对的“key”实际上是两个值,其中一个是来自.proto文件的字段编号,加上提供正好足够的信息来查找下一个值的长度。在大多数语言实现中,这个 key 被称为 tag。

注意上图中,3 和 4 已经被废弃了,所以 wire_type 取值目前只有 0、1、2、5。
key 的计算方法是 (field_number << 3) | wire_type,换句话说,key 的最后 3 位表示的就是 wire_type。

举例,一般 message 的字段号都是 1 开始的,所以对应的 tag 可能是这样的:
1 | 0000 1000 |
末尾 3 位表示的是 value 的类型,这里是 000,即 0 ,代表的是 varint 值。右移 3 位,即 0001,这代表的就是字段号(field number)。tag 的例子就举这么多,接下来举一个 value 的例子,还是用 varint 来举例:
1 | 96 01 = 1001 0110 0000 0001 |
type 为 1 的编码方式类似于 utf-8 编码方式。
Non-varint Numbers
Non-varint 数字比较简单,double 、fixed64 的 wire_type 为 1,在解析时告诉解析器,该类型的数据需要一个 64 位大小的数据块即可。同理,float 和 fixed32 的 wire_type 为5,给其 32 位数据块即可。两种情况下,都是高位在后,低位在前。
说 Protocol Buffer 压缩数据没有到极限,原因就在这里,因为并没有压缩 float、double 这些浮点类型。
字符串
wire_type 类型为 2 的数据,是一种指定长度的编码方式:key + length + content,key 的编码方式是统一的,length 采用 varints 编码方式,content 就是由 length 指定长度的 Bytes。
wire_type 类型为 2 的数据,编码的时候会默认转换为 T-L-V (Tag - Length - Value)的形式。