Proto3使用指南
这篇指南讲述如何使用Protocol Buffers来结构化你的Protocol Buffer数据,包括.proto
文件语法以及如何从.proto
文件生成你的访问类型。本文主要涵盖了proto3的语法,proto2的语法参见Proto2 Language Guide。
这是一篇参考教程 -- 本文中诸多功能的分步示例,详见tutorial。
目录
- 定义消息类型
- 标量类型
- 默认值
- 枚举
- 使用其他消息类型
- 嵌套类型
- 更新消息类型
- 未知字段
- Any
- Oneof
- Maps
- 包
- 定义服务
- Json Mapping
- 可选项
- 编译生成
定义消息类型
首先来看一个非常简单的例子。假设你想定义一个搜素请求的消息格式,其中每个搜索请求都包含一个检索字段、特定的结果页(你感兴趣的结果所在的页面)以及每个页面的结果数量。你可以使用下面的.proto
文件来定义消息类型。
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
- 文件的第一行指明你要使用
proto3
语法:如果你不指定的话,protocol buffer编译器将默认你使用的是proto2
。这必须写在文件中非空、非注释的第一行。 SearchRequest
消息明确定义了三个字段(键值对),对应每一条你想包含在这个消息类型中的数据。每个字段都有一个名称和类型。
指明字段类型
在上面的例子中,所有的字段都是明确类型的:两个integers(page_number
和result_per_page
)和一个string(query
)。当然,你也可以将你的字段指定成复合类型,包括枚举和其他消息类型。
分配字段序号
如你所见,在定义的消息中的每个字段都有一个唯一的序号。这些序号用来在二进制消息结果中标识你的字段,而且一旦使用了消息类型,就不应该再变动。注意,字段序号在1到15的范围内占用1个字节来编码,包括字段序号和字段类型(详见Protocol Buffer Encoding)。字段序号在16到2047范围内占两个字节。所以你应该为经常使用的消息元素保留1到15的序号。切记为将来可能新增的常用元素预留一些空间。
你能使用的最小字段序号为1,最大为\(2^{29}-1\),或 536,870,911。但是你不能使用19000到19999(FieldDescriptor::kFirstReservedNumber
到FieldDescriptor::kLastReservedNumber
),因为它们是为Protocol Buffers实现预留的,如果在你的.proto
文件中使用了,protocol buffer编译器会报错。同样,你也不能使用任何以前保留的字段序号。
标明字段规则
消息字段可以遵循下列规则之一:
- singular:符合语法规则的消息可以拥有0个或1个该字段(但不能超过1个)。这是proto3默认的字段规则。
- repeated:在符合语法规则的消息中,该字段可以重复任意次数(包括0次)。重复变量的顺序将被保留。
在proto3中,repeated
字段的标量数字默认使用packed
编码。关于packed
编码,详见Protocol Buffer Encoding。
新增更多消息类型
在单个.proto
文件中可以定义多个消息类型。这在你定义多个关联的消息类型时非常有用,例如,如果你想定义应答消息格式来满足你的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.
}
保留字段
如果你通过完全删除或注释一个字段来更新消息类型,那么此后的用户在更新他们自己的类型时将可以重用该字段的序号。如果之后他们使用旧版的.proto
时,会引起严重的问题,包括数据损坏、隐私bug等。避免给问题的途径之一就是指明你要删除的字段需要(或者会在JSON序列化时会引起问题的名称)是reserved
的,这样将来用户在使用这些字段时protocol buffer编译器就会告警。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
注意,不能在同一个reserved
语句中混用字段名称和字段序号。
你的.proto
文件会生成什么?
当你使用protocol buffer 编译器时,编译器会根据你选定的语言来生成你.proto
文件中描述的消息类型,包括获取和设置字段的值,序列化你的消息到一个输出流中,从输入流中解析你的消息。
- 对C++,编译器会根据每个
.proto
文件生成一个.h
和一个.cc
,你定义的每个消息类型都会变成一个类。 - 对Java,编译器会生成一个
.java
文件,包含每个消息类型的类,同时还会指明一个Builder
类来创建消息类的实例。 - Python有点不同,Python编译器会生成一个模块,包含你
.proto
文件每一消息类型的静态描述,之后在运行时通过基类来创建必要的Python数据访问类。 - 对于Go,编译器会生成一个
.pb.go
的文件,包含文件中每个消息类型。 - 对于Ruby,编译器会生成一个
.rb
的文件,包含消息类型的Ruby模块。 - 对于Objective-C,编译器会根据每个
.proto
文件生成一个pbobjc.h
和一个pbobjc.m
文件,你定义的每个消息类型都会变成一个类。 - 对C#,编译器会根据每个
.proto
文件生成一个.cs
文件,你定义的每个消息类型都会变成一个类。 - 对Dart,编译器生成一个
.pb.dart
文件,文件中定义的每个消息类型都会变成一个类。
你可以在之后的教程中找关于对应语言的APIs的使用。更多APIs细节,详见API reference。
标量类型
标量字段可以是下面类型中的任意一个。下表展示了.proto
文件中标明的类型,以及在自动生成的类中对应的类型:
.proto 类型 | 说明 | C++类型 | Java类型 | Python类型\(^{[2]}\) | Go类型 | Ruby类型 | C#类型 | PHP类型 | Dart类型 |
---|---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double | float | double | |
float | float | float | float | float32 | Folat | 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\(^{[1]}\) | int/long\(^{[3]}\) | uint32 | Fixnum or Bignum(as required) | uint | integer | int |
uint64 | 使用可变长度编码。 | uint64 | long\(^{[1]}\) | int/long\(^{[3]}\) | uint64 | Bignum | ulong | integer/string\(^{[5]}\)}$ | int64 |
sint32 | 使用可变长度编码。有符号整数。编码负数比普通int32高效。 | int32 | int | int | int32 | Fixnum or Bignum(as required) | int | integer | int |
sint64 | 使用可变长度编码。有符号整数。编码负数比普通int64高效。 | int64 | long | int/long\(^{[3]}\) | int64 | Bignum | long | integer/string\(^{[5]}\) | int64 |
fixed32 | 4字节。如果变量大于2\(^{28}\)比uint32高效。 | uint32 | int\(^{[1]}\) | int | int32 | Fixnum or Bignum(as required) | int | integer | int |
fixed64 | 8字节。如果变量大于2\(^{56}\)比uint64高效。 | uint64 | long\(^{[1]}\) | int/long\(^{[3]}\) | uint64 | Bignum | ulong | integer/string\(^{[5]}\) | int64 |
sfixed32 | 4字节 | uint32 | int\(^{[1]}\) | int | int32 | Fixnum or Bignum(as required) | int | integer | int |
sfixed64 | 8字节 | uint64 | long\(^{[1]}\) | int/long\(^{[3]}\) | uint64 | Bignum | ulong | integer/string\(^{[5]}\) | int64 |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | |
string | 必须是UTF-8编码或7-bit的ASCII文本,长度不能大于2\(^{32}\) | string | String | str/unicode\(^{[4]}\) | string | String(UTF-8) | string | string | String |
bytes | 可以包含任何长度不超过232的字节序列 | string | ByteString | str | []byte | String(ASCII-8BIT) | ByteString | string | List<int> |
更多编码细节,详见Protocol Buffer Encoding。
\(^{[1]}\)在Java中,无符号32位和64位整数使用它们的有符号对应的整数表示,顶部的整数只存储在符号位中。
\(^{[2]}\)在所有场景中,给字段设置值时将调用类型检查来确保有效。
$^{[3]}$64位或无符号32位整数会解码成对应的长度,但如果在设置字段时给定的是int,也可以解码为int。
\(^{[4]}\)Python strings会解码成unicode,但如果给定的是ASCII string,会被解码成str。
\(^{[5]}\)Integer用在64位机器上,string用于32位机器。
默认值
在解析消息时,如果编码的消息不包含特定的singular元素,则解析对象中的相应字段将设置为该字段的默认值。这些默认值与类型有关:
- 对于string,默认值为空字符串。
- 对于bytes,默认值是空bytes。
- 对于bool,默认值为false。
- 对于数字类型,默认值为0。
- 对于枚举,默认值为第一个定义的枚举变量,其值必须为0。
- 对于消息字段,未设置。他都值因语言不同而不同,详见generated code guide。
对于repeated字段,其默认值为空(通常是目标语言的空列表)。
对于标量消息字段来说,一旦消息被解析,就无法判断该字段是真实被设为默认值(例如bool变量被设为false)还是就没有设置:在定义消息类型时需要牢记这一点。例如,如果你不想在默认情况向执行某种行为,那么就不要用boolean被设置为false
来切换这些行为。同时,如果一个标量消息字段被设为它的默认值,那么改值在传输时将不会被序列化。
在生成代码时,在你所选的语言中默认值如何工作,详见generated code guide。
枚举
当你在定义消息类型时,你可能想让它的字段只是用预定义列表中的一个值。例如,假如你想给SearchRequest
添加一个corpus
字段,其值可以是UNIVERSAL
、WEB
、IMAGE
、LOCAL
、NEWS
、PRODUCT
或者VIDEO
。通过enum
,在你的消息中定义一个包含每个可能值得枚举变量,可以很简单地做的。
在下面的例子中,我们添加了一个名为Corpus
的enum
类型,它包含了所有可能的值,和一个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作为数字默认值。
- 0值必须作为第一个元素,以便于proto2兼容,它的第一枚举变量总是默认值。
你可以定义别买来给不同的枚举常量分配相同的值。这样就需要你将allow_alias
设置为true
,否则编译器在发现别名时会产生错误消息。
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}
枚举常量取值范围必须在32位正整数之间。因为enum
变量在传输时使用varint encoding,负数是低效且不推荐的。你可以如上述例子一样在消息定义内部定义enum
,也可以在外部定义,这样的enum
可以被.proto
文件中的其它消息定义使用。你也可以使用MessageType.EnumType
语法将一个消息的enum
声明类型作为另一个不同消息的字段类型。
当你编译一个使用了enum
的.proto
文件时,生成的代码中会包含Java或C++对应的enum
,针对Python的特定的EnumDescriptor
类,用来在执行生成的类中创建一系列包含数值的符号常量。
在反序列化期间,无法识别的enum值将保留在消息中,尽管在反序列化消息时如何表示该值取决于语言。在支持指定符号范围之外使用值的开放枚举类型的语言,如c++和Go,未知的枚举值只是作为其基础整数表示形式存储。在具有封闭枚举类型的语言,如Java,枚举中的大小写用于表示无法识别的值,并且可以使用特殊的访问器访问底层整数。在任何一种情况下,如果消息被序列化,未被识别的值仍将与消息一起序列化。
在你所选的语言中,带有enum
的消息如何工作,详见generated code guide。
保留变量
如果你通过完全删除或注释一个字段来更新枚举类型时,那么之后的用户在更新他们自己的类型时将可以重用该字段的序号。如果之后他们使用旧版的.proto
时,会引起严重的问题,包括数据损坏、隐私bug等。避免给问题的途径之一就是指明你要删除的字段需要(或者会在JSON序列化时会引起问题的名称)是reserved
的,这样将来用户在使用这些字段时protocol buffer编译器就会告警。你可以指明你要保留的数字到可能的最大值(通过max
关键字)得范围。
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
注意,不能在同一个reserved
语句中混用字段名称和字段序号。
使用其他消息类型
你也可以使用其它消息类型作为字段类型。例如,假如你想在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
文件中导入它们来使用这些定义。要使用其它.proto
的定义,你需要在你的文件头部导入声明:
import "myproject/other_protos.proto";
默认情况下,你只能使用直接导入的.proto
文件。但有时候你可能需要将.proto
文件移到新的路径。相比于直接移到.proto
文件然后更新所有用到它的地方,现在你可以在旧的路径下放置一个虚拟的.proto
文件,以便使用import public概念将所有导入转发到新位置:
// 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_path
标志设为项目的根目录,并且使用全路径导入。
使用proto2消息类型
可以在你的proto3消息中导入并使用proto2的消息类型,反之亦可。然而proto2的枚举不能再proto3中直接使用(可以在导入的proto2的消息中使用)。
嵌套类型
你可以在一个消息类型中定义并使用其它的消息类型,就像下面的例子 -- 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;
}
}
}
更新消息类型
如果已存在的消息类型不再满足你的需求 -- 例如,你想在消息格式中添加新的字段,但还想使用就格式生成的代码。别担心!在不破坏你现有代码的基础上更新消息类型很简单。只需要记住下面的规则:
- 不要修改已有字段的序号。
- 如果你新增了字段,任何使用旧格式序列化的消息仍能被新生成的代码解析。你应该记住这些元素的默认值,以便新代码可以正确地与旧代码生成的消息进行交互。类似地,由新代码创建的消息可以由旧代码解析:旧的二进制文件在解析时简单地忽略新字段。有关详细信息,请参阅未知字段部分。
- 字段可以被移除,只要它的序号不再被你更新的消息类型使用。你可以重命名字段,或者添加前缀“OBSOLETE_”,或者保留字段号,这样
.proto
的未来用户就不会意外地重用该号码。 int32
、uint32
、int64
、uint64
和bool
都是兼容的——这意味着你可以将一个字段从这些类型中的一种更改为另一种,而不会中断向前或向后兼容。如果从连线中解析出一个不适合相应类型的数字,那么你将获得与在c++中将该数字强制转换为该类型相同的效果(例如,如果将64位数字读取为int32,那么它将被截断为32位)。sint32
和sint64
是相互兼容的,但不与其它整型兼容。string
和bytes
兼容,bytes
与UTF-8
兼容。- 如果字节包含消息的编码版本,则嵌入的消息与
bytes
兼容。 fixed32
与sfixed32
、fixed64
和sfixed64
兼容。- 在传输格式中
enum
与int32
、uint32
、int64
、uint64
兼容(注意变量不兼容的部分将被截断)。然而需要留意的是在消息反序列化时,客户端代码会被区别对待:例如,尽管无法识别的proto3中的enum
类型会被保存在消息中,但是在消息反序列化时,它是如何表示这取决于语言。int字段总会保留它的值。 - 修改new
oneof
成员中的单个变量是安全且二进制兼容的。如果您确定没有代码一次设置多个字段,那么将多个字段移动到一个新的字段中可能是安全的。将任何字段移动到现有字段中都是不安全的
未知字段
未知字段是protocol buffer在序列化数据时无法解析的数据。例如,当旧的二进制代码在解析带有新字段的新二进制代码发送的数据时,这些新字段将成为旧二进制代码中的未知字段。
最初,在解析时proto3总是丢弃未知字段,但在3.5版本之后,重新引入了未知字段的保留来匹配proto2的行为。在3.5及之后的版本中,在解析时未知字段会被保留并将其包含的序列化的输出中。
Any
Any
消息类型允许你在没有.proto
定义的情况下将你的消息类型作为嵌入类型使用。Any包含作为bytes
的任意序列化消息,以及充当全局惟一标识符并解析为该消息类型的URL。要使用Any
类型,你需要导入google/protobuf/any.proto
。
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
给定消息类型的默认URL类型为type.googleapis.com/packagename.messagename
。
不同的语言实现以类型安全的方式来提供打包和解包Any
变量的运行时库帮助程序。例如,在Java中,Any类型使用特定的pack()
和unpack
访问,而在C++中,使用PackFrom()
和UnpackTo()
方法:
// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);
// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const Any& detail : status.details()) {
if (detail.Is<NetworkErrorDetails>()) {
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
... processing network_error ...
}
}
目前,用于处理Any类型的运行时库都在开发中。
Oneof
如果有有一个包含多个字段的消息,在同一时间最多只能设置一个字段,那么你可以通过使用oneof特性强制执行此行为并节省内存。
除所有字段共享同一个Oneof内存和最多同时只能设置一个字段外,Oneof字段与常规字段类似。设置oneof字段中的任何成员都将自动清除其它成员。根据你所使用的的语言不同,你可以使用(必要时)特定的case()
或WhichOneof()
方法来检查Oneof中的哪个变量被设置。
使用 Oneof
要在你的.proto
文件中定义一个Oneof字段,你可以在的oneof
关键字后跟上你的oneof名称,就如下面的test_oneof
:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
之后你可以添加你的oneof字段到oneof定义中。除了不能使用repeated
字段,你可以使用任意字段。
在你生成的代码中,oneof字段有着与常规字段一样的getters
和setters
。必要时,你也可以使用特定的方法来确定oneof中的哪个值被设置。关于你所选语言的oneof API,详见API reference。
Oneof 特性
- 设置oneof字段中的任何成员都将自动清除其它成员。如果你设置了多个字段,那么只有最后设置的字段保留变量。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // Will clear name field.
CHECK(!message.has_name());
- 如果解析器在网络中遇到同一个Oneof的多个成员,在解析消息时仅使用最后看到的成员。
- 不能使用
repeated
。 - oneof字段使用反射 APIs。
- 如果你设置oneof字段为默认值(比如设施int32字段为0),该字段的“case”将被设置,且在传输时被序列化。
- 如果你使用C++,请确保你的代码不会引起内存崩溃。下面的代码会引起崩溃,因为在调用
set_name()
方法时sub_message
已经删除。
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name"); // Will delete sub_message
sub_message->set_... // Crashes here
- 同样是在C++中,如果你使用
Swap()
来交换两个带有oneofs的消息,每个消息会以另一个的oneof case结束:在下面的例子中,msg1
将拥有sub_message
,msg2
将拥有name
。
SampleMessage msg1;
msg1.set_name("name");
SampleMessage msg2;
msg2.mutable_sub_message();
msg1.swap(&msg2);
CHECK(msg1.has_sub_message());
CHECK(msg2.has_name());
向后兼容问题
在新增或移除oneof字段时要慎重。如果检测到oneof的返回值为None/Not_SET
,可能意味着这个oneof尚未设置或已在不同版本的oneof中设置。无法区分这两者之间的不同,因为无法确定传输中的未知字段是否是给oneof的成员。
Tag重用问题
- 移入/移出字段到oneof:在消息序列化和解析后,你可能会丢失部分消息(有些字段被清理了)。但是,你可以安全地将单个字段移动到一个新的oneof字段中,如果知道只设置了一个字段,则可以移动多个字段。
- 删除一个oneof字段后有添加:在消息序列化和解析后,可能会将你当前的设置清零。
- 切割/合并 oneof:与移动常规字段问题相似。
Maps
如果你想创建一个关联映射作为你的数据定义的一部分,protocol buffers提供了一个方便快捷的语法:
map<key_type, value_type> map_field = N;
这里的key_type
可以是任意的integral或string类型(即除了浮点型和bytes
外的所有标量类型)。注意enum
不是有效的key_type
。value_type
可以是除了其它Map外的所有类型。
那么,假如你想创建一个项目映射,每个项目关联一个string键,定义如下:
map<string, Project> projects = 3;
- Map字段不可以是
repeated
。 - 映射值的网络格式排序和映射迭代排序是未定义的,所以在特定的排序中你不能依赖你的映射元素组成。
- 为
.proto
生成文本格式时,映射根据键排序。数字键按数字大小排序。 - 从网络解析/合并时,如果键有多个副本,那么使用最后遇到的键。当从文本格式中解析映射时,如果键存在副本,则可能解析失败。
- 如果你仅提供了Map字段的键而没有提供值,字段序列化时的行为因语言而异。在C++、Java和Python中,值会被序列化为该类型的默认值,在其它语言中并不会被序列化。
Proto3现已全面支持生成Map API。关于不同语言的Map API,详见API reference。
向后兼容
在网络中,Map语法等效如下示例,因此即便proto buffers实现不支持的Maps也能处理你的数据:
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
所有的protocol buffers实现都必须能产生和接受上述定义所接受的数据。
包
你可以在.proto
文件中添加package
说明符来避免协议消息类型键的名称冲突。
package foo.bar;
message Open { ... }
之后在定义你的消息类型字段时,你可以使用package
说明符:
message Foo {
...
foo.bar.Open open = 1;
...
}
package
说明符影响生成代码的方式依赖于你所选的语言:
- 在C++中,生成的类会被打包到C++的命名空间中。例如:
Open
位于foo::bar
命名空间中。 - 在Java中,
package
作为Java包使用,除非在.proto
文件中额外提供option java_package
。 - 在Python中,
package
指令会被忽略,Python模块是根据它们在文件系统中的位置来组织的。 - 在Go中,
package
将被用作Go的包名,除非在.proto
文件中额外提供option go_package
。 - 在Ruby中,生成的类会被打包嵌入到Ruby的命名空间中,并转换为所需的Ruby大小写样式(第一个字母大写;如果第一个字符不是字母,PB_是前缀)。例如:
Open
位于foo::bar
命名空间中。 - 在C# 中,
package
在被转换为PascalCase
后作为命名空间使用,除非在.proto
文件中额外提供option csharp_namespace
。
包和名称解析
Protocol buffer语言中的类型名称解析类似于C++:首先在最内层查找,之后是下一层,一次类推,每个包在其父包的“内部”。“.”开头(例如,.foo.bar.Baz
)意味着从最外层作用域开始查找。
Protocol buffer编译器通过导入的.proto
文件来解析所有的类型名称。即使有着不同的作用域规则,各语言生成的代码也知道如何每种类型该如何使用。
定义服务
如果你现在RPC(远程调用)系统中使用你的消息类型,你可以在.proto
文件中定义RPC服务接口,之后protocol buffer编译器会生成所选语言的服务接口代码和存根。比如,你要定义一个RPC服务,它使用你的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实现中使用protocol buffer。详见Proto2 Language Guide。
也有一些正在进行的第三方项目来为protocol buffer开发RPC实现。有关我们所知项目的链接列表,请参阅third-part add-ons wiki page。
Json Mapping
Proto3支持Json编码规范,这使得在不同系统间共享数据变得更加方便。在下面的表中,将逐个类型地描述编码。
如果一个值在JSON编码中丢失或为null
,在解析到protocol buffer时它会被解释为合适的默认值。如果protocol buffer中的字段有默认值,那么在Json编码的数据中将默认省略该字段,以节省空间。在Json编码输出中,实现可以提供带有默认字段的选项。
proto3 | Json | Json示例 | 备注 |
---|---|---|---|
message | object | 生成Json对象。消息字段名称被映射为lowerCamelCase并成为Json对象的键。如果指定了json_name字段选项,则指定的值将被作为键使用。解析器既接受lowerCamelCase名称(或使用json_name指定的名称),也接受原生的proto字段名称。所有字段类型都可接受null,并被视为该类型的默认值。 | |
enum | string | "FOO_BAR" | 使用proto中指定的enum值名称。解析器既接受枚举名称,也接受整数值。 |
map<K,V> | object | 所有的键都被转换成string。 | |
repeated V | array | [v, ...] | null被当做空列表[]。 |
bool | true,false | true,false | |
string | string | "Hello World!" | |
bytes | base64 string | "YWJjMTIzIT8kKiYoKSctPUB+" | Json值会变成使用添加padding的标准base64编码的string。标准的或url安全的base64编码,带/不带padding也都可以接受。 |
int32,fixed32,uint32 | number | 1,-10,0 | Json值会变成十进制的数字。数字或string都可被接受。 |
int64,fixed64,uint64 | string | "1","-10" | Json值会变成十进制的string。数字或string都可被接受。 |
float,double | number | 1.1,-10.0,0,"NaN","Infinity" | Json值会变成数字或"NaN"、"Infinity"、"-Infinity"其中之一。数字或string都可被接受。指数表示法也被接受。 |
Any | object | 如果Any包含的值有特定的Json映射,它将被转换为如下格式:{"@type": xxx, "value": yyy}。否则,该值会被转换为Json对象,且”@type“字段会被插入以指示实际数据类型。 | |
Timestamp | string | "1972-01-01T10:00:20.021Z" | 使用RFC 3339,其生成的输出总是Z-normalized后的,并使用0、3、6或9位小数。除“Z”以外的偏移量也可以接受。 |
Duration | string | "1.000340012s","1s" | 根据所需的精度,生成的输出总是包含0、3、6或9位小数,跟后缀”s“。只要符合纳秒精度和后缀“s”的要求,任何小数(也可以没有)都可以接受。 |
Struct | object | 任意的Json对象。参见struc.proto | |
Wrapper types | various types | 2,"2","foo",true,"true",null,0,... | 包装器使用与包装的原始类型相同的JSON表示,但在数据转换和传输期间允许并保留null。 |
FieldMask | string | "f.fooBar,h" | 参见field_mask.proto |
ListValue | array | [foo,bar, ...] | |
Value | value | 任意的Json值 | |
NullValue | null | Json null | |
Empty | object | {} | 任意的空Json对象。 |
JSON 选项
Proto3的Json实现可支持下列选项:
- 带默认值得空字段:默认情况下,在proto3 JSON输出中会省略具有默认值的字段。实现可以提供一个选项来覆盖此行为,并使用其默认值输出字段。
- 忽略未知类型:默认情况下,Proto3 Json解析器会驳回未知字段,但在解析时可以提供选项来忽略未知字段。
- 使用proto字段来代替lowerCamelCase名称:默认情况下,proto3 Json的输出应该将字段名转换为lowerCamelCase并作为Json名称使用。该实现可以通过提供选项来使用proto字段作为Json名称。Proto3 Json解析器被设计为可同时接受转换后的lowerCamelCase名称和proto字段名称。
- 指明enum值作为整数而不是string:默认情况下,在Json输出中使用枚举值的名称。通过选项可指定使用数字代替枚举值。
可选项
proto
文件中的各个声明可以用许多选项进行注释。选项不会改变声明的总体含义,但可能影响在特定上下文中处理它的方式。可用选项的完整列表在google/protobuf/description.proto
中定义。
有些选项是文件级别的,意味着它们应该写在开头位置,而不是在消息、枚举或服务定义中。有些选项是消息级别的,意味着它们应该写在消息定义中。有些选项是字段选项,意味着它们应该写在字段定义中。选项也可以写在枚举类型、枚举值、服务类型和服务方法中,然而,当前不存在对Any有用的选项。
下面是一些常用的选项:
java_package
(文件级):这个包你想用来生成Java类。如果.proto
文件中没有额外给出java_package
选项,默认情况下使用proto包(在.proto
文件中使用package关键字指明的)。然而通常情况下proto包并不适合Java包,因为不希望proto包以反向域名展开。如果不生成Java代码,此项无效。
option java_package = "com.example.foo";
java_multiple_files
(文件级):使顶级消息、枚举和服务在包级别定义,而不是在以.proto
文件命名的外部类中定义。
option java_multiple_files = true;
java_outer_classname
(文件级):希望生成的最外层Java类的类名(以及文件名)。如果在.proto
文件中没有指定显式的java_outer_classname
,那么将通过将.proto
文件名转换为驼峰写法(比如foo_bar.proto
变为FooBar.java
)来构造类名。如果不生成Java代码,则此选项无效。
option java_outer_classname = "Ponycopter";
-
optimize_for
(文件级):可被设为SPEED
、CODE_SIZE
或LITE_RUNTIME
。这将通过以下方式影响C++和Java代码生成(也可能影响第三方生成):SPEED
(默认):Protocol buffer编译器会为你的消息类型生成序列化、解析和其它常用操作的代码。此代码高度优化。CODE_SIZE
:Protocol buffer编译器会生成最小的类,其依赖共享、反射的代码来实现序列化、解析和其它操作。因此生成的代码比SPEED
小很多,但操作也会比较慢。Classes仍会实现与SPEED
模式相同的公共API。这种模式在包含大量.proto
文件且不是所有文件都需要快速生成的应用程序中最有用。LITE_RUNTIME
:Protocol buffer编译器依赖“轻量的”运行时库(使用libprotobuf-lite
而不是libprotobuf
)。lite运行时比完整的库小得多(大约小一个数量级),但是忽略了某些特性,比如描述符和反射。这对于在受限平台(如移动电话)上运行的应用程序尤其有用。编译器仍然会像在SPEED
模式下那样生成所有方法的快速实现。生成的类将仅用每种语言实现MessageLite
接口,该接口只提供完整Message
接口方法的一个子集。
option optimize_for = CODE_SIZE;
-
cc_enable_arenas
(文件级):为C++代码生成启用arena allocation。 -
objc_class_prefix
(文件级):为.proto
文件生成的所有Objective-C类设置前缀。没有默认值。你应该使用苹果推荐的前缀,即3-5个大写字母。注意苹果保留所有的2个字母的前缀。 -
deprecated
(文件级):如果设置为true
,则表示该字段已被废弃,新代码不应使用该字段。在大多数语言中,该选项并没有实际效果。在Java中,会变成一个@Deprecated
注释。将来,其它语言的代码生成器可能会在字段访问器上生成弃用注释,这将使得编译器在尝试使用该字段时发出警告。如果该字段将不再使用且你也不希望有新的用户使用它,那么可以考虑使用保留语句替换字段声明。
int32 old_field = 6 [deprecated=true]
自定义选项
Protocol buffer也允许你定义并使用自定义的选项。这是大多数人用不到的高级功能。如果你真的想创建自定义选项,详见Proto2 语言指南。注意,用扩展来创建自定义选项,这是proto3中唯一允许使用的自定义选项。
编译生成
要从.proto
文件中包括你定义的消息类型的Java、Python、C++、Go、Ruby、Objective-C或C#代码,你需要允许protocol buffer编译器protoc
。如果你还没安装编译器,可从这里下载并根据README编译安装。对于Go,你还需要安装特定的生成插件:在这里你可以找到它。
Protocol编译器使用如下:
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
选项指明,它们将按顺序检索。--proto_path
可简写为-I=IMPORT_PATH
。 -
你可以提供一个或多个输出命令:
--cpp_out
在DST_DIR
目录中生成C++代码。详见C++ 生成代码引用。--java_out
在DST_DIR
目录中生成Java代码。详见Java 生成代码引用。--python_out
在DST_DIR
目录中生成Python代码。详见Python 生成代码引用。--go_out
在DST_DIR
目录中生成Go代码。详见Go 生成代码引用。--ruby_out
在DST_DIR
目录中生成Ruby代码。详见Ruby 生成代码引用。--objc_out
在DST_DIR
目录中生成Object-C代码。详见Object-C 生成代码引用。--csharp_out
在DST_DIR
目录中生成C#代码。详见C# 生成代码引用。--php_out
在DST_DIR
目录中生成PHP代码。详见PHP 生成代码引用。
作为额外的便利,如果
DST_DIR
以.zip
或.jar
,编译器将生成指定名称的ZIP格式的压缩包。.jar
输出还将根据Java JAR规范的要求提供一个清单文件。注意如果输出文件已存在,那么它将被重写,编译器并不会生成一个新的副本。 -
你必须提供一个或者多个
.proto
文件作为输入。多个.proto
文件可以一次指定。虽然这些文件是相对于当前目录命名的,但是每个文件必须驻留在IMPORT_PATH
中的一个,以便编译器可以确定它的规范名称。
更多信息,参见这里。