protobuf 使用详解

简介

protobuf(Google Protocol Buffers), 官方文档对 protobuf 的定义: protocol buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法, 可用于数据通信协议和数据存储等, 它是 Google 提供的一个具有高效协议数据交换格式工具库, 是一种灵活、高效和自动化机制的结构数据序列化方法。 相比 XML, 有编码后体积更小, 编解码速度更快的优势;相比于 Json, Protobuf 有更高的转化效率, 时间效率和空间效率都是 JSON 的 3-5 倍。

protobuf 优缺点

优点

  1. 性能好, 效率高
    • 时间和空间开销更优
  2. 有代码生成机制
    • 编写 proto 文件, 可以通过编译器生成对应语言的类
  3. 支持向后兼容和向前兼容
    • 当客户端和服务器同时使用一个协议时, 服务器在协议中增加一个字节, 并不会影响客户端的使用
  4. 支持多种编程语言
    • 在 Google 官方发布的源代码中包含了
    • C++
    • C#
    • Dart
    • Go
    • Java
    • Kotlin
    • Python
  5. 二进制格式让信息读取有门槛

缺点

  1. 二进制格式导致可读性差
  2. 缺乏自描述
  3. 通用性相比 json 和 XML 差一点

proto 文件

proto 版本

proto 有 proto2 和 proto3 两个版本, 有些语言不支持 proto3, 如: lua

proto2 和 proto3 在语法和使用上有些许不同。 总的来说, proto3 比 proto2 支持更多语言但更简洁。 去掉了一些复杂的语法和特性, 更强调约定而弱化语法。

消息体

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
message SearchRequest {
required string query = 1;
optional int32 pageNumber = 2;
optional int32 resultPerPage = 3;
}

enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}

enum 可以定义在 message 中, 也可以定义在外面。

proto 文件批量编译

1
2
3
4
for %%i in (*.proto) do (
.\protoc.exe --java_out=.\java\ %%i
)
pause

注意事项

  1. syntax = “proto2” 表示用的是 proto2, syntax = “proto3” 表示用的是 proto3
  2. package 表示在的包目录
  3. option java_outer_classname 表示生成的 java 文件名
  4. message 中引用自定义的 message 类型, 被引用的 message 需要放在上方
    原因: 某些语言是顺序读取, 不支持乱序读取, 如: lua

语法

字段域类型(Specifying Field Rules)

You specify that message fields are one of the following:

  1. required: a well-formed message must have exactly one of this field.
  2. optional: a well-formed message can have zero or one of this field (but not more than one).
  3. repeated: this field can be repeated any number of times (including zero) in a well-formed message. The order of the repeated values will be preserved.

也可以用map: map<string, int32> param = 1;

字段值类型(Scalar Value Types)

.proto Type Notes C++ Type Java Type Python Type2 Go Type
double double double float *float64
float float float float *float32
int32 Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. int32 int int *int32
int64 Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. int64 long int/long3 *int64
uint32 Uses variable-length encoding. uint32 int1 int/long3 *uint32
uint64 Uses variable-length encoding. uint64 long1 int/long3 *uint64
sint32 Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. int32 int int *int32
sint64 Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. int64 long int/long3 *int64
fixed32 Always four bytes. More efficient than uint32 if values are often greater than 228. uint32 int1 int/long3 *uint32
fixed64 Always eight bytes. More efficient than uint64 if values are often greater than 256. uint64 long1 int/long3 *uint64
sfixed32 Always four bytes. int32 int int *int32
sfixed64 Always eight bytes. int64 long int/long3 *int64
bool bool boolean bool *bool
string A string must always contain UTF-8 encoded or 7-bit ASCII text. string String unicode (Python 2) or str (Python 3) *string
bytes May contain any arbitrary sequence of bytes. string ByteString bytes []byte

You can find out more about how these types are encoded when you serialize your message in Protocol Buffer Encoding.

  • [1] In Java, unsigned 32-bit and 64-bit integers are represented using their signed counterparts, with the top bit simply being stored in the sign bit.
  • [2] In all cases, setting values to a field will perform type checking to make sure it is valid.
  • [3] 64-bit or unsigned 32-bit integers are always represented as long when decoded, but can be an int if an int is given when setting the field. In all cases, the value must fit in the type represented when set. See [2].

