什么是 Protobuf
Protobuf 是 Protocol Buffers 的简称,它是 Google 公司开发的一种数据描述语言,用于描述一种轻便高效的结构化数据存储格式,并于 2008 年对外开源。Protobuf 可以用于结构化数据串行化,或者说序列化。它的设计非常适用于在网络通讯中的数据载体,很适合做数据存储或 RPC 数据交换格式,它序列化出来的数据量少再加上以 K-V 的方式来存储数据,对消息的版本兼容性非常强,可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。开发者可以通过 Protobuf 附带的工具生成代码并实现将结构化数据序列化的功能。
Protobuf 中最基本的数据单元是 message,是类似 Go 语言中结构体的存在。在 message 中可以嵌套 message 或其它的基础数据类型的成员。
教程中将描述如何用 protocol buffer 语言构造你的 protocol buffer 数据,包括 .proto
文件的语法以及如何通过 .proto
文件生成数据访问类。教程中使用的是 proto3 版本的 protocol buffer 语言。
定义 Message
首先看一个简单的例子,比如说你定义一个搜索请求的 message,每一个搜索请求会包含一个搜索的字符串,返回第几页的结果,以及结果集的大小。在 .proto
文件中定义如下:
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
-
.proto
文件的第一行指定了使用proto3
语法。如果省略 protocol buffer 编译器默认使用proto2
语法。他必须是文件中非空非注释行的第一行。 -
SearchRequest
定义中指定了三个字段(name/value 键值对),每个字段都会有名称和类型。
指定字段类型
上面的例子中,所有的字段都是标量类型的两个整型 (page_number 和 result_per_page) 和一个字符串型(query)。不过你还可以给字段指定复合类型,包括枚举类型和其他 message 类型
指定字段编号
在 message 定义中每个字段都有一个唯一的编号,这些编号被用来在二进制消息体中识别你定义的这些字段,一旦你的 message 类型被用到后就不应该在修改这些编号了。注意在将 message 编码成二进制消息体时字段编号 1 -15 将会占用 1 个字节,16-2047 将占用两个字节。所以在一些频繁使用用的 message 中,你应该总是先使用前面 1 -15 字段编号。
你可以指定的最小编号是 1,最大是 2E29 – 1(536,870,911)。其中 19000 到 19999 是给 protocol buffers 实现保留的字段标号,定义 message 时不能使用。同样的你也不能重复使用任何当前 message 定义里已经使用过和预留的字段编号。
定义字段的规则
message 的字段必须符合以下规则:
- singular:一个遵循 singular 规则的字段,在一个结构良好的 message 消息体 (编码后的 message) 可以有 0 或 1 个该字段(但是不可以有多个)。这是 proto3 语法的默认字段规则。(这个理解起来有些晦涩,举例来说上面例子中三个字段都是 singular 类型的字段,在编码后的消息体中可以有 0 或者 1 个 query 字段,但不会有多个。)
- repeated:遵循 repeated 规则的字段在消息体重可以有任意多个该字段值,这些值的顺序在消息体重可以保持(就是数组类型的字段)
添加更多消息类型
在单个 .proto
文件中可以定义多个 message,这在定义多个相关 message 时非常有用。比如说,我们定义 SearchRequest
对应的响应 message SearchResponse
, 把它加到之前的 .proto
文件中。
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {...}
添加注释
.proto
文件中的注释和 C,C++ 的注释风格相同,使用 // 和 / … /
/* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response. */
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.
}
保留字段
当你删掉或者注释掉 message 中的一个字段时,未来其他开发者在更新 message 定义时就可以重用之前的字段编号。如果他们意外载入了老版本的 .proto
文件将会导致严重的问题,比如数据损坏、隐私泄露等。一种避免问题发生的方式是指定保留的字段编号和字段名称。如果未来有人用了这些字段标识那么在编译是 protocol buffer 的编译器会报错。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
proto 会生成什么代码
当使用 protocol buffer 编译器编译 .proto
文件时,编译器会根据你在 .proto
文件中定义的 message 类型生成指定编程语言的代码。生成的代码包括访问和设置字段值、格式化 message 类型到输出流,从输入流解析出 message 等。
- For C++, the compiler generates a
.h
and.cc
file from each.proto
, with a class for each message type described in your file. - For Java, the compiler generates a
.java
file with a class for each message type, as well as a specialBuilder
classes for creating message class instances. -
Python is a little different – the Python compiler generates a module with a static descriptor of each message type in your
.proto
, which is then used with a metaclass to create the necessary Python data access class at runtime. - For Go, the compiler generates a
.pb.go
file with a type for each message type in your file. - For Ruby, the compiler generates a
.rb
file with a Ruby module containing your message types. - For Objective-C, the compiler generates a
pbobjc.h
andpbobjc.m
file from each.proto
, with a class for each message type described in your file. - For C#, the compiler generates a
.cs
file from each.proto
, with a class for each message type described in your file. - For Dart, the compiler generates a
.pb.dart
file with a class for each message type in your file.
标量类型
.proto Type | Notes | C++ Type | Java Type | Python Type[2] | Go Type | Ruby Type | C# Type | PHP Type | Dart Type |
---|---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double | float | double | |
float | float | float | float | float32 | Float | float | float | double | |
int32 | 使用可变长度编码。编码负数的效率低 – 如果您的字段可能有负值,请改用 sint32。 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
int64 | 使用可变长度编码。编码负数的效率低 – 如果您的字段可能有负值,请改用 sint64。 | int64 | long | int/long[3] | int64 | Bignum | long | integer/string[5] | Int64 |
uint32 | 使用可变长度编码 | uint32 | int | int/long | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
uint64 | 使用可变长度编码. | uint64 | long | int/long | uint64 | Bignum | ulong | integer/string[5] | Int64 |
sint32 | 使用可变长度编码。签名的 int 值。这些比常规 int32 更有效地编码负数。 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
sint64 | 使用可变长度编码。签名的 int 值。这些比常规 int64 更有效地编码负数。 | int64 | long | int/long | int64 | Bignum | long | integer/string[5] | Int64 |
fixed32 | 总是四个字节。如果值通常大于 228,则比 uint32 更有效。 | uint32 | int | int/long | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
fixed64 | 总是八个字节。如果值通常大于 256,则比 uint64 更有效 | uint64 | long | int/long[3] | uint64 | Bignum | ulong | integer/string[5] | Int64 |
sfixed32 | 总是四个字节 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
sfixed64 | 总是八个字节 | int64 | long | int/long | int64 | Bignum | long | integer/string[5] | Int64 |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | |
string | 字符串必须始终包含 UTF- 8 编码或 7 位 ASCII 文本,且不能超过 232。 | string | String | str/unicode | string | String (UTF-8) | string | string | String |
bytes | 可以包含不超过 232 的任意字节序列。 | string | ByteString | str | []byte | String (ASCII-8BIT) | ByteString | string | List<int> |
默认值
当时一个被编码的 message 体中不存在某个 message 定义中的 singular 字段时,在 message 体解析成的对象中,相应字段会被设置为 message 定义中该字段的默认值。默认值依类型而定:
- 对于字符串,默认值为空字符串。
- 对于字节,默认值为空字节。
- 对于 bools,默认值为 false。
- 对于数字类型,默认值为零。
- 对于枚举,默认值是第一个定义的枚举值,该值必须为 0。
- 对于消息字段,未设置该字段。它的确切值取决于语言。有关详细信息,请参阅代码生成指南。
枚举类型
在定义消息类型时,您可能希望其中一个字段只有一个预定义的值列表中的值。例如,假设您要为每个 SearchRequest
添加 corpus
字段,其中 corpus
可以是 UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS 或 VIDEO。您可以非常简单地通过向消息定义添加枚举,并为每个可能的枚举值值添加常量来实现。
在下面的例子中,我们添加了一个名为 Corpus
的枚举类型,和一个 Corpus
类型的字段:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
如你所见,Corpus
枚举的第一个常量映射到了 0:所有枚举定义都需要包含一个常量映射到 0 并且作为定义的首行,这是因为:
- 必须有 0 值,这样我们就可以将 0 作为枚举的默认值。
- proto2 语法中首行的枚举值总是默认值,为了兼容 0 值必须作为定义的首行。
使用其他 Message 类型
可以使用其他 message 类型作为字段的类型,假设你想在每个 SearchResponse
消息中携带类型为 Result
的消息,
你可以在同一个 .proto
文件中定义一个 Result
消息类型,然后在 SearchResponse
中指定一个 Result
类型的字段。
message SearchResponse {repeated Result results = 1;}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
导入消息定义
在上面的示例中,Result
消息类型在与 SearchResponse
相同的文件中定义 – 如果要用作字段类型的消息类型已在另一个 .proto
文件中定义,该怎么办?
您可以通过导入来使用其他.proto 文件中的定义。要导入另一个.proto 的定义,请在文件顶部添加一个 import 语句:
import "myproject/other_protos.proto";
默认情况下,您只能使用直接导入的 .proto
文件中的定义。但是,有时你可能需要将 .proto
文件移动到新位置。现在,你可以在旧位置放置一个虚拟 .proto
文件,在文件中使用 import public
语法将所有导入转发到新位置,而不是直接移动 .proto
文件并在一次更改中更新所有调用点。任何导入包含 import public
语句的 proto
文件的人都可以传递依赖导入公共依赖项。例如
// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
编译器会在通过命令行参数 -I
或者 --proto-path
中指定的文件夹中搜索 .proto
文件,如果没有提供编译器会在唤其编译器的目录中进行搜索。通常来说你应该将 --proto-path
的值设置为你项目的根目录,并对所有导入使用完全限定名称。
使用 proto2 的消息类型
可以导入 proto2 版本的消息类型到 proto3 的消息类型中使用,当然也可以在 proto2 消息类型中导入 proto3 的消息类型。但是 proto2 的枚举类型不能直接应用到 proto3 的语法中。
嵌套消息类型
消息类型可以被定义和使用在其他消息类型中,下面的例子里 Result
消息被定义在 SearchResponse
消息中
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果你想在外部使用定义在父消息中的子消息,使用 Parent.Type
引用他们
message SomeOtherMessage {SearchResponse.Result result = 1;}
你可以嵌套任意多层消息
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
}
更新 Message
如果一个现存的消息类型不再满足你当前的需求 – 比如说你希望在消息中增加一个额外的字段 – 但是仍想使用由旧版的消息格式生成的代码,不用担心!只要记住下面的规则,在更新消息定义的同时又不破坏现有的代码就非常简单。
- 不要更改任何已存字段的字段编号。
- 如果添加了新字段,任何由旧版消息格式生成的代码所序列化的消息,仍能被依据新消息格式生成的代码所解析。你应该记住这些元素的默认值这些新生成的代码就能够正确地与由旧代码序列化创建的消息交互了。类似的,新代码创建的消息也能由旧版代码解析:旧版消息(二进制)在解析时简单地忽略了新增的字段,查看下面的未知字段章节了解更多。
- 只要在更新后的消息类型中不再重用字段编号,就可以删除该字段。你也可以重命名字段,比如说添加
OBSOLETE_
前缀或者将字段编号设置为reserved
,这些未来其他用户就不会意外地重用该字段编号了。
未知字段
未知字段是格式良好的协议缓冲区序列化数据,表示解析器无法识别的字段。例如,当旧二进制文件解析具有新字段的新二进制文件发送的数据时,这些新字段将成为旧二进制文件中的未知字段。
最初,proto3 消息在解析期间总是丢弃未知字段,但在 3.5 版本中,我们重新引入了未知字段的保留以匹配 proto2 行为。在版本 3.5 及更高版本中,未知字段在解析期间保留,并包含在序列化输出中。
映射类型
如果你想创建一个映射作为 message 定义的一部分,protocol buffers 提供了一个简易便利的语法
map<key_type, value_type> map_field = N;
key_type
可以是任意整数或者字符串(除了浮点数和 bytes 以外的所有标量类型)。注意 enum
不是一个有效的 key_type
。value_type
可以是除了映射以外的任意类型(意思是 protocol buffers 的消息体中不允许有嵌套 map)。
举例来说,假如你想创建一个名为 projects 的映射,每一个 Project
消息关联一个字符串键,你可以像如下来定义:
map<string, Project> projects = 3;
- 映射里的字段不能是 follow repeated 规则的(意思是映射里字段的值不能是数组)。
- 映射里的值是无序的,所以不能依赖映射里元素的顺序。
- 生成.proto 的文本格式时,映射按键排序。数字键按数字排序。
- 从线路解析或合并时,如果有重复的映射键,则使用最后看到的键。从文本格式解析映射时,如果存在重复键,则解析可能会失败。
- 如果未给映射的字段指定值,字段被序列化时的行为依语言而定。在 C ++,Java 和 Python 中字段类型的默认值会被序列化作为字段值,而其他语言则不会。
给 Message 加包名
你可以在 .proto
文件中添加一个可选的 package
符来防止消息类型之前的名称冲突。
package foo.bar;
message Open {...}
在定义 message 的字段时像如下这样使用 package 名称
message Foo {
...
foo.bar.Open open = 1;
...
}
package 符对生成代码的影响视编程语言而定
定义服务
如果想消息类型与 RPC(远程过程调用)系统一起使用,你可以在 .proto
文件中定义一个 RPC 服务接口,然后 protocol buffer 编译器将会根据你选择的编程语言生成服务接口代码和 stub,加入你要定义一个服务,它的一个方法接受 SearchRequest
消息返回 SearchResponse
消息,你可以在 .proto
文件中像如下示例这样定义它:
service SearchService {rpc Search (SearchRequest) returns (SearchResponse);
}
与 protocol buffer 一起使用的最简单的 RPC 系统是 gRPC
:一种由 Google 开发的语言和平台中立的开源 RPC 系统。gRPC
特别适用于 protocol buffer,并允许您使用特殊的 protocol buffer 编译器插件直接从 .proto
文件生成相关的 RPC 代码。
如果你不想使用gRPC
,可以使用自己实现的 RPC 系统,更多关于实现 RPC 系统的细节可以在 Proto2 Language Guide 中找到。
JSON 编解码
Proto3 支持 JSON 中的规范编码,使得在系统之间共享数据变得更加容易。在下表中逐个类型地列出了编码规则。
如果 JSON 编码数据中缺少某个值,或者其值为 null,则在解析为 protocol buffer 时,它将被解释为相应的默认值。如果字段在 protocol buffer 中具有默认值,则默认情况下将在 JSON 编码的数据中省略该字段以节省空间。编写编解码实现可以覆盖这个默认行为在 JSON 编码的输出中保留具有默认值的字段的选项。
proto3 | JSON | JSON example | Notes |
---|---|---|---|
message | object | {"fooBar": v, "g": null,…} |
生成 JSON 对象。消息字段名称会被转换为小驼峰并成为 JSON 对象键。如果指定了 json_name 字段选项,则将指定的值用作键。解析器接受小驼峰名称(或由 json_name 选项指定的名称)和原始 proto 字段名称。null 是所有字段类型的可接受值,并被视为相应字段类型的默认值。 |
enum | string | "FOO_BAR" |
使用 proto 中指定的枚举值的名称。解析器接受枚举名称和整数值。 |
map<K,V> | object | {"k": v, …} |
所有键都将被转换为字符串 |
repeated V | array | [v, …] |
null 会被转换为空列表[] |
bool | true, false | true, false |
|
string | string | "Hello World!" |
|
bytes | base64 string | "YWJjMTIzIT8kKiYoKSctPUB+" |
JSON 值将是使用带填充的标准 base64 编码编码为字符串的数据。接受带有 / 不带填充的标准或 URL 安全 base64 编码。 |
int32, fixed32, uint32 | number | 1, -10, 0 |
JSON value will be a decimal number. Either numbers or strings are accepted. |
int64, fixed64, uint64 | string | "1", "-10" |
JSON value will be a decimal string. Either numbers or strings are accepted. |
float, double | number | 1.1, -10.0, 0, "NaN","Infinity" |
JSON value will be a number or one of the special string values “NaN”, “Infinity”, and “-Infinity”. Either numbers or strings are accepted. Exponent notation is also accepted. |
Any | object |
{"@type": "url", "f": v, …} |
If the Any contains a value that has a special JSON mapping, it will be converted as follows: {"@type": xxx, "value": yyy} . Otherwise, the value will be converted into a JSON object, and the "@type" field will be inserted to indicate the actual data type. |
Timestamp | string | "1972-01-01T10:00:20.021Z" |
Uses RFC 3339, where generated output will always be Z-normalized and uses 0, 3, 6 or 9 fractional digits. Offsets other than “Z” are also accepted. |
Duration | string | "1.000340012s", "1s" |
Generated output always contains 0, 3, 6, or 9 fractional digits, depending on required precision, followed by the suffix “s”. Accepted are any fractional digits (also none) as long as they fit into nano-seconds precision and the suffix “s” is required. |
Struct | object |
{…} |
Any JSON object. See struct.proto . |
Wrapper types | various types | 2, "2", "foo", true,"true", null, 0, … |
Wrappers use the same representation in JSON as the wrapped primitive type, except that null is allowed and preserved during data conversion and transfer. |
FieldMask | string | "f.fooBar,h" |
See field_mask.proto . |
ListValue | array | [foo, bar, …] |
|
Value | value | Any JSON value | |
NullValue | null | JSON null | |
Empty | object | {} | An empty JSON object |
生成代码
要生成 Java,Python,C ++,Go,Ruby,Objective- C 或 C#代码,你需要使用 .proto
文件中定义的消息类型,你需要在 .proto
上运行 protocol buffer 编译器protoc
。如果尚未安装编译器,请下载该软件包并按照 README 文件中的说明进行操作。对于 Go,还需要为编译器安装一个特殊的代码生成器插件:你可以在 GitHub 上的 golang/protobuf 项目中找到这个插件和安装说明。
编译器像下面这样唤起:
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 --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
-
IMPORT_PATH
指定了在解析import
命令时去哪里搜索.proto
文件,如果忽略将在当前工作目录进行查找,可以通过传递多次--proto-path
参数来指定多个 import 目录,他们将会按顺序被编译器搜索。-I=IMPORT_PATH
是--proto_path
的简短形式。 -
你可以提供一个或多个输出命令:
-
--cpp_out
generates C++ code inDST_DIR
. See the C++ generated code reference for more. -
--java_out
generates Java code inDST_DIR
. See the Java generated code reference for more. -
--python_out
generates Python code inDST_DIR
. See the Python generated code reference for more. -
--go_out
generates Go code inDST_DIR
. See the Go generated code reference for more. -
--ruby_out
generates Ruby code inDST_DIR
. Ruby generated code reference is coming soon! -
--objc_out
generates Objective-C code inDST_DIR
. See the Objective-C generated code reference for more. -
--csharp_out
generates C# code inDST_DIR
. See the C# generated code reference for more. -
--php_out
generates PHP code inDST_DIR
. See the PHP generated code reference for more.
-
- 必须提供一个或多个.proto 文件作为输入。可以一次指定多个.proto 文件。虽然文件是相对于当前目录命名的,但每个文件必须存在于其中一个 IMPORT_PATH 中,以便编译器可以确定其规范名称。