目录
Protocol Buffers 是 google 的一种数据交换的格式,它独立于语言,独立于平台。提供了多种语言的实现:java、c#、c++、go 和 python,每一种实现都包含了相应语言的编译器以及库文件。由于它是一种二进制的格式,比使用 xml 进行数据交换快许多。
本文主要介绍proto3的使用语法。
proto3语法
定义一个 Message
首先我们来定义一个 Search 请求,在这个请求里面,我们需要给服务端发送三个信息:
- query:查询条件
- page_number:你想要哪一页数据
- result_per_page:每一页有多少条数据
于是我们可以这样定义:
// 指定使用proto3,如果不指定的话,编译器会使用proto2去编译
syntax = "proto3";
message SearchRequests {
// 定义SearchRequests的成员变量,需要指定:变量类型、变量名、变量Tag
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
定义多个 message 类型
一个 proto 文件可以定义多个 message ,比如我们可以在刚才那个 proto 文件中把服务端返回的消息结构也一起定义:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
repeated string result = 1;
}
message 可以嵌套定义,比如 message 可以定义在另一个 message 内部
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
定义在 message 内部的 message 可以这样使用:
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
定义变量类型
在刚才的例子之中,我们使用了2个标准值类型
: string 和 int32,除了这些标准类型之外,变量的类型还可以是复杂类型,比如自定义的枚举
和自定义的 message
这里我们把标准类型列举一下protobuf内置的标准类型以及跟各平台对应的关系:
.proto | 说明 | C++ | Java | Python | Go | Ruby | C# | PHP |
---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double | float | |
float | float | float | float | float32 | Float | float | float | |
int32 | 使用变长编码,对负数编码效率低,如果你的变量可能是负数,可以使用sint32 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer |
int64 | 使用变长编码,对负数编码效率低,如果你的变量可能是负数,可以使用sint64 | int64 | long | int/long | int64 | Bignum | long | integer/string |
uint32 | 使用变长编码 | uint32 | int | int/long | uint32 | Fixnum or Bignum (as required) | uint | integer |
uint64 | 使用变长编码 | uint64 | long | int/long | uint64 | Bignum | ulong | integer/string |
sint32 | 使用变长编码,带符号的int类型,对负数编码比int32高效 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer |
sint64 | 使用变长编码,带符号的int类型,对负数编码比int64高效 | int64 | long | int/long | int64 | Bignum | long | integer/string |
fixed32 | 4字节编码, 如果变量经常大于 的话,会比uint64高效 | uint64 | long | int/long | uint64 | Bignum | ulong | integer/string |
sfixed32 | 4字节编码 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer |
sfixed64 | 8字节编码 | int64 | long | int/long | int64 | Bignum | long | integer/string |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | |
string | 必须包含utf-8编码或者7-bit ASCII text | string | String | str/unicode | string | String (UTF-8) | string | string |
bytes | 任意的字节序列 | string | ByteString | str | []byte | String (ASCII-8BIT) | ByteString | string |
分配Tag
每一个变量在message内都需要自定义一个唯一的数字Tag,protobuf会根据Tag从数据中查找变量对应的位置,具体原理跟protobuf的二进制数据格式有关。Tag一旦指定,以后更新协议的时候也不能修改,否则无法对旧版本兼容。
Tag的取值范围最小是1,最大是-1,但 19000~19999 是 protobuf 预留的,用户不能使用。
虽然 Tag 的定义范围比较大,但不同 Tag 也会对 protobuf 编码带来一些影响:
- 1 ~ 15:单字节编码
- 16 ~ 2047:双字节编码
使用频率高的变量最好设置为1 ~ 15,这样可以减少编码后的数据大小,但由于Tag一旦指定不能修改,所以为了以后扩展,也记得为未来保留一些 1 ~ 15 的 Tag
指定变量规则
在 proto3 中,可以给变量指定以下两个规则:
singular
:0或者1个,但不能多于1个repeated
:任意数量(包括0)
当构建 message 的时候,build 数据的时候,会检测设置的数据跟规则是否匹配
在proto2中,规则为:
- required:必须有一个
- optional:0或者1个
- repeated:任意数量(包括0)
注释
用//
表示注释开头,如
message SearchRequest {
string query = 1;
int32 page_number = 2; // Which page number do we want
int32 result_per_page = 3; // Number of results to return per page
}
保留变量不被使用
上面我们说到,一旦 Tag 指定后就不能变更,这就会带来一个问题,假如在版本1的协议中,我们有个变量:
int32 number = 1;
在版本2中,我们决定废弃对它的使用,那我们应该如何修改协议呢?注释掉它?删除掉它?如果把它删除了,后来者很可能在定义新变量的时候,使新的变量 Tag = 1 ,这样会导致协议不兼容。那有没有办法规避这个问题呢?我们可以用 reserved
关键字,当一个变量不再使用的时候,我们可以把它的变量名或 Tag 用 reserved
标注,这样,当这个 Tag 或者变量名字被重新使用的时候,编译器会报错
message Foo {
// 注意,同一个 reserved 语句不能同时包含变量名和 Tag
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
默认值
当解析 message 时,如果被编码的 message 里没有包含某些变量,那么根据类型不同,他们会有不同的默认值:
- string:默认是空的字符串
- byte:默认是空的bytes
- bool:默认为false
- numeric:默认为0
- enums:定义在第一位的枚举值,也就是0
- messages:根据生成的不同语言有不同的表现,参考generated code guide
注意,收到数据后反序列化后,对于标准值类型的数据,比如bool,如果它的值是 false,那么我们无法判断这个值是对方设置的,还是对方压根就没给这个变量设置值。
定义枚举 Enumerations
在 protobuf 中,我们也可以定义枚举,并且使用该枚举类型,比如:
message SearchRequest {
string query = 1;
int32 page_number = 2; // Which page number do we want
int32 result_per_page = 3; // Number of results to return per page
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
}
Corpus corpus = 4;
}
枚举定义在一个消息内部或消息外部都是可以的,如果枚举是 定义在 message 内部,而其他 message 又想使用,那么可以通过 MessageType.EnumType
的方式引用。定义枚举的时候,我们要保证第一个枚举值必须是0,枚举值不能重复,除非使用 option allow_alias = true
选项来开启别名。如:
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
枚举值的范围是32-bit integer,但因为枚举值使用变长编码,所以不推荐使用负数作为枚举值,因为这会带来效率问题。