0.说明
ProtoBuf3语法指南,
又称为proto3,
是谷歌的Protocol Buffers第3个版本。
本文基于官方英文版本翻译,
加上了自己的理解少量修改,
一共分为上下两部分。
1.序言
本指南描述了如何使用protocol buffer语言来构造protocol buffer数据,
包括编写.proto文件的语法,
以及如何从.proto文件生成数据访问类。
本文涵盖了protocol buffer语言的proto3版本语法,
而有关proto2版本语法的信息,
请参阅proto2语法指南。
下面将通过一个一个示例,
逐步展示文档中描述的许多特性。
2.定义一个消息类型
先来看一个非常简单的例子。
假设你想定义一个"搜索请求"的消息格式,
每一个请求含有一个查询字符串、
你感兴趣的查询结果所在的页数,
以及每一页查询结果的条数。
下面是用于定义消息类型的.proto文件:
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
-文件的第一行指定了你正在使用proto3语法,如果你没有指定,编译器会默认使用proto2。这个必须是文件的非空非注释的第一个行。
-SearchRequest消息定义了3个字段(名称/值对),每个字段对应了需要在消息中承载的数据,且都有一个名字和一种类型。
2.1.指定字段类型
在上面的例子中,所有字段都是标量类型:
两个整数类型(page_number和result_per_page)
和一个字符串类型(query)。
还可以为字段指定复合类型,
包括枚举和其他消息类型。
2.2.分配字段编号
消息定义中的每个字段都有一个唯一的数字编号。
这些编号用于在消息的二进制格式中标识字段,
一旦编号被使用就不应该再改变。
注意:[1,15]之内的编号在编码的时候会占用一个字节,
包括字段编号和字段类型,
可以在Protocol Buffer Encoding中找到更多有关信息,
[16,2047]之内的编号则占用2个字节。
因此应该为频繁出现的消息元素保留数字[1,15]之内的编号,
记住为将来可能添加的频繁出现的元素预留一些编号。
最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。
不可以使用其中的[19000-19999]的标识号,
从FieldDescriptor::kFirstReservedNumber到FieldDescriptor::kLastReservedNumber,
Protobuf协议实现中对这些进行了预留。
如果非要在.proto文件中使用这些预留的编号,
编译时就会报警。
同样你也不能使用早期保留的编号。
2.3.指定字段规则
消息字段的修饰符必须是如下之一:
- singular:一个格式良好的消息中,应该有0个或者1个这种字段(但是不能超过1个)。这是proto3语法的默认字段规则。
- repeated:一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序将保持不变。相当于Java中的List列表。
在proto3中,标量数值类型的重复字段默认使用packed编码。
可以在Protocol Buffer Encoding中找到关于打包编码的更多信息。
2.4.添加更多消息类型
在一个.proto文件中可以定义多个消息类型。
如果想定义SearchRequest的响应消息格式SearchResponse,
可以将它们添加到相同的.proto文件:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
2.5.添加注释
要向.proto文件添加注释,
请使用C/C++/Java风格的双斜杠(//)和/… /语法:
/* 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.
}
2.6.保留字段
当你需要修改消息类型时,
如果删除或者注释了某个字段,
后来的用户可以在修改该消息类型时,
可能会重用该字段的名称和数字编号。
如果以后不小心加载了相同的.proto文件的旧版本,
这可能会导致严重的问题,
包括数据损坏、隐私问题等等。
确保不会发生这种情况的一种方法是,
指定保留已删除字段的名称(因为这也会导致JSON序列化问题)和数字编号。
如果将来有任何用户试图使用这些字段标识符,
protocol buffer的编译器会发出警告。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
注意不要在一行reserved声明中同时包括字段的名称和数字编号。
2.7.从.proto文件生成了什么?
当使用protocol buffer编译器来运行.proto文件时,
编译器将生成所选择语言的代码,
这些代码可以操作在.proto文件中定义的消息类型,
包括获取、设置字段值,
将消息序列化到一个输出流中,
以及从一个输入流中解析消息。
- 对于c++,编译器会从每个.proto生成一个.h和.cc文件,并为文件中描述的每个消息类型生成一个类。
- 对于Java,编译器为每一个消息类型生成了一个.java文件,以及用于创建消息类实例的特殊Builder类。
- 对于Python,稍有不同,Python编译器为.proto文件中的每个消息类型生成一个含有静态描述符的模块,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的Python数据访问类。
- 对于Go,编译器会位每个消息类型生成了一个.pd.go文件。
- 对于Ruby,编译器会为每个消息类型生成了一个.rb文件。
- 对于Objective-C,编译器会为每个消息类型生成了一个pbobjc.h文件和pbobjcm文件,.proto文件中的每一个消息有一个对应的类。
- 对于c#,编译器会为每个消息类型生成了一个.cs文件,.proto文件中的每一个消息有一个对应的类。
- 对于Dart,编译器会为每个消息类型生成了一个.pb.dart文件,.proto文件中的每一个消息有一个对应的类。
可以通过所选择的语言的教程(即将发布的proto3版本),
了解更多关于每种语言使用api的信息。
更多API细节,请参阅相关API参考(proto3版本也即将发布)。
3.标量值类型
一个标量消息字段可以是以下类型,
表格显示了.proto文件中支持的类型,
以及自动生成的类中对应语言的类型:
.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[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个字节。如果值经常大于228,则比uint32更有效。 | uint32 | int[1] | int/long[3] | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
fixed64 | 总是8个字节。如果值经常大于256,则比uint64更有效。 | uint64 | long[1] | int/long[3] | uint64 | Bignum | ulong | integer/string[5] | Int64 |
sfixed32 | 总是4个字节。 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
sfixed64 | 总是8个字节。 | int64 | long | int/long[3] | 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[4] | string | String (UTF-8) | string | string | String |
bytes | 可以包含不超过232的任意字节序列。 | string | ByteString | str | []byte | String (ASCII-8BIT) | ByteString | string | List |
你可以在Protocol Buffer Encoding中,
找到更多"序列化消息时这些类型如何编码"的信息。
[1] 在java中,无符号32位和64位整型被表示成他们的相对的有符号整型类型,最高位储存在符号位中。
[2] 在所有的情况下,设定值的时候,会执行类型检查以确保此值是有效的。
[3] 64位或者无符号32位整型在解码时总是表示为long,但如果在设置字段时使用int,则可以表示为int。在所有情况下,值必须符合在设置时所使用的类型。请参阅[2]。
[4] Python字符串在解码时表示为unicode,但如果给定ASCII字符串,则可以表示为str(这可能会改变)。
[5] Integer在64位的机器上使用,string在32位机器上使用
4.默认值
在解析消息时,如果已编码的消息不包含特定的singular元素,
则解析对象中的对应字段将被设置为该字段的默认值。
对于不同类型的默认值如下:
- 对于string,默认值是一个空string。
- 对于byte,默认值是一个空的bytes。
- 对于bool,默认值是false。
- 对于数值类型,默认值是0。
- 对于枚举类型,默认值是第一个定义的枚举值,且必须为0。
- 对于消息类型,没有默认值,它的确切值取决于选择的语言,请参考生成的代码指南
可重复字段的默认值为空,
通常情况下是对应语言中空列表。
注意对于标量消息字段,
一旦消息被解析,
就无法判断一个字段是否显式地设置为默认值,
还是根本没有被设置:
例如boolean值是否被显示设置为false,
还是没有设置,而使用了默认值false。
你应该在定义消息类型时牢记这一点。
如果你不想让某个行为在默认情况下发生,
就不要设置一个布尔值来开启某个行为。
如果将标量消息字段设置为默认值,
则该值将不会在传输时被序列化。
有关默认值如何在生成代码中工作的详细信息,
请参考所选语言的生成的代码指南。
5.枚举
在定义消息类型时,
可能想为一个字段指定"预定义的值列表"中的一个值。
假设给SearchRequest添加一个corpus字段,
其中corpus的值可能是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO中的一个
只需要向消息定义中添加一个枚举(enum),
并且为每个可能的值定义一个常量就可以了。
在下面的例子中,
添加了一个名为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作为默认值。
这个0必须为第一个元素,
是为了兼容proto2语义,
因为proto2的第一个枚举值总是默认值。
为了定义别名,
可以将不同的枚举常量映射到相同的值。
为此,你需要将allow_alias选项设置为true,
否则编译器会在找到别名时报告错误。
message MyMessage1 {
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
}
message MyMessage2 {
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值传输时使用可变编码,
对负数不够高效,因此不推荐在enum中使用负数。
如上所示,
可以在一个消息定义的内部或外部定义枚举,
这些枚举可以在.proto文件中的任何消息定义里使用。
当然也可以在一个消息中定义一个枚举类型,
而在另一个不同的消息中使用它,
采用MessageType.EnumType的语法格式。
当对一个使用了枚举的.proto文件运行protocol buffer编译器的时候,
生成的代码中将有一个对应的enum(对Java或C++来说),
或者一个特殊的EnumDescriptor类(对 Python来说),
它被用来在运行时生成的类中创建一系列的整型值符号常量。
注意:生成的代码可能会受到特定于语言的枚举数限制,
一种语言的枚举数低至数千,
请注意你计划使用的语言的限制。
在反序列化的过程中,
无法识别的枚举值会被保存在消息中,
尽管在反序列化消息时如何表示该值取决于选择的语言。
在那些支持开放枚举类型超出指定范围之外的语言中(例如C++和Go),
无法识别的值会被简单的表示成所支持的整型。
在具有封闭枚举类型的语言中(例如Java),
使用枚举中的一个类型来表示无法识别的值,
并且可以使用特殊的访问器访问底层整数。
在其他情况下,如果消息被序列化,
那么无法识别的值仍将与消息一起序列化。
关于如何在应用程序的消息中使用枚举的更多信息,
请参考所选语言的生成的代码指南。
5.1.保留字段
枚举的保留字段和上面消息的保留字段基本相同:
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
6.使用其他消息类型
你可以将其他消息类型用作字段类型。
例如在SearchResponse消息中包含Result消息,
此时可以在相同的.proto文件中定义一个Result消息类型,
然后在SearchResponse消息中指定一个Result类型的字段:
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
6.1.导入定义
在上面的例子中,
Result消息类型与SearchResponse是定义在同一文件中的。
如果想要使用的消息类型已经在其他.proto文件中已经定义过了呢?
你可以通过导入其他.proto文件中的定义来使用它们。
要导入另一个.proto的定义,
需要在文件的顶部添加一条导入声明:
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参数,
protocal编译器会在指定目录搜索要导入的文件。
如果没有指定目录,
则在调用编译器的目录中查找。
通常只需要配置proto_path为工程根目录,
并且正确配置需要导入的名称即可。
6.2.使用proto2消息类型
在proto3中导入proto2的消息类型也是可以的,
反之亦然,
然而proto2枚举不可以直接在proto3语法中使用,
如果仅仅在被导入的proto2消息中使用是可以的。
7.嵌套类型
可以在其他消息类型中定义和使用消息类型,
在下面的例子中,
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;
}
}
}
8.更新一个消息类型
如果现有的消息类型不再满足新的需求,
如果要在消息中添加一个额外的字段,
但是同时保持旧版本写的代码仍然可用。
不用担心,
在不破坏任何现有代码的情况下更新消息类型非常简单,
只要记住以下规则:
- 不要更改任何现有字段的数字编号。
- 如果增加新的字段,那么使用旧格消息格式的代码序列化的任何消息,仍然可以被新产生的代码解析。你应该记住这些元素的默认值,这样新代码就可以正确的和旧代码生成的消息交互。类似的,通过新代码产生的消息也可以被旧代码解析:只不过新的字段在解析时会被忽略掉。更多详细信息,请参阅无法识别字段章节的内容。
可以删除字段,只要字段号不在更新的消息类型中再次使用。 - 只要字段的数字编号在新的消息类型中不再使用,这些字段就可以移除,更好的做法可能是重命名那个字段,例如在字段前添加"OBSOLETE_"前缀,或者保留字段编号,以便将来不会意外地重用该数字。
- int32、uint32、int64、uint64和bool是都是兼容的,这意味着可以将字段从其中一种类型更改为另一种类型,而不会破坏向前或向后的兼容性。如果解析出来的数字与对应的类型不相符,那么结果就像在C++中对它进行了强制类型转换一样,例如把一个64位数字当作int32来 读取,那么它就会被截断为32位的数字。
- sint32和sint64是互相兼容的,但与其他整数类型不兼容。
- string和bytes是兼容的,只要bytes是有效的UTF-8编码。
- 嵌套消息与bytes是兼容的,只要bytes包含该消息的一个编码过的版本。
- fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的。
- 对于string、bytes和消息字段,optional与repeated是兼容的。给定重复字段的序列化数据作为输入,如果该字段是原始类型字段,则期望该字段是optional的,客户端将接受最后一个输入值;如果该字段是消息类型字段,则将合并所有输入元素。注意对于数值类型,包括布尔和枚举,这通常是不安全的。数值类型的repeated字段可以用packed格式序列化,然而对于optional字段时,将不能被正确解析。
- enum兼容int32、uint32、int64和uint64,注意不兼容的值将被截断。然而在客户端反序列化之后可能会有不同的处理方式:例如,无法识别的proto3枚举类型将保留在消息中,但是在对消息进行反序列化时如何表示它是依赖于语言的。int字段总是保留它们的值。
- 将一个值更改为一个新值的成员是安全的和二进制兼容的。如果保证代码不会一次设置多个字段,那么将多个字段移到一个新的字段中可能是安全的。将任何字段移动到现有字段中是不安全的。
9.未知字段
未知字段是格式良好的protocol buffer 序列化数据,
但是解析器无法识别的字段。
例如,当旧二进制文件解析带有新字段的新二进制文件发送的数据时,
这些新字段在旧二进制文件中成为未知字段。
最初,proto3总是在解析时丢弃未知字段,
但在3.5版本中,重新引入了保留未知字段的功能,
以匹配proto2的行为。
在版本3.5及更高版本中,
解析时会保留未知字段,并包含在序列化输出的数据中。
10.参考文章
Language Guide (proto3)
Protobuf3教程
ProtoBuf v3 语法简介
gRPC之proto语法