[译]Protocol Buffer Language Guide (proto3)
文章目录
原文链接:Language Guide (proto3) | Protocol Buffers | Google Developers
本指导文档描述了如何使用protocol buffer语言来构建你的protocol buffer数据,包括.proto
文件的语法规则,包括如何从你的.proto
文件中生成数据访问类。本文介绍的是proto3版本的语言规则,关于proto2版本的信息,请参考: Proto2 Language Guide。
本文是一个指导性的参考文档,关于如何使用本文中提及的特性的例子,请参考tutorial 并选择你想要的特定编程语言的例子。
定义一个消息类型
首先让我们来看一个非常简单的例子。比如说你想要定义一个搜索请求这样一个消息,这个消息里包含一个查询字符串(query string)、你所感兴趣页面的数量以及每个页面包含的结果。如下是你的.proto
文件定义:
1syntax = "proto3";
2
3message SearchRequest {
4 string query = 1;
5 int32 page_number = 2;
6 int32 result_per_page = 3;
7}
第一行说明你使用的是
proto3
版本的语言规则。如果没有这一行,那么protocol buffer编译器会认为你是使用proto2
版本的语言规则。syntax
定义必须是在.proto
文件的第一行SearchRequest
包含了三个字段(键值对),每个字段都有一个名字和对应的数据类型
指定字段类型
上面的例子中,三个字段都是标量类型(相对与复合类型,比如struct而言):两个整型变量(page_number
和result_per_page
)和一个字符串变量(query
)。当然,你可以定义你的字段为复合类型,比如枚举和其他消息类型。
分配字段号码
正如你所见,消息定义中的每一个字段都一个唯一的号码。这些字段号码是在二进制消息格式中(message binary format)的标识,一旦你开始使用你定义的消息,这些字段号码都不应该再改变。注意,字段号码为1到15的字段在编码的时候占用一个字节(包括字段号码和字段类型,关于这点的更多细节,请参看:Protocol Buffer Encoding)。字段号码从16到2047的字段会占用两个字节。因此你应该将1到15范围的号码留给那些在消息里经常用到的字段。并且要考虑预留一些该范围的号码,以便将来扩展你的消息。
你可以使用的最小字段号码是1,最大字段229 - 1或者536,870,911。你不可以使用字段号码为19000到19999的部分(FieldDescriptor::kFirstReservedNumber
到 FieldDescriptor::kLastReservedNumber
),这些是为protocol buffer本身的实现所保留的 - 如果你在.proto
文件中使用了这些保留范围内的字段号码,编译器报错。同样的,如果你使用了那些被标记为reserved
的字段号码,编译器也会报错。
指定字段规则
消息的字段可以是下面两种形式:
- 单数形式:一个良好组织的消息可以有零个或者一个这种字段(不能超过一个)。这是
proto3
版本的默认字段规则。(个人理解,这里不是说只能定义零个或者一个这种单数形式的字段,而是说定义为单数形式的字段里最多包含一个元素。这个要对应repeated
来理解) repeated
:这种字段对应的元素在一个消息里可以重复任意多次。repeated
中的元素的顺序是固定的。
在proto3
中,标量类型的repeated
字段是默认是使用packed
来编码的。你可以从Protocol Buffer Encoding中了解更多关于packed
编码的信息。
添加更多的消息类型
在一个.proto
文件中你可以定义多种不同的消息类型,如果你要定义多个相关的消息,这就非常有用 - 比如你想要定义和SearchRequest
相对应的SearchResponse
消息,你可以在同一个.proto
文件中添加:
1message SearchRequest {
2 string query = 1;
3 int32 page_number = 2;
4 int32 result_per_page = 3;
5}
6
7message SearchResponse {
8 ...
9}
添加注释
在.proto
中添加注释,你可以使用C/C++风格的语法:
1/* SearchRequest represents a search query, with pagination options to
2 * indicate which results to include in the response. */
3
4message SearchRequest {
5 string query = 1;
6 int32 page_number = 2; // Which page number do we want?
7 int32 result_per_page = 3; // Number of results to return per page.
8}
保留字段
如果你更新息类型的时候删除了一个字段,或者把一个字段注释掉,将来其他人在更新相同的消息的时候是可以重用之前字段的号码。之后在他们使用老版本的.proto
时将会导致许多问题,包括数据出错,privacy bugs等。为了确保这些问题不会发生,有一个办法是把那些你删除的字段的使用过的字段号码(和/或名称)标记为reserved
。如果你使用了这些被reserved
标记的号码,编译器会报错。
1message Foo {
2 reserved 2, 15, 9 to 11;
3 reserved "foo", "bar";
4}
注意,你不可以在同一个reserved
语句中同时制定字段号码和字段名称。
从.proto
文件中生成的代码
当你编译.proto
文件时,编译器会生成你指定的语言代码来和你定义在.proto
文件中消息交互,包括访问和设置字段的值、序列化你的消息问输出流以及从输入流中解析为消息。
- **C++**语言:生成
.h
和.cc
文件,每个消息都会生成一个对应的类 - Java语言,每个消息都会生成一个包含对应类的
.java
文件,以及创建对应类实例的Builder
类 - Kotlin语言,除了和Java语言生成的一样的代码以外,还会生成一个
.kt
文件包含一个用例创建类型实例的DSL - Pytyon语言少许不同,生成一个包含了每个消息对应的静态描述器的模块,这些被用来在运行时创建必要的Python数据访问类
- Go语言,生成一个包含所有消息的
.pb.go
代码文件 - Ruby语言,生成一个包含所有消息的
.rb
代码文件 - Object-C语言,生成一个
probjc.h
和projc.m
文件,每一个消息对应了一个类 - **C#**语言,生成一个
.cs
文件,每一个消息对应了一个类 - Dart,生成一个
.pb.dart
文件,每一个消息对应了一个类
关于特定语言的API的使用,你可以参考:API reference。
标量类型
标量类型即下表中的一种类型。下表展示了.proto
中支持的标量类型和特定语言标量类型的对应关系(原文有更多的编程语言的对应关系,译文中我只选取我关心的语言☺):
.proto Type | Notes | C++ Type | Java/Kotlin Type[1] | Python Type[3] | Go Type | C# Type |
---|---|---|---|---|---|---|
double | double | double | float | float64 | double | |
float | float | float | float | float32 | float | |
int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int | int32 | int |
int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long[4] | int64 | long |
uint32 | Uses variable-length encoding. | uint32 | int[2] | int/long[4] | uint32 | uint |
uint64 | Uses variable-length encoding. | uint64 | long[2] | int/long[4] | uint64 | ulong |
sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int | int32 | int |
sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long[4] | int64 | long |
fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 228. | uint32 | int[2] | int/long[4] | uint32 | uint |
fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 256. | uint64 | long[2] | int/long[4] | uint64 | ulong |
sfixed32 | Always four bytes. | int32 | int | int | int32 | int |
sfixed64 | Always eight bytes. | int64 | long | int/long[4] | int64 | long |
bool | bool | boolean | bool | bool | bool | |
string | A string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232. | string | String | str/unicode[5] | string | string |
bytes | May contain any arbitrary sequence of bytes no longer than 232. | string | ByteString | str | []byte | ByteString |
你可以从Protocol Buffer Encoding了解更多关于消息序列化时的编码相关的信息。
默认值
解析消息的时候,如果原先经过编码的消息里没有包含特定的单数形式的元素(原文很拗口,个人觉得这里就是说从编码的消息里解析的时候找不到特定的字段),那些这些字段就被赋值成对应类型的默认值,规则如下:
- strings类型的默认值是空字符串
- bytes类型的默认值是空字节
- bools类型的默认值是false
- 数字类型的默认值是0
- 枚举类型类型的默认值是枚举的第一个元素,也就是枚举值为0的元素
- 如果字段类型是其他的消息,那么其默认值跟特定的语言相关,详见:generated code guide
repeated
类型的字段的默认值一般是对应语言里的空列表。
对于标量类型的字段,实际上消息解析以后是没办法确定字段的值是人为设置的还是由于根本就没有赋值,而后解析的时候被解析成了默认值 - 你在定义消息的时候需要牢记这一点。例如说,不要定义一个boolean类型的开关是的当值为false的时候执行某些动作(如果你不希望这个动作默认是要执行的,因为boolean类型的默认值就是false)。同时还需要注意,当一个标量字段被设置为默认值的时候,值是不会被序列化的。
枚举
当你定义你的消息的时候,你可能希望某些字段的值只能从一些预先定义好的值里来。例如,你想为你的SearchRequest
添加一个corpus
字段,这个字段的值可以是UNIVERSAL
, WEB
, IMAGES
, LOCAL
, NEWS
, PRODUCTS
或者 VIDEO
。你可以通过定义枚举类满足你的需求,枚举中所有可能的值都是一个常量。
下面的例子中我们添加了一个名为Corpus
的枚举,这个枚举中定义了它可能的值,同时也定义类型为Corpus
的字段:
1message SearchRequest {
2 string query = 1;
3 int32 page_number = 2;
4 int32 result_per_page = 3;
5 enum Corpus {
6 UNIVERSAL = 0;
7 WEB = 1;
8 IMAGES = 2;
9 LOCAL = 3;
10 NEWS = 4;
11 PRODUCTS = 5;
12 VIDEO = 6;
13 }
14 Corpus corpus = 4;
15}
如你所见,枚举Corpus
中的第一个值UNIVERSAL
是常量0:实际上所有的枚举的第一个常量值都应该是0,这是因为:
- 一定得有0值,这样枚举可以当成数字类型,默认值就是0
- 0值作为枚举的第一个元素,这个和
proto2
的语言兼容,即第一个元素的值总是默认值
你可以通过给不同的枚举元素赋相同的值来定义别名,前提是你需要把allow_alias
选项设置为true
,否者编译器遇到这样的别名设置会报错。
1message MyMessage1 {
2 enum EnumAllowingAlias {
3 option allow_alias = true;
4 UNKNOWN = 0;
5 STARTED = 1;
6 RUNNING = 1;
7 }
8}
9message MyMessage2 {
10 enum EnumNotAllowingAlias {
11 UNKNOWN = 0;
12 STARTED = 1;
13 // RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
14 }
15}
枚举元素的值不能超过32位整型的范围。由于枚举值是使用varint encoding 编码,负值是无效的因此也不建议使用负值。你可以在一个消息内部定义枚举(如上面的例子所示)或者外部定义,然后可以在.proto
中定义的任意消息里使用内部或者外部定义的枚举。你可以像使用消息里的字段一样来使用定义在一个消息内部的枚举:_MessageType_._EnumType_
。
在.proto
中定义的enum
,对应生成的代码中,比如Java,Kotlin,C++也会有对应的enum
,Pyton语言是EnumDescriptor
类。
注意: 对于枚举类型的值的个数限制,这个是和编程语言本身相关,关于这个限制请查看特定的语言说明。
在反序列化的时候,对于无法识别的枚举值,也会在消息中保留下来,至于保留下来以何种方式展现,这个就和语言相关了。对于那些允许枚举值越界的变成语言,比如C++和Go,这些未知的枚举值就是被简单的以整型形式保留。对于不允许枚举值越界的语言,比如Java,可以通过特定的访问器来访问底层的整型值。不管何种情况,消息序列化的时候,会把无法识别的枚举值包含进去。
更多关于如何使用enum
的信息,请参考:generated code guide中特定编程语言的部分。
枚举保留值
如果你更新消息类型的时候删除了一个枚举项或者注释掉,后面其他人在更新这个枚举的时候可以重用原枚举项对应的数值。之后在他们使用老版本的.proto
时将会导致许多问题,包括数据出错,privacy bugs等。为了确保这些问题不会发生,有一个办法是把那些你删除的枚举项的使用过的数值(和/或名称)标记为reserved
。如果你使用了这些被reserved
标记的号码,编译器会报错。你可以指定你保留的枚举数值的范围,你可以使用max
关键字来指定范围的最大值。
1enum Foo {
2 reserved 2, 15, 9 to 11, 40 to max;
3 reserved "FOO", "BAR";
4}
注意,你不可以在同一个reserved
语句中同时指定枚举项的名称和枚举值。
使用其他消息类型
你可以使用其他消息作为你的字段的类型。例如,你希望你的SearchResponse
中包含Result
类型的字段 - 你可以在同一个.proto
文件找那个定义Result
消息,然后指定SearchResponse
的字段为Result
类型:
1message SearchResponse {
2 repeated Result results = 1;
3}
4
5message Result {
6 string url = 1;
7 string title = 2;
8 repeated string snippets = 3;
9}
导入定义
注意,这个特性不适用于Java
在上面的例子中,Result
是和SearchResponse
位于相同的.proto
文件中 - 如果你想要使用的消息类型是定义在其他.proto
文件中呢?
你可以通过导入.proto
文件来使用定义在其他.proto
文件中的消息类型。你可以在文件头部添加import语句来导入:
1import "myproject/other_protos.proto";
默认情况下,你只能使用直接导入的.proto
文件中定义的消息类型。但是,有时候你可能需要把.proto
文件移动到一个新的位置。你可以直接移动.proto
文件到一个新的位置,然后然后更新所有的import语句指向.proto
的新位置,你现在也可以在原来的位置放一个”假“的.proto
然后使用import public
语句把老的.proto
中的import语句重定向到新的位置。任何导入了包含import public
的.proto
文件也就把import public
的依赖也导入了。例如:
1// new.proto
2// All definitions are moved here
1// old.proto
2// This is the proto that all clients are importing.
3import public "new.proto";
4import "other.proto";
1// client.proto
2import "old.proto";
3// You use definitions from old.proto and new.proto, but not other.proto
protocol buffer编译器从命令行参数-I
/--proto_path
指定的一系列路径中查找导入的.proto
文件。如果这个参数没有指定一个值,编译器就从运行的目录里查找。通常地,你应该把--proto_path
的值设置为你项目的根目录,然后在import语句中使用完整的名称。
使用proto2
的消息类型
在proto3
的消息中导入并使用proto2
的消息类型是可以的,相反也是可以。但是proto2
的枚举是无法在proto3
中使用(如果proto2
的消息体中使用proto2
枚举是可以的)
嵌套类型
你可以在一个消息类型里定义嵌套的消息类型并使用它,如下面的例子所示 - Result
消息是定义在SearchResponse
里的:
1message SearchResponse {
2 message Result {
3 string url = 1;
4 string title = 2;
5 repeated string snippets = 3;
6 }
7 repeated Result results = 1;
8}
如果你想在定义嵌套消息的父消息外使用,你可以通过_Parent_._Type_
来使用:
1message SomeOtherMessage {
2 SearchResponse.Result result = 1;
3}
你可以多层嵌套消息,如下所示:
1message Outer { // Level 0
2 message MiddleAA { // Level 1
3 message Inner { // Level 2
4 int64 ival = 1;
5 bool booly = 2;
6 }
7 }
8 message MiddleBB { // Level 1
9 message Inner { // Level 2
10 int32 ival = 1;
11 bool booly = 2;
12 }
13 }
14}
更新消息类型
如果当前的消息已经不能满足你的需要,例如你希望你的消息里添加一个额外的字段,但是你仍然希望使用基于老的格式生成的代码。不用担心,更新消息类型而不破坏现有的代码是很简单的。你只需要记住以下规则:
- 不要更改现有字段的字段号码
- 如果你添加了新的字段,那些通过老的代码序列化出来的消息仍然可以被新的代码解析。你要牢记默认值 default values规则,这样才能恰当地和老代码生成的消息交互。同样的,新代码创建的消息也是可以被老代码解析:老代码程序解析的时候只是简单地忽略新加的字段。你可以参考未知字段获取更多的细节
- 字段可以被删除,但是要确保这些要删除的字段对应的字段号码不会在消息中再次被使用。你或许可以对字段重命名,比如添加"OBSOLETE_"这样的前缀,或者把字段号码标记为保留字段,这样将来更新你的
.proto
文件的时候就不会意外的使用那些号码 int32
,uint32
,int64
,uint64
和bool
是相互兼容的 - 这意味着你从其中的一种类型改为另一种类型不会破坏向前或者向后的兼容性。如果一个解析一个数字的时候和目标类型不匹配,会发生和在C++中转化数字类型一样的结果,例如一个64位的数字读取为32位的时候,会被截断为32位的数字sint32
和sint64
兼容,但是和其他整型类型不兼容string
和bytes
兼容,只要bytes
是有效的UTF-8格式- 嵌套的消息和
bytes
兼容,只要bytes
包含编码过的消息 fixed32
和sfixed32
,fixed64
以及sfixed64
兼容- 对于
string
,byte
和消息字段,optional
和repeated
兼容。如果一个repeated
类型字段的序列化数据作为输入,对于期望这个字段为optional
的客户端来说,如果这个字段是基本类型,那么会获取最后一个输入的值;如果这个字段是一个消息类型,那么会把消息里的所有元素合并(这个有点拗口,不知道该怎么理解)。需要注意的是,对于数值类型(包括bools和enums)来说这是不安全的。数值类型的repeated
字段可以序列化为packed 格式,如果客户端期望的是optional
字段,那么不能被正确解析 enum
和int32
,uint32
,int64
,uint64
兼容(注意如果类型不完全匹配,数据可能被截断)。然而需要注意的是,客户端的代码在反序列的时候可能会有不同的行为,比如未识别的proto3 enum
类型的值会在消息中保留下来,这种行为是和语言本身相关。整型的字段的值总是保持了原来的值- 把一个字段的类型改为
oneof
的成员是安全的且二进制上兼容。把多个字段变成oneof
或许是安全的,但是你要确保设置的代码只能有一次(这里也不理解是啥意思)。把任何字段移到已存在的oneof
里是不安全的
未知字段
未知字段是指protocol buffer序列化数据的时候被良好的组织的数据,但是反序列化的时候解析器不认识的字段。例如,对于老代码程序解析由新代码程序序列化的带有新添加的字段的数据时,那些新添加的字段对于老代码程序就变成了未知字段。
原先proto3消息在解析的时候总是丢弃未知字段,但是在版本3.5我们重新引入了对未知字段的保留以匹配proto2的行为。在版本3.5以及之后未知字段在解析和序列化的时候又重新保留了下来。
Any
Any
消息类型能让你像使用嵌套类型一样来使用一个消息而不需要有.proto
的定义。一个Any
消息包含一个序列化为bytes
的消息以及用来解释成对应消息类型的一个全局唯一标识符URL。你需要导入google/protobuf/any.proto
来使用Any
类型。(注:说白了,这个Any类型就是一种序列化,把一种特定类型按照某种方式序列化为字节数组)
1import "google/protobuf/any.proto";
2
3message ErrorStatus {
4 string message = 1;
5 repeated google.protobuf.Any details = 2;
6}
默认的类型URL是type.googleapis.com/_packagename_._messagename_
。
不同的语言实现会提供类型安全的运行时库来打包和解包Any
类型的值 - 例如在Java中,Any
类型提供了pack()
和unpack()
访问器,在C++中提供了PackFrom()
和UnpackTo()
方法:
1// Storing an arbitrary message type in Any.
2NetworkErrorDetails details = ...;
3ErrorStatus status;
4status.add_details()->PackFrom(details);
5
6// Reading an arbitrary message from Any.
7ErrorStatus status = ...;
8for (const Any& detail : status.details()) {
9 if (detail.Is<NetworkErrorDetails>()) {
10 NetworkErrorDetails network_error;
11 detail.UnpackTo(&network_error);
12 ... processing network_error ...
13 }
14}
当前用于和Any
类型交互的运行时库还在开发中。
如果你熟悉 proto2 语法,和proto2
消息的允许extensions一样,Any
可以用来存放proto3
的消息。
Oneof
如果你的一个消息有多个字段,并且同一时刻最多只有一个字段被赋值,你可以使用oneof
特性来强制这种行为和节省内存。
oneof
字段和常规字段一样,只是oneof
里所有的成员是共享内存的,且同一时刻最多只有一个字段被赋值。设置任何一个oneof
里的成员将自动清空其他的成员。你可以一个特殊的case()
或者WhichOneof()
方法(取决特定的语言)来判断oneof
里的成员是否被赋值了。
使用oneof
你可以使用oneof
关键字跟上你的oneof
名称来定义一个oneof
,比如在test_oneof
中:
1message SampleMessage {
2 oneof test_oneof {
3 string name = 4;
4 SubMessage sub_message = 9;
5 }
6}
然后你可以添加oneof
成员到它的定义中。你可以添加除了map
和repeated
类型的成员到oneof
定义里。在你生成的代码中,oneof
有着和常规字段一样的读写访问器。你还会有一个方法(取决特定的语言)来判断oneof
里的成员是否被赋值了。请参考特定语言的API reference以了解更多。
oneof
特性
设置任何一个
oneof
里的成员将自动清空其他的成员。如果你设置了多个成员,只有最后一个设置的成员才有值。1SampleMessage message; 2message.set_name("name"); 3CHECK(message.has_name()); 4message.mutable_sub_message(); // Will clear name field. 5CHECK(!message.has_name());
解析的时候如果遇到来自同一个
oneof
的多个成员,只有最后一次看到的成员才会被解析oneof
不用标识成repeat
反射API可以访问
oneof
字段如果
oneof
字段设置为默认值(比如int32类型的oneof
成员设置为0),case
方法就会判定这个成员被设置了,这个值会被序列化如果你使用C++,确保你的代码不会引发内存泄漏。下面的代码会奔溃原因是因为
sub_message
在调用了set_name()
后就被删除了1SampleMessage message; 2SubMessage* sub_message = message.mutable_sub_message(); 3message.set_name("name"); // Will delete sub_message 4sub_message->set_... // Crashes here
还是C++,
Swap()
两个oneof
消息后,每个消息的oneof
字段发生互换。在下面的例子中,msg1
将会有sub_message
成员而msg2
将会有name
成员:1SampleMessage msg1; 2msg1.set_name("name"); 3SampleMessage msg2; 4msg2.mutable_sub_message(); 5msg1.swap(&msg2); 6CHECK(msg1.has_sub_message()); 7CHECK(msg2.has_name());
1SampleMessage msg1; 2msg1.set_name("name"); 3SampleMessage msg2; 4msg2.mutable_sub_message(); 5msg1.swap(&msg2); 6CHECK(msg1.has_sub_message()); 7CHECK(msg2.has_name());
向后兼容的问题
新增或者移除oneof
字段的是要小心 - 在检查oneof
的成员是否被赋值的时候返回None/NOT_SET
,这可能意味着oneof
成员没有被赋值或者某个成员的赋值是基于不同版本的oneof
字段。这其中的区别无从得知,因为无法确定未知字段是否是oneof
的成员。
标签复用问题:
- 从
oneof
中移出成员或者添加成员到oneof
:在消息序列化和解析后,你可能会丢失一些信息(比如一些字段被清空)。但是你可以安全的把一个字段移到一个新的oneof
里,或者把多个字段移到也可以,前提是你知道只能有一个字段被赋值 - **删除一个成员或者重新添加回去:**这个可能会知道消息序列化和解析后成员的赋值丢失
- **分离或者合并
oneof
成员:**这和移动常规的字段有一样的问题
Maps
如果你希望在你的数据定义中创建map
,protocol buffer提供了一个方便的语法:
1map<key_type, value_type> map_field = N;
key_type
可以是任何整型或者字符串类型(也就是说除了浮点和bytes
之外的标量类型)。因此,如果你希望创建一个键为字符串类型值为Project
消息的map
,你可以定义如下:
1map<string, Project> projects = 3;
map
类型的字段是不可以定义为repeated
- Wire格式的顺序不固定,迭代
map
也是无序的,因此你不能依赖map
为你提供有序的成员 - 为
.proto
生成text格式的时候,maps
是按照键排序的。数值类型的键是根据数值排序(这是啥意思,是不是和编码有关系?) - 如果你只给定了一个键,对应的值没有赋值,这种情况在序列化的时候不同的语言有不同的行为 - C++,Java,Kotlin和Python中会用默认值来序列化,其他语言中这种情况不会被序列化
访问map
的API目前已对proto3
支持。你可以从 API reference中查找特定语言相关的说明。
向后兼容
map
的语法和下面的定义等价,因此对那些不支持map
的protocol buffer实现依然可以处理你的数据。
1message MapFieldEntry {
2 key_type key = 1;
3 value_type value = 2;
4}
5
6repeated MapFieldEntry map_field = N;
任何支持map
的protocol buffer的实现必须能够基于上面的定义生成和接受数据。
包
你可以在.proto
中添加一个可选的package
指示符,这样可以防止不同的消息类型间名字冲突。
1package foo.bar;
2message Open { ... }
你可以在定义你的消息类型的时候使用package
指示符:
1message Foo {
2 ...
3 foo.bar.Open open = 1;
4 ...
5}
package
指示符是如何起作用的,这个取决于你选择的编程语言:
- **C++**中,生成的类被包含在C++命名空间里。例如,
Open
会在命名空间foo::bar
里 - Java和Kotlin中,
package
和Java的包一样,除非你自己显式的在.proto
里指定了option java_package
- Python中,
package
会被忽略,因为Python modules是按照文件位置来组织的 - Go中,
package
和Go的包一样,除非你自己显式的在.proto
里指定了option go_package
- **C#**中,
package
的名称先被转变成PascalCase作为命名空间的名称,除非你自己显式的在.proto
里指定了option csharp_namespace
。例如,Open
会放在命名空间Foo.Bar
下
包和名字解析
在protocol buffer中类型名称的解析就跟C++中类似:从最内部的范围里查找,然后次之以此类推(任何一个包都被当成父包的内部包)。'.'意味着从最外层开始(比如:.foo.bar.Baz
)
protocol buffer编译器通过解析导入的.proto
文件来解析所有的类型名称。即使不同语言有不同的范围规则,不同语言的代码生成器知道如何找到特定的类型。
定义服务
如果你希望在你的RPC服务中使用你定义的消息,你可以在.proto
文件中定义RPC服务接口,而后protocol buffer会生成特定语言的服务接口代码和mock实现。因此,假如你需要定义个RPC服务的方法接受SearchRequest
参数返回SearchResponse
,你可以按如下方式定义:
1service SearchService {
2 rpc Search(SearchRequest) returns (SearchResponse);
3}
gRPC是最直接使用protocol buffer的RPC系统:Google开发的一个语言和平台中立的开源的RPC系统。gRPC能非常好的和protocol buffer工作,使用一个特殊的protocol buffer插件能完美的生成gRPC代码。
如果你不使用gRPC,也是可以在你自己的RPC实现中使用protocol buffers,你可以参考:Proto2 Language Guide
有许多基于protocol buffers的RPC项目正在开发中,这里有一个我们所知道的项目的列表:third-party add-ons wiki page
Json Mapping
proto3
支持通用的Json编码,这使得它可以很方便地跨系统共享数据。如下表所示,Json编译是按照type-by-type
的方式进行。
如果在Json编码的数据中缺少值,或者值是null
,那么在解析成protocol buffre数据的时候将被解释成默认值。如果protocol buffer编码的字段的值是对应类型的默认值,那么编码成Json的时候,为了节省空间这些值会被忽略。一种实现或许可以提供选项给用户使得把默认值也包括在Json编码的数据中。
proto3 | JSON | JSON example | Notes |
---|---|---|---|
message | object | {"fooBar": v, "g": null, …} | Generates JSON objects. Message field names are mapped to lowerCamelCase and become JSON object keys. If the json_name field option is specified, the specified value will be used as the key instead. Parsers accept both the lowerCamelCase name (or the one specified by the json_name option) and the original proto field name. null is an accepted value for all field types and treated as the default value of the corresponding field type. |
enum | string | "FOO_BAR" | The name of the enum value as specified in proto is used. Parsers accept both enum names and integer values. |
map<K,V> | object | {"k": v, …} | All keys are converted to strings. |
repeated V | array | [v, …] | null is accepted as the empty list [] . |
bool | true, false | true, false | |
string | string | "Hello World!" | |
bytes | base64 string | "YWJjMTIzIT8kKiYoKSctPUB+" | JSON value will be the data encoded as a string using standard base64 encoding with paddings. Either standard or URL-safe base64 encoding with/without paddings are accepted. |
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. -0 is considered equivalent to 0. |
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. Check google.protobuf.Value for details. | |
NullValue | null | JSON null | |
Empty | object | {} | An empty JSON object |
JSON 选项
proto3
的JSON实现可能提供以下选项(这下面说的选项原文也没有说明是什么以及怎么使用):
- **包含默认值在JSON编码中:**默认情况下,具有默认值的字段在编码成JSON的时候其值被忽略。一种实现或许可以提供一个选项来覆盖这种默认行为使得把默认值包含进去
- **忽略未知字段:**Proto3 JSON解析器默认会拒绝未知的字段,但是可能可以提供一个选项在解析的时候忽略未知字段
- **使用字段名称来代替lowerCamelCase格式的:**默认情况,proto3 JSON printer会把字段名称转换为lowerCamelCase格式作为Json名称。一种实现可能可以提供一个选项来直接使用字段名称作为Json名称。proto3 JSON解析器应该都要接受字段名称和lowerCamelCase格式的名称
- **把枚举解释成值而不是枚举项:**默认情况下,枚举项被解释成Json。一种实现可能可以提供一个选项来直接使用枚举值还是枚举项(枚举项就是枚举名称,枚举值就是枚举项对应的数值)
选项option
.proto
文件中的声明可以用一些列的option
来标记。option
不会改变这些声明的整体的含义,但是可能会按照某种方式影响这些声明被处理的方式。完整可用的option
是定义在google/protobuf/descriptor.proto
中。
有些option
是文件级别的,意味着他们应该放在作用范围的顶层而不是放在任何一个消息、枚举或者服务的定义里。有些option
是消息级别的,意味着这些应该放在消息里面。有些则是字段级别的,应该放在字段的定义里。除此之外,还可以放在enum
类型、enum
值、oneof
字段、服务类型和服务方法里,只是目前还有有用的option
作用这以上这些(那原文说这个干啥?!)。
(原文中列举了一些最重要的option
适用于Java,C++等我不使用的语言我就没有放进来,偷个懒省点翻译的时间)
自定义Option
Protocol Buffers允许你自定义选项然后使用。这是一个大多数人都不需要用到的高级选项。如果你确实觉得你需要这些自定义选项,你可以参考Proto2 Language Guide。注意,创建自定义选项需要扩展extensions的支持,这些扩展只能用于在proto3
中自定义选项。
生成你的类
你需要运行Protocol buffer编译器.protoc
来编译你的.proto
文件来生成和消息交互的Java/Kotlin/Python/C++/Go/Ruby/Objective-C/C# 代码。如果你还没哟uanzhuang编译器,请先下载然后参考README的说明来安装。对Go语言,你需要安装一个特定的代码生成器插件:你可以从golang/protobuf 找到这个插件以及这个插件的安装说明。
Protocol编译器是按照以下的方式来调用:
1protoc --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
指定了从什么位置导入.proto
文件。如果没有指定,默认从当前位置导入。如果有多个导入位置,可以多次指定--proto_path
并设置路径,这些路径将被按顺序查找和导入。-I=_IMPORT_PATH_
是--proto_path
的简短形式。你可以提供以下一个或者多个导出目录(这里我只列举了生成Go语言的目录,其他语言请参考原文)
--go_out
指定生成Go代码于DST_DIR
目录。更多细节请参考Go generated code reference
你必须指定一个或者多个
.proto
文件作为输入。可以同时指定多个.proto
文件,前提是这些文件都位于IMPORT_PATH
,这样编译器才能根据名字找到这些文件。