proto2 和 proto 区别

  1. 该文件的第一行指定您正在使用 proto3 语法: 如果您不这样做, protocol buffer 编译器将假定您使用的是 proto2。 这必须是文件的第一个非空、非注释行。
  2. proto3 取消了 proto2 的 required, 而 proto3 的 singular 就是 proto2 的 optional
  3. proto3 repeated 标量数值类型默认 packed, 而 proto2 默认不开启。
  4. proto3 增加了 Kotlin, Ruby, Objective-C, C#, Dart 的支持
  5. proto2 可以选填 default, 而 proto3 只能使用系统默认的。 也就是说, 默认值全部是约定好的, 而不再提供指定默认值的语法。
  6. 在字段被设置为默认值的时候, 该字段不会被序列化。 这样可以节省空间, 提高效率。 但这样就无法区分某字段是根本没赋值, 还是赋值了默认值。 这在 proto3 中问题不大, 但在 proto2 中会有问题。
    比如, 在更新协议的时候使用 default 选项为某个字段指定了一个与原来不同的默认值, 旧代码获取到的该字段的值会与新代码不一样。
  7. 枚举类型的第一个字段必须为 0 。
  8. proto3 在 3.5 版本之前会丢弃未知字段。 但在 3.5 版本中, 重新引入了未知字段的保留以匹配 proto2 行为。 在 3.5 及更高版本中, 未知字段在解析过程中保留并包含在序列化输出中。
    移除了对扩展的支持, 新增了 Any 类型;
  9. proto3 移除了 proto2 的扩展, 新增了 Any(仍在开发中)和 JSON 映射。

编码方式

消息经过序列化后会成为一个二进制数据流,该流中的数据为一系列的 Key-Value 对,定义好结构体的优势就是不用多余的数据来分隔不同的键值对。Key 用来标识具体的 field,在解包的时候,Protocol Buffer 根据 Key 就可以知道相应的 Value 应该对应于消息中的哪一个 field。

Key 定义:(field_number << 3) | wire_type

field_number 代表在 .proto 中定义的编号,115 用一个字节,162047 用两个字节,结合公式 (field_number << 3) | wire_type ,如果 filed_number 大于等于 16,两个字节共 16 位,去掉移位的 3 位,去掉两个字节中第一个比特位(msb, most significant bit: 最高有效位),总共 16 个比特位只有 16-5=11 个比特位用来表示 Key,所以 Key 的 filed_number 要小于 2^11== 2048。

更大的以此类推,主要就是看一个字节的 msb 是否为 1,最大可以为 2^29 - 1。

wire_type 表示该数据的类型,有 Vaint、64-bit、Length-delimi、Start group、End group、32-bit共 6 中类型,具体可查看 官方文档

Varint

了解 protobuf 首先就要了解 Varint,是它的一大核心,变长整数存储。长整数存储多的位数,短整数存储少的位数,来减少空间的浪费。除了最后一字节外每字节第一位都是 most significant big(msb),表示是否后面是否还有字节表示该整数。例如:

0000 0001 就表示 1

1010 1100 0000 0010 第一个字节第一位为 1 表示后面还有数据,直到字节第一位为 0(这里就是第二个字节 0000 0010),将字节顺序逆向,变为 0000 0010 010 1100:100101100(300)

ZigZag

有符号整数则使用 ZigZag 编码方式,用无符号的整数同时代表正负两种数:

Signed Original Encoded As
0 0
-1 1
1 2
-2 3
2147483647 4294967294
-2147483648 4294967295

这样就大大减少了占用的位数,算法使用:

1
2
(n << 1) ^ (n >> 31) sint32
(n << 1) ^ (n >> 63) sint64

float double

不压缩,多少位就多少位存储。

string

string 则是在 Value 中多加了一个或多个字节表示长度(msb标识),剩下的内容才是真正的值。

repeated

这里介绍压缩率更高的 Packed Repeated Fields。

在 2.1.0 版本以后,protocol buffers 引入了该种类型,其与 repeated 字段一样,只是在末尾声明了 [packed=true]。类似 repeated 字段却又不同。在 proto3 中 Repeated 字段默认就是以这种方式处理。

例如有如下 message 类型:

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

构造一个 Test4 字段,并且设置 repeated 字段 d 3 个值:3,270 和 86942,编码后:

1
2
3
4
5
6
7
8
9
22 // tag 0010 0010(field number 010 0 = 4, wire type 010 = 2)

06 // payload size (设置的length = 6 bytes)

03 // first element (varint 3)

8E 02 // second element (varint 270)

9E A7 05 // third element (varint 86942)

形成了 Tag - Length - Value - Value - Value …… ,增加了压缩率。

参考资料

  1. 【超详细】Protobuf(Protocol Buffers)proto3 与 proto2 的区别
  2. kryo vs avro vs protobuf vs thrift vs jce
  3. Programming Guides / Encoding