

























Protocol Buffers 是 google 的一种数据交换的格式,它独立于语言,独立于平台。google 提供了多种语言的实现:java、c#、c++、go 和 python,每一种实现都包含了相应语言的编译器以及库文件。由于它是一种二进制的格式,比使用 xml 进行数据交换快许多。可以把它用于分布式应用之间的数据通信或者异构环境下的数据交换。作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域。
至于protobuf是什么、使用场景、有什么好处,本文不做说明,这里将会为大家介绍怎么用 protobuf 来定义我们的交互协议,包括 .proto 的语法以及如何根据proto文件生成相应的代码。本文基于proto3,读者也可以点击了解proto2
首先我们来定义一个 Search 请求,在这个请求里面,我们需要给服务端发送三个信息:
于是我们可以这样定义:
1 | // 指定使用proto3,如果不指定的话,编译器会使用proto2去编译 |
一个 proto 文件可以定义多个 message ,比如我们可以在刚才那个 proto 文件中把服务端返回的消息结构也一起定义:
1 | message SearchRequest { |
message 可以嵌套定义,比如 message 可以定义在另一个 message 内部
1 | message SearchResponse { |
定义在 message 内部的 message 可以这样使用:
1 | message SomeOtherMessage { |
在刚才的例子之中,我们使用了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字节编码, 如果变量经常大于$ 2^{28} $ 的话,会比uint32高效 | uint32 | int | int | int32 | Fixnum or Bignum (as required) | uint | integer |
| fixed64 | 8字节编码, 如果变量经常大于$ 2^{56} $ 的话,会比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 |
补充说明:
关于标准值类型,还可以参考Scalar Value Types
如果你想了解这些数据是怎么序列化和反序列化的,可以点击 Protocol Buffer Encoding 了解更多关于protobuf编码内容。
每一个变量在message内都需要自定义一个唯一的数字Tag,protobuf会根据Tag从数据中查找变量对应的位置,具体原理跟protobuf的二进制数据格式有关。Tag一旦指定,以后更新协议的时候也不能修改,否则无法对旧版本兼容。
Tag的取值范围最小是1,最大是$ 2^{29} $-1,但 19000~19999 是 protobuf 预留的,用户不能使用。
虽然 Tag 的定义范围比较大,但不同 Tag 也会对 protobuf 编码带来一些影响:
使用频率高的变量最好设置为1 ~ 15,这样可以减少编码后的数据大小,但由于Tag一旦指定不能修改,所以为了以后扩展,也记得为未来保留一些 1 ~ 15 的 Tag
在 proto3 中,可以给变量指定以下两个规则:
singular:0或者1个,但不能多于1个repeated:任意数量(包括0)当构建 message 的时候,build 数据的时候,会检测设置的数据跟规则是否匹配
在proto2中,规则为:
用//表示注释开头,如
1 | message SearchRequest { |
上面我们说到,一旦 Tag 指定后就不能变更,这就会带来一个问题,假如在版本1的协议中,我们有个变量:
1 | int32 number = 1; |
在版本2中,我们决定废弃对它的使用,那我们应该如何修改协议呢?注释掉它?删除掉它?如果把它删除了,后来者很可能在定义新变量的时候,使新的变量 Tag = 1 ,这样会导致协议不兼容。那有没有办法规避这个问题呢?我们可以用 reserved 关键字,当一个变量不再使用的时候,我们可以把它的变量名或 Tag 用 reserved 标注,这样,当这个 Tag 或者变量名字被重新使用的时候,编译器会报错
1 | message Foo { |
当解析 message 时,如果被编码的 message 里没有包含某些变量,那么根据类型不同,他们会有不同的默认值:
注意,收到数据后反序列化后,对于标准值类型的数据,比如bool,如果它的值是 false,那么我们无法判断这个值是对方设置的,还是对方压根就没给这个变量设置值。
在 protobuf 中,我们也可以定义枚举,并且使用该枚举类型,比如:
1 | message SearchRequest { |
枚举定义在一个消息内部或消息外部都是可以的,如果枚举是 定义在 message 内部,而其他 message 又想使用,那么可以通过 MessageType.EnumType 的方式引用。定义枚举的时候,我们要保证第一个枚举值必须是0,枚举值不能重复,除非使用 option allow_alias = true 选项来开启别名。如:
1 | enum EnumAllowingAlias { |
枚举值的范围是32-bit integer,但因为枚举值使用变长编码,所以不推荐使用负数作为枚举值,因为这会带来效率问题。
在proto语法中,有两种引用其他 proto 文件的方法: import 和 import public,这两者有什么区别呢?下面举个例子说明:
1 | // my.proto |
1 | // first.proto |
1 | // second.proto |
升级更改 proto 需要遵循以下原则
Any可以让你在 proto 文件中使用未定义的类型,具体里面保存什么数据,是在上层业务代码使用的时候决定的,使用 Any 必须导入 import google/protobuf/any.proto
1 | import "google/protobuf/any.proto"; |
Oneof 类似union,如果你的消息中有很多可选字段,而同一个时刻最多仅有其中的一个字段被设置的话,你可以使用oneof来强化这个特性并且节约存储空间,如
1 | message LoginReply { |
这样,name 和 age 都是 LoginReply 的成员,但不能给他们同时设置值(设置一个oneof字段会自动清理其他的oneof字段)。
protobuf 支持定义 map 类型的成员,如:
1 | map<key_type, value_type> map_field = N; |
使用 map 要注意:
为了防止不同消息之间的命名冲突,你可以对特定的.proto文件提指定 package 名字。在定义消息的成员的时候,可以指定包的名字:
1 | package foo.bar; |
1 | message Foo { |
Options 分为 file-level options(只能出现在最顶层,不能在消息、枚举、服务内部使用)、 message-level options(只能在消息内部使用)、field-level options(只能在变量定义时使用)
1 | option java_package = "com.example.foo"; |
这个其实和gRPC相关,详细可参考:gRPC, 这里做一个简单的介绍
要定义一个服务,你必须在你的 .proto 文件中指定 service
1 | service RouteGuide { |
然后在我们的服务中定义 rpc 方法,指定它们的请求的和响应类型。gRPC 允许你定义4种类型的 service 方法
客户端使用 Stub 发送请求到服务器并等待响应返回,就像平常的函数调用一样,这是一个阻塞型的调用
1 | // Obtains the feature at a given position. |
客户端发送请求到服务器,拿到一个流去读取返回的消息序列。客户端读取返回的流,直到里面没有任何消息。从例子中可以看出,通过在响应类型前插入 stream 关键字,可以指定一个服务器端的流方法
1 | // Obtains the Features available within the given Rectangle. Results are |
客户端写入一个消息序列并将其发送到服务器,同样也是使用流。一旦客户端完成写入消息,它等待服务器完成读取返回它的响应。通过在请求类型前指定 stream 关键字来指定一个客户端的流方法
1 | // Accepts a stream of Points on a route being traversed, returning a |
双方使用读写流去发送一个消息序列。两个流独立操作,因此客户端和服务器可以以任意喜欢的顺序读写:比如, 服务器可以在写入响应前等待接收所有的客户端消息,或者可以交替的读取和写入消息,或者其他读写的组合。每个流中的消息顺序被预留。你可以通过在请求和响应前加 stream 关键字去制定方法的类型
1 | // Accepts a stream of RouteNotes sent while a route is being traversed, |
使用 protoc 工具可以把编写好的 proto 文件“编译”为Java, Python, C++, Go, Ruby, JavaNano, Objective-C,或C#代码, protoc 可以从点击这里进行下载。protoc 的使用方式如下:
1 | protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto |
参数说明:
--proto_path此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。