语言指南 (proto 2)
本指南描述了如何使用 protocol buffer 语言来结构化您的 protocol buffer 数据,包括 .proto
文件语法以及如何从您的 .proto
文件生成数据访问类。它涵盖了 protocol buffer 语言的 proto2 版本。
有关 editions 语法的信息,请参阅 Protobuf Editions 语言指南。
有关 proto3 语法的信息,请参阅 Proto3 语言指南。
这是一份参考指南——若想通过一个分步示例来了解本文档中描述的许多功能,请参阅你所选语言的教程。
定义消息类型
首先,我们来看一个非常简单的例子。假设您想定义一个搜索请求消息格式,其中每个搜索请求都有一个查询字符串、您感兴趣的特定结果页面以及每页的结果数量。这是您用来定义消息类型的 .proto
文件。
syntax = "proto2";
message SearchRequest {
optional string query = 1;
optional int32 page_number = 2;
optional int32 results_per_page = 3;
}
文件的第一行指定您正在使用 protobuf 语言规范的 proto2 版本。
syntax
必须是文件中第一个非空、非注释行。- 如果未指定
syntax
,protocol buffer 编译器将假定您正在使用 proto2。
SearchRequest
消息定义指定了三个字段(名称/值对),每种你想包含在此类消息中的数据都对应一个字段。每个字段都有一个名称和一个类型。
指定字段类型
在前面的例子中,所有字段都是标量类型:两个整数(page_number
和 results_per_page
)和一个字符串(query
)。您还可以为您的字段指定枚举和复合类型,例如其他消息类型。
分配字段编号
你必须为消息定义中的每个字段赋予一个介于 1
和 536,870,911
之间的编号,并遵守以下限制:
- 给定的编号必须在该消息的所有字段中是唯一的。
- 字段编号
19,000
到19,999
为 Protocol Buffers 实现保留。如果你在消息中使用这些保留的字段编号,protocol buffer 编译器会报错。 - 您不能使用任何先前保留的字段编号或任何已分配给扩展的字段编号。
一旦您的消息类型投入使用,这个编号就不能更改,因为它在消息的有线格式中标识该字段。“更改”字段编号等同于删除该字段并创建一个具有相同类型但编号不同的新字段。有关如何正确执行此操作,请参阅删除字段。
字段编号永远不应被重用。切勿将一个字段编号从保留列表中移出,用于新的字段定义。请参阅重用字段编号的后果。
您应该为最常设置的字段使用 1 到 15 的字段编号。较小的字段编号值在有线格式中占用更少的空间。例如,范围在 1 到 15 之间的字段编号需要一个字节进行编码。范围在 16 到 2047 之间的字段编号需要两个字节。您可以在Protocol Buffer 编码中找到更多相关信息。
重用字段编号的后果
重用字段编号会使解码线路格式的消息变得模棱两可。
protobuf 线路格式是精简的,没有提供检测使用一种定义编码的字段并用另一种定义解码的方法。
使用一种定义编码一个字段,然后用不同的定义解码同一个字段可能导致:
- 开发者浪费时间调试
- 解析/合并错误(最好的情况)
- 个人身份信息/敏感个人身份信息泄露
- 数据损坏
字段编号重用的常见原因
重编号字段(有时为了实现字段编号顺序更美观)。重编号实际上是删除并重新添加所有涉及的字段,导致不兼容的线路格式变更。
删除一个字段并且没有保留其编号以防止未来重用。
字段编号限制为 29 位而不是 32 位,因为有三位用于指定字段的线路格式。有关更多信息,请参阅编码主题。
指定字段基数
消息字段可以是以下之一:
单一 (Singular):
在 proto2 中,有两种类型的 singular 字段:
optional
:(推荐)一个optional
字段处于两种可能的状态之一:- 该字段已设置,并包含一个被显式设置或从线路中解析出的值。它将被序列化到线路中。
- 该字段未设置,并将返回默认值。它将不会被序列化到线路中。
你可以检查该值是否被显式设置。
required
:不要使用。 Required 字段问题太多,以至于在 proto3 和 editions 中被移除。required 字段的语义应在应用层实现。当它*被*使用时,一个格式良好的消息必须有且仅有一个该字段。
repeated
:此字段类型在一个格式良好的消息中可以重复零次或多次。重复值的顺序将被保留。map
:这是一个键值对字段类型。有关此字段类型的更多信息,请参阅映射。
为新的重复字段使用 Packed 编码
由于历史原因,标量数字类型(例如 int32
, int64
, enum
)的 repeated
字段编码效率不如预期。新代码应使用特殊选项 [packed = true]
以获得更高效的编码。例如:
repeated int32 samples = 4 [packed = true];
repeated ProtoEnum results = 5 [packed = true];
你可以在 Protocol Buffer 编码中找到更多关于 packed
编码的信息。
强烈不推荐使用 Required
重要
Required 是永久性的 如前所述,新字段不应使用required
。required 字段的语义应在应用层实现。现有的 required
字段应被视为消息定义的永久、不可变元素。将字段从 required
安全地更改为 optional
几乎是不可能的。如果存在任何过时的读取器,它会将没有此字段的消息视为不完整,并可能拒绝或丢弃它们。required 字段的第二个问题出现在有人向枚举中添加值时。在这种情况下,未识别的枚举值被视为缺失,这也会导致 required 值检查失败。
格式良好的消息
术语“格式良好”在应用于 protobuf 消息时,指的是序列化/反序列化的字节。protoc 解析器会验证给定的 proto 定义文件是否可解析。
Singular 字段可以在有线格式字节中出现多次。解析器会接受输入,但只有该字段的最后一个实例可以通过生成的绑定访问。有关此主题的更多信息,请参阅后者为准。
添加更多消息类型
可以在单个 .proto
文件中定义多个消息类型。如果您正在定义多个相关的消息,这很有用——例如,如果您想定义与您的 SearchResponse
消息类型相对应的回复消息格式,您可以将其添加到同一个 .proto
文件中:
message SearchRequest {
optional string query = 1;
optional int32 page_number = 2;
optional int32 results_per_page = 3;
}
message SearchResponse {
...
}
组合消息会导致臃肿 虽然可以在单个 .proto
文件中定义多种消息类型(如 message、enum 和 service),但当在单个文件中定义大量具有不同依赖关系的消息时,也可能导致依赖臃肿。建议在每个 .proto
文件中包含尽可能少的消息类型。
添加注释
要为你的 .proto
文件添加注释:
优先使用 C/C++/Java 风格的行尾注释 '//',放在 .proto 代码元素的前一行。
也接受 C 风格的内联/多行注释
/* ... */
。- 使用多行注释时,推荐使用 '*' 作为边距行。
/**
* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response.
*/
message SearchRequest {
optional string query = 1;
// Which page number do we want?
optional int32 page_number = 2;
// Number of results to return per page.
optional int32 results_per_page = 3;
}
删除字段
如果操作不当,删除字段可能会导致严重问题。
不要删除 required
字段。这样做几乎不可能安全。如果您必须删除一个 required
字段,您应该首先将该字段标记为 optional
和 deprecated
,并确保所有以任何方式观察该消息的系统都已部署了新的 schema。然后您可以考虑删除该字段(但请注意,这仍然是一个容易出错的过程)。
当您不再需要一个非 required
的字段时,首先从客户端代码中删除所有对该字段的引用,然后从消息中删除该字段定义。但是,您必须保留已删除的字段编号。如果您不保留该字段编号,开发人员将来可能会重用该编号并导致中断。
你也应该保留字段名称,以允许你的消息的 JSON 和 TextFormat 编码继续解析。
保留字段编号
如果您通过完全删除字段或将其注释掉来更新消息类型,未来的开发人员在对该类型进行自己的更新时可以重用该字段编号。这可能会导致严重问题,如重用字段编号的后果中所述。为确保这种情况不会发生,请将您删除的字段编号添加到 reserved
列表中。
如果未来的开发者试图使用这些保留的字段编号,protoc 编译器将生成错误消息。
message Foo {
reserved 2, 15, 9 to 11;
}
保留的字段编号范围是包含性的(9 to 11
与 9, 10, 11
相同)。
保留字段名
之后重用旧字段名通常是安全的,除非在使用 TextProto 或 JSON 编码时,字段名会被序列化。为避免这种风险,你可以将被删除的字段名添加到 reserved
列表中。
保留名称仅影响 protoc 编译器的行为,而不影响运行时行为,但有一个例外:TextProto 实现可能会在解析时丢弃具有保留名称的未知字段(而不会像其他未知字段那样引发错误)(目前只有 C++ 和 Go 实现这样做)。运行时 JSON 解析不受保留名称的影响。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
请注意,你不能在同一个 reserved
语句中混合使用字段名和字段编号。
从你的 .proto
文件生成了什么?
当您对 .proto
文件运行protocol buffer 编译器时,编译器会以您选择的语言生成您需要的代码,用于处理您在文件中描述的消息类型,包括获取和设置字段值、将消息序列化到输出流以及从输入流解析消息。
- 对于 C++,编译器会从每个
.proto
文件生成一个.h
和一个.cc
文件,其中为你文件中描述的每种消息类型都提供一个类。 - 对于 Java,编译器会为每种消息类型生成一个包含一个类的
.java
文件,以及一个用于创建消息类实例的特殊Builder
类。 - 对于 Kotlin,除了 Java 生成的代码外,编译器还为每种消息类型生成一个
.kt
文件,其中包含一个改进的 Kotlin API。这包括一个简化消息实例创建的 DSL、一个可空字段访问器和一个复制函数。 - Python 有点不同——Python 编译器会生成一个模块,其中包含您
.proto
文件中每种消息类型的静态描述符,然后该描述符与一个*元类 (metaclass)* 一起使用,以在运行时创建必要的 Python 数据访问类。 - 对于 Go,编译器会生成一个
.pb.go
文件,其中包含你文件中每种消息类型的一个类型。 - 对于 Ruby,编译器会生成一个
.rb
文件,其中包含一个 Ruby 模块,该模块包含你的消息类型。 - 对于 Objective-C,编译器会从每个
.proto
文件生成一个pbobjc.h
和pbobjc.m
文件,其中为你文件中描述的每种消息类型都提供一个类。 - 对于 C#,编译器会从每个
.proto
文件生成一个.cs
文件,其中为你文件中描述的每种消息类型都提供一个类。 - 对于 PHP,编译器为您文件中描述的每种消息类型生成一个
.php
消息文件,并为您编译的每个.proto
文件生成一个.php
元数据文件。元数据文件用于将有效的消息类型加载到描述符池中。 - 对于 Dart,编译器会生成一个
.pb.dart
文件,其中包含你文件中每种消息类型的一个类。
你可以通过按照你所选语言的教程来了解更多关于使用每种语言 API 的信息。要了解更详细的 API 信息,请参阅相关的API 参考。
标量值类型
一个标量消息字段可以有以下类型之一——该表显示了在 .proto
文件中指定的类型,以及在自动生成的类中对应的类型:
Proto 类型 | 说明 |
---|---|
double | |
float | |
int32 | 使用可变长度编码。对于编码负数效率低下——如果你的字段可能包含负值,请改用 sint32。 |
int64 | 使用可变长度编码。对于编码负数效率低下——如果你的字段可能包含负值,请改用 sint64。 |
uint32 | 使用可变长度编码。 |
uint64 | 使用可变长度编码。 |
sint32 | 使用可变长度编码。有符号整数值。这些比常规的 int32 更高效地编码负数。 |
sint64 | 使用可变长度编码。有符号整数值。这些比常规的 int64 更高效地编码负数。 |
fixed32 | 总是四个字节。如果值经常大于 228,比 uint32 更高效。 |
fixed64 | 总是八个字节。如果值经常大于 256,比 uint64 更高效。 |
sfixed32 | 总是四个字节。 |
sfixed64 | 总是八个字节。 |
bool | |
string | 字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,且长度不能超过 232。 |
bytes | 可包含任意字节序列,长度不超过 232。 |
Proto 类型 | C++ 类型 | Java/Kotlin 类型[1] | Python 类型[3] | Go 类型 | Ruby 类型 | C# 类型 | PHP 类型 | Dart 类型 | Rust 类型 |
---|---|---|---|---|---|---|---|---|---|
double | double | double | float | *float64 | Float | double | float | double | f64 |
float | float | float | float | *float32 | Float | float | float | double | f32 |
int32 | int32_t | int | int | int32 | Fixnum 或 Bignum (根据需要) | int | integer | *int32 | i32 |
int64 | int64_t | long | int/long[4] | *int64 | Bignum | long | integer/string[6] | Int64 | i64 |
uint32 | uint32_t | int[2] | int/long[4] | *uint32 | Fixnum 或 Bignum (根据需要) | uint | integer | int | u32 |
uint64 | uint64_t | long[2] | int/long[4] | *uint64 | Bignum | ulong | integer/string[6] | Int64 | u64 |
sint32 | int32_t | int | int | int32 | Fixnum 或 Bignum (根据需要) | int | integer | *int32 | i32 |
sint64 | int64_t | long | int/long[4] | *int64 | Bignum | long | integer/string[6] | Int64 | i64 |
fixed32 | uint32_t | int[2] | int/long[4] | *uint32 | Fixnum 或 Bignum (根据需要) | uint | integer | int | u32 |
fixed64 | uint64_t | long[2] | int/long[4] | *uint64 | Bignum | ulong | integer/string[6] | Int64 | u64 |
sfixed32 | int32_t | int | int | *int32 | Fixnum 或 Bignum (根据需要) | int | integer | int | i32 |
sfixed64 | int64_t | long | int/long[4] | *int64 | Bignum | long | integer/string[6] | Int64 | i64 |
bool | bool | boolean | bool | *bool | TrueClass/FalseClass | bool | boolean | bool | bool |
string | string | String | unicode (Python 2), str (Python 3) | *string | String (UTF-8) | string | string | String | ProtoString |
bytes | string | ByteString | bytes | []byte | String (ASCII-8BIT) | ByteString | string | List | ProtoBytes |
[1] 为了确保在混合的 Java/Kotlin 代码库中的兼容性,Kotlin 即使对于无符号类型也使用 Java 对应的类型。
[2] 在 Java 中,无符号 32 位和 64 位整数使用其有符号的对应类型来表示,最高位仅存储在符号位中。
[3] 在所有情况下,为字段设置值都会进行类型检查以确保其有效。
[4] 64 位或无符号 32 位整数在解码时总是表示为 long,但在设置字段时如果给定的是 int,也可以是 int。在所有情况下,设置的值必须符合所表示的类型。参见 [2]。
[5] Proto2 通常从不检查字符串字段的 UTF-8 有效性。不过,不同语言的行为有所不同,不应将无效的 UTF-8 数据存储在字符串字段中。
[6] 在 64 位机器上使用 Integer,在 32 位机器上使用 string。
你可以在 Protocol Buffer 编码中了解更多关于这些类型在序列化消息时的编码方式。
字段默认值
当解析消息时,如果编码的消息字节中不包含某个特定字段,那么在解析的对象中访问该字段将返回该字段的默认值。默认值是类型特定的:
- 对于字符串,默认值是空字符串。
- 对于字节,默认值是空字节。
- 对于布尔值,默认值是 false。
- 对于数值类型,默认值是零。
- 对于消息字段,该字段未被设置。其确切值取决于语言。详情请参阅您所用语言的生成代码指南。
- 对于枚举,默认值是第一个定义的枚举值,该值应为 0(为与开放枚举兼容,建议使用 0)。请参阅枚举默认值。
对于 repeated 字段,默认值是空的(通常是相应语言中的空列表)。
对于 map 字段,默认值是空的(通常是相应语言中的空 map)。
覆盖默认标量值
在 proto2 中,您可以为 singular 的非消息字段指定显式默认值。例如,假设您想为 SearchRequest.results_per_page
字段提供一个默认值 10:
optional int32 results_per_page = 3 [default = 10];
如果发送方未指定 results_per_page
,接收方将观察到以下状态:
results_per_page
字段不存在。也就是说,has_results_per_page()
(hazzer 方法)方法将返回false
。results_per_page
的值(从“getter”返回)是10
。
如果发送方确实为 results_per_page
发送了一个值,则默认值 10 将被忽略,并且发送方的值将从“getter”返回。
有关默认值在生成代码中如何工作的更多细节,请参阅你所选语言的生成代码指南。
因为枚举的默认值是第一个定义的枚举值,所以在向枚举值列表的开头添加值时要小心。有关如何安全更改定义的指南,请参阅更新消息类型部分。
枚举
当您定义消息类型时,您可能希望它的某个字段只能是预定义列表中的值之一。例如,假设您想为每个 SearchRequest
添加一个 corpus
字段,其中 corpus 可以是 UNIVERSAL
、WEB
、IMAGES
、LOCAL
、NEWS
、PRODUCTS
或 VIDEO
。您可以通过在您的消息定义中添加一个 enum
并为每个可能的值定义一个常量来非常简单地实现这一点。
在下面的例子中,我们添加了一个名为 Corpus
的 enum
,包含了所有可能的值,以及一个类型为 Corpus
的字段:
enum Corpus {
CORPUS_UNSPECIFIED = 0;
CORPUS_UNIVERSAL = 1;
CORPUS_WEB = 2;
CORPUS_IMAGES = 3;
CORPUS_LOCAL = 4;
CORPUS_NEWS = 5;
CORPUS_PRODUCTS = 6;
CORPUS_VIDEO = 7;
}
message SearchRequest {
optional string query = 1;
optional int32 page_number = 2;
optional int32 results_per_page = 3;
optional Corpus corpus = 4;
}
枚举默认值
SearchRequest.corpus
字段的默认值是 CORPUS_UNSPECIFIED
,因为这是枚举中定义的第一个值。
强烈建议将每个枚举的第一个值定义为 `ENUM_TYPE_NAME_UNSPECIFIED = 0;` 或 `ENUM_TYPE_NAME_UNKNOWN = 0;`。这是因为 proto2 处理枚举字段未知值的方式。
还建议这个第一个默认值除了“此值未指定”外,不具有任何语义含义。
像 `SearchRequest.corpus` 这样的枚举字段的默认值可以像这样显式覆盖:
optional Corpus corpus = 4 [default = CORPUS_UNIVERSAL];
枚举值别名
您可以通过为不同的枚举常量赋予相同的值来定义别名。为此,您需要将 `allow_alias` 选项设置为 `true`。否则,当发现别名时,protocol buffer 编译器会生成一条警告消息。虽然所有别名值在序列化时都有效,但在反序列化时只使用第一个值。
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 1;
EAA_FINISHED = 2;
}
enum EnumNotAllowingAlias {
ENAA_UNSPECIFIED = 0;
ENAA_STARTED = 1;
// ENAA_RUNNING = 1; // Uncommenting this line will cause a warning message.
ENAA_FINISHED = 2;
}
枚举器常量必须在 32 位整数的范围内。由于 `enum` 值在传输时使用 varint 编码,负值效率低下,因此不推荐使用。您可以在消息定义中定义 `enum`,如前面的示例所示,也可以在外部定义——这些 `enum` 可以在您的 `.proto` 文件中的任何消息定义中重用。您还可以使用在一个消息中声明的 `enum` 类型作为另一个消息中字段的类型,使用语法 `_MessageType_._EnumType_`。
当您对使用 `enum` 的 `.proto` 文件运行 protocol buffer 编译器时,生成的代码将为 Java、Kotlin 或 C++ 提供相应的 `enum`,或者为 Python 提供一个特殊的 `EnumDescriptor` 类,用于在运行时生成的类中创建一组带有整数值的符号常量。
重要
生成的代码可能会受到特定语言对枚举器数量的限制(对于某种语言来说是几千个)。请查看你计划使用的语言的限制。重要
有关枚举应如何工作与它们目前在不同语言中如何工作的对比信息,请参阅枚举行为。删除枚举值对于持久化的 proto 来说是一个破坏性更改。与其删除一个值,不如用 `reserved` 关键字标记该值,以防止该枚举值被代码生成,或者保留该值但通过使用 `deprecated` 字段选项来表明它将在以后被删除:
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3 [deprecated = true];
reserved 4,5;
}
有关如何在你的应用程序中使用消息 enum
的更多信息,请参阅你所选语言的生成代码指南。
保留值
如果您通过完全删除一个枚举条目或将其注释掉来更新一个枚举类型,未来的用户在对该类型进行自己的更新时可以重用该数值。如果他们以后加载同一 .proto
的旧实例,这可能会导致严重问题,包括数据损坏、隐私漏洞等。确保这种情况不发生的一种方法是指定您删除的条目的数值(和/或名称,名称也可能导致 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 {
optional string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
导入定义
在前面的例子中,Result
消息类型与 SearchResponse
在同一个文件中定义——如果你想用作字段类型的消息类型已经定义在另一个 .proto
文件中,该怎么办?
你可以通过导入来使用其他 .proto
文件中的定义。要导入另一个 .proto
的定义,你在文件顶部添加一个 import 语句:
import "myproject/other_protos.proto";
默认情况下,您只能使用直接导入的 .proto
文件中的定义。然而,有时您可能需要将一个 .proto
文件移动到新位置。您可以不在一次更改中直接移动 .proto
文件并更新所有调用点,而是在旧位置放置一个占位符 .proto
文件,使用 import public
概念将所有导入转发到新位置。
请注意,public import 功能在 Java、Kotlin、TypeScript、JavaScript、GCL 以及使用 protobuf 静态反射的 C++ 目标中不可用。
import public
依赖可以被任何导入包含 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_path` 标志设置为项目的根目录,并对所有导入使用完全限定名称。
使用 proto3 消息类型
可以导入 proto3 和 edition 2023 的消息类型并在您的 proto2 消息中使用它们,反之亦然。但是,proto2 枚举不能直接在 proto3 语法中使用(如果导入的 proto2 消息使用它们则可以)。
嵌套类型
你可以在其他消息类型内部定义和使用消息类型,如下例所示——这里的 Result
消息是在 SearchResponse
消息内部定义的。
message SearchResponse {
message Result {
optional string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果你想在其父消息类型之外重用此消息类型,你可以通过 _Parent_._Type_
的方式引用它:
message SomeOtherMessage {
optional SearchResponse.Result result = 1;
}
你可以随心所欲地嵌套消息。在下面的例子中,请注意两个名为 Inner
的嵌套类型是完全独立的,因为它们是在不同的消息中定义的:
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
optional int64 ival = 1;
optional bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
optional int32 ival = 1;
optional bool booly = 2;
}
}
}
Groups
请注意,groups 功能已被弃用,在创建新消息类型时不应使用。请改用嵌套消息类型。
Groups 是在您的消息定义中嵌套信息的另一种方式。例如,指定一个包含多个 `Result` 的 `SearchResponse` 的另一种方法如下:
message SearchResponse {
repeated group Result = 1 {
optional string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
}
一个 group 只是将一个嵌套消息类型和一个字段组合成一个单一的声明。在您的代码中,您可以将此消息视为它有一个名为 `result` 的 `Result` 类型字段(后一个名称被转换为小写,以免与前者冲突)。因此,这个例子与之前的 `SearchResponse` 完全等价,只是消息具有不同的有线格式。
更新消息类型
如果现有的消息类型不再满足您的所有需求——例如,您希望消息格式有一个额外的字段——但您仍然想使用旧格式创建的代码,别担心!当您使用二进制有线格式时,更新消息类型非常简单,不会破坏任何现有代码。
注意
如果您使用 ProtoJSON 或 proto 文本格式来存储您的 protocol buffer 消息,那么您可以在 proto 定义中做的更改是不同的。ProtoJSON 有线格式的安全更改在这里描述。请查阅 Proto 最佳实践和以下规则:
二进制线路非安全变更
有线不安全 (Wire-unsafe) 的更改是指,如果您使用新的 schema 解析器来解析使用旧 schema 序列化的数据(或反之),将会导致中断的 schema 更改。只有在您知道数据的所有序列化器和反序列化器都使用新的 schema 时,才进行有线不安全的更改。
- 更改任何现有字段的字段编号是不安全的。
- 更改字段编号等同于删除该字段并添加一个具有相同类型的新字段。如果你想重编号一个字段,请参阅删除字段的说明。
- 将字段移入一个现有的
oneof
是不安全的。
二进制线路安全变更
线路安全的变更是指完全安全地演进模式,而不会有数据丢失或新的解析失败的风险。
请注意,任何有线安全 (wire-safe) 的更改对于给定语言的应用代码来说都可能是破坏性更改。例如,向一个已有的枚举中添加一个值,对于任何对该枚举进行穷尽式 switch 的代码来说,都会导致编译中断。因此,Google 可能会避免在公共消息上进行这类更改:AIPs 中包含了关于哪些更改是安全的指导意见。
- 添加新字段是安全的。
- 移除字段是安全的。
- 在您更新的消息类型中,不得再次使用相同的字段编号。您可能需要重命名字段,例如添加前缀“OBSOLETE_”,或者将字段编号设为保留,这样您
.proto
的未来用户就不会意外地重用该编号。
- 在您更新的消息类型中,不得再次使用相同的字段编号。您可能需要重命名字段,例如添加前缀“OBSOLETE_”,或者将字段编号设为保留,这样您
- 向枚举添加额外的值是安全的。
- 将一个单一的显式存在字段或扩展变更为一个新的
oneof
的成员是安全的。 - 将一个只包含一个字段的
oneof
更改为显式存在字段是安全的。 - 将一个字段更改为具有相同编号和类型的扩展是安全的。
二进制线路兼容变更(有条件安全)
与有线安全 (Wire-safe) 的更改不同,有线兼容 (wire-compatible) 意味着相同的数据在给定更改前后都可以被解析。然而,在这种类型的更改下,数据的解析可能会是有损的。例如,将一个 int32 更改为 int64 是一个兼容的更改,但是如果写入了一个大于 INT32_MAX 的值,一个将其作为 int32 读取的客户端将丢弃该数字的高位比特。
只有在您仔细管理系统推出过程的情况下,才能对您的 schema 进行兼容性更改。例如,您可以将 int32 更改为 int64,但要确保在新的 schema 部署到所有端点之前,您继续只写入合法的 int32 值,然后在之后才开始写入更大的值。
如果你的模式是在组织外部发布的,通常不应该进行线路兼容的更改,因为你无法管理新模式的部署,从而无法知道何时使用不同范围的值是安全的。
int32
、uint32
、int64
、uint64
和bool
都是兼容的。- 如果从线路中解析出一个不适合相应类型的数字,你将得到与在 C++ 中将该数字强制转换为该类型相同的效果(例如,如果一个 64 位数字被作为 int32 读取,它将被截断为 32 位)。
sint32
和sint64
彼此兼容,但与其他整数类型不兼容。- 如果写入的值在 INT_MIN 和 INT_MAX(含)之间,那么用任一类型解析都会得到相同的值。如果写入了一个超出该范围的 sint64 值,并被解析为 sint32,则 varint 会被截断为 32 位,然后进行 zigzag 解码(这将导致观察到不同的值)。
- 只要字节是有效的 UTF-8,
string
和bytes
就是兼容的。 - 如果字节包含消息的编码实例,则嵌入式消息与
bytes
兼容。 fixed32
与sfixed32
兼容,fixed64
与sfixed64
兼容。- 对于
string
、bytes
和消息字段,singular 与repeated
兼容。- 对于重复字段的序列化数据作为输入,期望该字段为 singular 的客户端,如果它是原始类型字段,将取最后一个输入值;如果它是消息类型字段,将合并所有输入元素。请注意,这对于数字类型(包括布尔值和枚举)通常是不安全的。数字类型的重复字段默认以打包格式序列化,当期望的是 singular 字段时,将无法正确解析。
enum
与int32
、uint32
、int64
和uint64
兼容。- 请注意,当消息被反序列化时,客户端代码可能会以不同的方式处理它们:例如,未识别的 proto3 `enum` 值将保留在消息中,但当消息被反序列化时,其表示方式是依赖于语言的。
- 在
map<K, V>
和相应的repeated
消息字段之间更改字段是二进制兼容的(有关消息布局和其他限制,请参阅下面的映射)。- 然而,更改的安全性取决于应用程序:在反序列化和重新序列化消息时,使用 `repeated` 字段定义的客户端将产生语义上相同的结果;但是,使用 `map` 字段定义的客户端可能会重新排序条目并丢弃具有重复键的条目。
未知字段
未知字段是格式良好的 protocol buffer 序列化数据,表示解析器无法识别的字段。例如,当一个旧的二进制文件解析一个由新的二进制文件发送的带有新字段的数据时,这些新字段在旧的二进制文件中就成为未知字段。
最初,proto3 消息在解析过程中总是丢弃未知字段,但在 3.5 版本中,我们重新引入了未知字段的保留,以匹配 proto2 的行为。在 3.5 及更高版本中,未知字段在解析过程中会被保留,并包含在序列化的输出中。
保留未知字段
一些操作可能导致未知字段丢失。例如,如果你执行以下操作之一,未知字段将丢失:
- 将 proto 序列化为 JSON。
- 遍历消息中的所有字段以填充一个新消息。
为避免丢失未知字段,请执行以下操作:
- 使用二进制格式;避免使用文本格式进行数据交换。
- 使用面向消息的 API,如
CopyFrom()
和MergeFrom()
,来复制数据,而不是逐个字段复制。
TextFormat 是一个有点特殊的情况。序列化为 TextFormat 会使用字段编号打印未知字段。但如果存在使用字段编号的条目,将 TextFormat 数据解析回二进制 proto 会失败。
扩展
扩展是在其容器消息之外定义的字段;通常在一个与容器消息的 .proto
文件不同的 .proto
文件中。
为什么要使用扩展?
使用扩展主要有两个原因:
- 容器消息的 `.proto` 文件将有更少的导入/依赖。这可以提高构建时间,打破循环依赖,并以其他方式促进松散耦合。扩展对此非常有效。
- 允许系统以最少的依赖和协调将数据附加到容器消息中。由于字段编号空间有限以及重用字段编号的后果,扩展对于这一点并不是一个很好的解决方案。如果您的用例需要对大量扩展进行非常低的协调,请考虑使用 `Any` 消息类型。
扩展示例
让我们看一个扩展的例子:
// file kittens/video_ext.proto
import "kittens/video.proto";
import "media/user_content.proto";
package kittens;
// This extension allows kitten videos in a media.UserContent message.
extend media.UserContent {
// Video is a message imported from kittens/video.proto
repeated Video kitten_videos = 126;
}
请注意,定义扩展的文件 (`kittens/video_ext.proto`) 导入了容器消息的文件 (`media/user_content.proto`)。
容器消息必须为其字段编号的一个子集保留用于扩展。
// file media/user_content.proto
package media;
// A container message to hold stuff that a user has created.
message UserContent {
// Set verification to `DECLARATION` to enforce extension declarations for all
// extensions in this range.
extensions 100 to 199 [verification = DECLARATION];
}
容器消息的文件(`media/user_content.proto`)定义了消息 `UserContent`,它为扩展保留了字段编号 [100 到 199]。建议为该范围设置 `verification = DECLARATION`,以要求其所有扩展都必须有声明。
当新的扩展 (`kittens/video_ext.proto`) 被添加时,应该在 `UserContent` 中添加一个相应的声明,并且应该移除 `verification`。
// A container message to hold stuff that a user has created.
message UserContent {
extensions 100 to 199 [
declaration = {
number: 126,
full_name: ".kittens.kitten_videos",
type: ".kittens.Video",
repeated: true
}
];
}
`UserContent` 声明字段编号 `126` 将被一个 `repeated` 扩展字段使用,其完全限定名为 `.kittens.kitten_videos`,完全限定类型为 `.kittens.Video`。要了解更多关于扩展声明的信息,请参阅扩展声明。
请注意,容器消息的文件(`media/user_content.proto`)**没有**导入 kitten_video 扩展定义(`kittens/video_ext.proto`)。
扩展字段的有线格式编码与具有相同字段编号、类型和基数的标准字段没有区别。因此,将标准字段移出其容器成为扩展,或将扩展字段移入其容器消息作为标准字段是安全的,只要字段编号、类型和基数保持不变。
然而,由于扩展是在容器消息之外定义的,因此不会生成用于获取和设置特定扩展字段的专用访问器。对于我们的例子,protobuf 编译器**不会生成** `AddKittenVideos()` 或 `GetKittenVideos()` 访问器。相反,扩展是通过参数化函数访问的,例如:`HasExtension()`、`ClearExtension()`、`GetExtension()`、`MutableExtension()` 和 `AddExtension()`。
在 C++ 中,它看起来会是这样:
UserContent user_content;
user_content.AddExtension(kittens::kitten_videos, new kittens::Video());
assert(1 == user_content.GetExtensionCount(kittens::kitten_videos));
user_content.GetExtension(kittens::kitten_videos, 0);
定义扩展范围
如果您是容器消息的所有者,您将需要为您的消息的扩展定义一个扩展范围。
分配给扩展字段的字段编号不能被标准字段重用。
定义后扩展扩展范围是安全的。一个好的默认做法是分配 1000 个相对较小的数字,并使用扩展声明密集地填充该空间:
message ModernExtendableMessage {
// All extensions in this range should use extension declarations.
extensions 1000 to 2000 [verification = DECLARATION];
}
在添加实际扩展之前为扩展声明添加范围时,您应该添加 `verification = DECLARATION` 以强制此新范围使用声明。一旦添加了实际的声明,就可以移除此占位符。
将一个现有的扩展范围拆分成覆盖相同总范围的多个独立范围是安全的。这对于将旧版消息类型迁移到扩展声明可能是必要的。例如,在迁移之前,范围可能定义为:
message LegacyMessage {
extensions 1000 to max;
}
迁移后(拆分范围),它可以是:
message LegacyMessage {
// Legacy range that was using an unverified allocation scheme.
extensions 1000 to 524999999 [verification = UNVERIFIED];
// Current range that uses extension declarations.
extensions 525000000 to max [verification = DECLARATION];
}
增加起始字段编号或减少结束字段编号来移动或缩小扩展范围是不安全的。这些更改可能会使现有扩展失效。
对于在大多数 proto 实例中都会填充的标准字段,优先使用 1 到 15 的字段编号。不建议将这些编号用于扩展。
如果您的编号约定可能涉及扩展具有非常大的字段编号,您可以使用 `max` 关键字指定您的扩展范围直至可能的最大字段编号:
message Foo {
extensions 1000 to max;
}
`max` 是 229 - 1,即 536,870,911。
选择扩展编号
扩展只是可以在其容器消息之外指定的字段。所有适用于分配字段编号的规则也适用于扩展字段编号。同样的重用字段编号的后果也适用于重用扩展字段编号。
如果容器消息使用扩展声明,选择唯一的扩展字段编号就很简单。在定义新扩展时,选择容器消息中定义的最高扩展范围中所有其他声明之上的最低字段编号。例如,如果一个容器消息定义如下:
message Container {
// Legacy range that was using an unverified allocation scheme
extensions 1000 to 524999999;
// Current range that uses extension declarations. (highest extension range)
extensions 525000000 to max [
declaration = {
number: 525000001,
full_name: ".bar.baz_ext",
type: ".bar.Baz"
}
// 525,000,002 is the lowest field number above all other declarations
];
}
`Container` 的下一个扩展应该添加一个编号为 `525000002` 的新声明。
未经验证的扩展编号分配(不推荐)
容器消息的所有者可以选择放弃扩展声明,转而使用他们自己的未经核实的扩展编号分配策略。
未经验证的分配方案使用 protobuf 生态系统之外的机制来在所选扩展范围内分配扩展字段编号。一个例子可能是使用单体仓库的提交号。从 protobuf 编译器的角度来看,这个系统是“未经验证的”,因为无法检查扩展是否使用了正确获取的扩展字段编号。
与扩展声明等经过验证的系统相比,未经验证的系统的好处是能够在不与容器消息所有者协调的情况下定义扩展。
一个未经验证系统的缺点是,protobuf 编译器无法保护参与者免于重用扩展字段编号。
不推荐使用未经验证的扩展字段编号分配策略,因为重用字段编号的后果会落在消息的所有扩展者身上(不仅仅是未遵循建议的开发者)。如果您的用例需要非常低的协调,请考虑使用 `Any` 消息。
未经验证的扩展字段编号分配策略仅限于 1 到 524,999,999 的范围。字段编号 525,000,000 及以上只能与扩展声明一起使用。
指定扩展类型
扩展可以是除 `oneof` 和 `map` 之外的任何字段类型。
嵌套扩展(不推荐)
您可以在另一个消息的作用域内声明扩展:
import "common/user_profile.proto";
package puppies;
message Photo {
extend common.UserProfile {
optional int32 likes_count = 111;
}
...
}
在这种情况下,访问此扩展的 C++ 代码是:
UserProfile user_profile;
user_profile.SetExtension(puppies::Photo::likes_count, 42);
换句话说,唯一的效果是 `likes_count` 被定义在了 `puppies.Photo` 的作用域内。
这是一个常见的困惑来源:在消息类型内嵌套声明一个 `extend` 块*并不*意味着外部类型和扩展类型之间有任何关系。特别是,前面的例子*并不*意味着 `Photo` 是 `UserProfile` 的任何类型的子类。它只意味着符号 `likes_count` 是在 `Photo` 的作用域内声明的;它只是一个静态成员。
一种常见的模式是在扩展字段类型的作用域内定义扩展——例如,这里有一个 `media.UserContent` 的扩展,类型为 `puppies.Photo`,其中扩展被定义为 `Photo` 的一部分:
import "media/user_content.proto";
package puppies;
message Photo {
extend media.UserContent {
optional Photo puppy_photo = 127;
}
...
}
但是,并没有要求具有消息类型的扩展必须在该类型内部定义。您也可以使用标准的定义模式:
import "media/user_content.proto";
package puppies;
message Photo {
...
}
// This can even be in a different file.
extend media.UserContent {
optional Photo puppy_photo = 127;
}
为了避免混淆,首选这种标准的(文件级)语法。不熟悉扩展的用户常常将嵌套语法误认为是子类化。
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 google::protobuf::Any& detail : status.details()) {
if (detail.Is<NetworkErrorDetails>()) {
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
... processing network_error ...
}
}
如果您想将包含的消息限制在少数几种类型中,并且要求在向列表中添加新类型之前获得许可,请考虑使用扩展和扩展声明,而不是 `Any` 消息类型。
Oneof
如果您有一个包含许多可选字段的消息,并且在同一时间最多只会设置一个字段,您可以使用 oneof 功能来强制执行此行为并节省内存。
Oneof 字段类似于 optional 字段,只是 oneof 中的所有字段共享内存,并且在同一时间最多只能设置一个字段。设置 oneof 的任何成员都会自动清除所有其他成员。您可以使用特殊的 `case()` 或 `WhichOneof()` 方法来检查 oneof 中设置了哪个值(如果有的话),具体取决于您选择的语言。
请注意,如果*设置了多个值,则由 proto 中的顺序决定的最后一个设置的值将覆盖所有先前的值*。
oneof 字段的字段编号必须在封闭消息中是唯一的。
使用 Oneof
要在你的 .proto
文件中定义一个 oneof,你使用 oneof
关键字,后跟你 oneof 的名称,在本例中是 test_oneof
:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
然后,您将 oneof 字段添加到 oneof 定义中。您可以添加任何类型的字段,除了 `map` 字段,但不能使用 `required`、`optional` 或 `repeated` 关键字。如果需要向 oneof 中添加一个 repeated 字段,您可以使用一个包含该 repeated 字段的消息。
在您生成的代码中,oneof 字段具有与常规 `optional` 字段相同的 getter 和 setter。您还会得到一个特殊的方法,用于检查 oneof 中设置了哪个值(如果有的话)。您可以在相关的API 参考中找到更多关于您所选语言的 oneof API 的信息。
Oneof 特性
设置一个 oneof 字段将自动清除 oneof 的所有其他成员。所以如果你设置了几个 oneof 字段,只有你设置的*最后一个*字段仍然有值。
SampleMessage message; message.set_name("name"); CHECK(message.has_name()); // Calling mutable_sub_message() will clear the name field and will set // sub_message to a new instance of SubMessage with none of its fields set. message.mutable_sub_message(); CHECK(!message.has_name());
如果解析器在传输线上遇到同一个 oneof 的多个成员,则在解析后的消息中只使用看到的最后一个成员。在解析传输线上的数据时,从字节的开头开始,评估下一个值,并应用以下解析规则:
首先,检查同一个 oneof 中的*另一个*字段当前是否已设置,如果是,则清除它。
然后像该字段不在 oneof 中一样应用内容:
- 一个基本类型将覆盖任何已设置的值。
- 一个消息将合并到任何已设置的值中。
oneof 不支持扩展。
一个 oneof 不能是
repeated
。反射 API 适用于 oneof 字段。
如果你将一个 oneof 字段设置为默认值(例如将一个 int32 oneof 字段设置为 0),该 oneof 字段的“case”将被设置,并且该值将被序列化到线路上。
如果你正在使用 C++,请确保你的代码不会导致内存崩溃。下面的示例代码会崩溃,因为
sub_message
已经通过调用set_name()
方法被删除了。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 的成员。
标签重用问题
- 将 optional 字段移入或移出 oneof:在消息序列化和解析后,您可能会丢失部分信息(某些字段将被清除)。但是,您可以安全地将单个字段移入一个**新的** oneof,并且如果已知只有一个字段会被设置,则可能可以移动多个字段。更多详情请参阅更新消息类型。
- 删除一个 oneof 字段再加回来:这可能会在消息被序列化和解析后清除你当前设置的 oneof 字段。
- 拆分或合并 oneof:这与移动 `optional` 字段有类似的问题。
映射
如果你想在数据定义中创建关联映射,protocol buffers 提供了一种方便的快捷语法:
map<key_type, value_type> map_field = N;
...其中 `key_type` 可以是任何整数或字符串类型(即,除浮点类型和 `bytes` 之外的任何标量类型)。请注意,枚举和 proto 消息都不能作为 `key_type`。`value_type` 可以是除另一个 map 之外的任何类型。
因此,举例来说,如果你想创建一个项目映射,其中每个 Project
消息都与一个字符串键相关联,你可以这样定义它:
map<string, Project> projects = 3;
映射特性
- map 不支持扩展。
- Map 不能是 `repeated`、`optional` 或 `required`。
- 映射值的线路格式顺序和映射迭代顺序是未定义的,所以你不能依赖你的映射项会以特定的顺序排列。
- 为
.proto
生成文本格式时,映射按键排序。数字键按数值排序。 - 从线路解析或合并时,如果存在重复的映射键,则使用最后看到的键。从文本格式解析映射时,如果存在重复的键,解析可能会失败。
- 如果你为一个 map 字段提供了键但没有提供值,该字段被序列化时的行为取决于语言。在 C++、Java、Kotlin 和 Python 中,会序列化该类型的默认值,而在其他语言中则什么也不序列化。
- 符号
FooEntry
不能与 mapfoo
存在于同一作用域,因为FooEntry
已经被 map 的实现使用了。
生成的 map API 目前可用于所有支持的语言。你可以在相关的 API 参考中找到更多关于你所选语言的 map API 的信息。
向后兼容性
map 语法在线路上等同于以下内容,因此不支持 map 的 protocol buffers 实现仍然可以处理你的数据:
message MapFieldEntry {
optional key_type key = 1;
optional value_type value = 2;
}
repeated MapFieldEntry map_field = N;
任何支持映射的 protocol buffers 实现都必须既能生成也能接受可被先前定义接受的数据。
包(Packages)
你可以向 .proto
文件添加一个可选的 package
说明符,以防止协议消息类型之间的名称冲突。
package foo.bar;
message Open { ... }
然后你可以在定义你的消息类型的字段时使用包说明符:
message Foo {
...
optional foo.bar.Open open = 1;
...
}
包说明符影响生成代码的方式取决于你选择的语言:
- 在 C++ 中,生成的类被包裹在一个 C++ 命名空间内。例如,
Open
会在命名空间foo::bar
中。 - 在 Java 和 Kotlin 中,除非你在你的
.proto
文件中明确提供一个option java_package
,否则该包将用作 Java 包。 - 在 Python 中,
package
指令被忽略,因为 Python 模块是根据它们在文件系统中的位置来组织的。 - 在 Go 中,`package` 指令被忽略,生成的 `.pb.go` 文件位于以相应 `go_proto_library` Bazel 规则命名的包中。对于开源项目,您**必须**提供一个 `go_package` 选项或设置 Bazel `-M` 标志。
- 在 Ruby 中,生成的类被包装在嵌套的 Ruby 命名空间内,并转换为所需的 Ruby 大写风格(首字母大写;如果第一个字符不是字母,则前缀为 `PB_`)。例如,`Open` 将位于 `Foo::Bar` 命名空间中。
- 在 PHP 中,除非您在
.proto
文件中明确提供了 `option php_namespace`,否则 package 在转换为 PascalCase 后被用作命名空间。例如,`Open` 将位于 `Foo\Bar` 命名空间中。 - 在 C# 中,除非您在 `.proto` 文件中明确提供 `option csharp_namespace`,否则 package 在转换为 PascalCase 后将用作命名空间。例如,`Open` 将位于 `Foo.Bar` 命名空间中。
请注意,即使 `package` 指令不直接影响生成的代码(例如在 Python 中),仍然强烈建议为 .proto
文件指定包,否则可能导致描述符中的命名冲突,并使 proto 对其他语言不可移植。
包和名称解析
protocol buffer 语言中的类型名称解析方式类似于 C++:首先搜索最内层的作用域,然后是次内层,以此类推,每个包都被视为其父包的“内层”。前导的点“.”(例如,`.foo.bar.Baz`)表示从最外层作用域开始。
protocol buffer 编译器通过解析导入的 .proto
文件来解析所有类型名称。每种语言的代码生成器都知道如何在该语言中引用每种类型,即使它有不同的作用域规则。
定义服务
如果您想将您的消息类型与 RPC(远程过程调用)系统一起使用,您可以在 .proto
文件中定义一个 RPC 服务接口,protocol buffer 编译器将以您选择的语言生成服务接口代码和存根 (stub)。因此,举例来说,如果您想定义一个 RPC 服务,其方法接受您的 SearchRequest
并返回一个 SearchResponse
,您可以在您的 .proto
文件中这样定义:
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
默认情况下,protocol 编译器将生成一个名为 `SearchService` 的抽象接口和一个相应的“存根 (stub)”实现。存根将所有调用转发到一个 `RpcChannel`,而 `RpcChannel` 本身是一个抽象接口,您必须根据自己的 RPC 系统来定义它。例如,您可以实现一个 `RpcChannel`,它将消息序列化并通过 HTTP 发送到服务器。换句话说,生成的存根提供了一个类型安全的接口,用于进行基于 protocol-buffer 的 RPC 调用,而不会将您锁定在任何特定的 RPC 实现中。因此,在 C++ 中,您最终可能会得到这样的代码:
using google::protobuf;
protobuf::RpcChannel* channel;
protobuf::RpcController* controller;
SearchService* service;
SearchRequest request;
SearchResponse response;
void DoSearch() {
// You provide classes MyRpcChannel and MyRpcController, which implement
// the abstract interfaces protobuf::RpcChannel and protobuf::RpcController.
channel = new MyRpcChannel("somehost.example.com:1234");
controller = new MyRpcController;
// The protocol compiler generates the SearchService class based on the
// definition given earlier.
service = new SearchService::Stub(channel);
// Set up the request.
request.set_query("protocol buffers");
// Execute the RPC.
service->Search(controller, &request, &response,
protobuf::NewCallback(&Done));
}
void Done() {
delete service;
delete channel;
delete controller;
}
所有服务类也实现了 `Service` 接口,该接口提供了一种在编译时不知道方法名称或其输入和输出类型的情况下调用特定方法的方式。在服务器端,这可以用来实现一个 RPC 服务器,您可以向其注册服务。
using google::protobuf;
class ExampleSearchService : public SearchService {
public:
void Search(protobuf::RpcController* controller,
const SearchRequest* request,
SearchResponse* response,
protobuf::Closure* done) {
if (request->query() == "google") {
response->add_result()->set_url("http://www.google.com");
} else if (request->query() == "protocol buffers") {
response->add_result()->set_url("http://protobuf.googlecode.com");
}
done->Run();
}
};
int main() {
// You provide class MyRpcServer. It does not have to implement any
// particular interface; this is just an example.
MyRpcServer server;
protobuf::Service* service = new ExampleSearchService;
server.ExportOnPort(1234, service);
server.Run();
delete service;
return 0;
}
如果您不想接入自己现有的 RPC 系统,可以使用 gRPC:一个由 Google 开发的、语言和平台中立的开源 RPC 系统。gRPC 与 protocol buffers 配合得特别好,它允许您使用一个特殊的 protocol buffer 编译器插件直接从您的 .proto
文件生成相关的 RPC 代码。但是,由于使用 proto2 和 proto3 生成的客户端和服务器之间存在潜在的兼容性问题,我们建议您使用 proto3 或 edition 2023 来定义 gRPC 服务。您可以在 Proto3 语言指南中找到更多关于 proto3 语法的信息,在 Edition 2023 语言指南中找到更多关于 edition 2023 的信息。
除了 gRPC,还有一些正在进行的第三方项目,旨在为 Protocol Buffers 开发 RPC 实现。有关我们所知项目的链接列表,请参阅第三方附加组件维基页面。
JSON 映射
标准的 protobuf 二进制有线格式是使用 protobuf 的两个系统之间通信的首选序列化格式。对于与使用 JSON 而非 protobuf 有线格式的系统通信,Protobuf 支持一种规范的 JSON 编码。
选项
.proto
文件中的单个声明可以用许多*选项*来注解。选项不会改变声明的整体含义,但可能会影响它在特定上下文中的处理方式。可用选项的完整列表定义在 /google/protobuf/descriptor.proto
中。
一些选项是文件级选项,意味着它们应该写在顶层作用域,而不是任何消息、枚举或服务定义内部。一些选项是消息级选项,意味着它们应该写在消息定义内部。一些选项是字段级选项,意味着它们应该写在字段定义内部。选项也可以写在枚举类型、枚举值、oneof 字段、服务类型和服务方法上;然而,目前对于这些都没有任何有用的选项。
以下是一些最常用的选项:
java_package
(文件选项): 您希望用于生成的 Java/Kotlin 类的包名。如果在.proto
文件中没有明确给出java_package
选项,那么默认情况下将使用 proto 包(在.proto
文件中使用“package”关键字指定)。然而,proto 包通常不是好的 Java 包,因为 proto 包不要求以反向域名开头。如果不生成 Java 或 Kotlin 代码,此选项无效。option java_package = "com.example.foo";
java_outer_classname
(文件选项): 您想要生成的包装器 Java 类的类名(因此也是文件名)。如果在.proto
文件中没有明确指定java_outer_classname
,类名将通过将.proto
文件名转换为驼峰式来构造(所以foo_bar.proto
变为FooBar.java
)。如果java_multiple_files
选项被禁用,那么为.proto
文件生成的所有其他类/枚举等都将作为嵌套类/枚举等生成在这个外部包装器 Java 类*内部*。如果不生成 Java 代码,此选项无效。option java_outer_classname = "Ponycopter";
java_multiple_files
(文件选项):如果为 false,则只会为此.proto
文件生成一个.java
文件,并且为顶层消息、服务和枚举生成的所有 Java 类/枚举/等都将嵌套在一个外部类中(参见 `java_outer_classname`)。如果为 true,则会为顶层消息、服务和枚举生成的每个 Java 类/枚举/等生成单独的.java
文件,并且为此.proto
文件生成的包装器 Java 类将不包含任何嵌套的类/枚举/等。这是一个布尔选项,默认为 `false`。如果不生成 Java 代码,此选项无效。option java_multiple_files = true;
optimize_for
(文件选项):可以设置为SPEED
、CODE_SIZE
或LITE_RUNTIME
。这会以下列方式影响 C++ 和 Java 代码生成器(以及可能的第三方生成器):SPEED
(默认): protocol buffer 编译器将生成用于序列化、解析以及对你的消息类型执行其他常见操作的代码。此代码经过高度优化。CODE_SIZE
:protocol buffer 编译器将生成最小化的类,并依赖于共享的、基于反射的代码来实现序列化、解析和各种其他操作。因此,生成的代码将比使用 `SPEED` 时小得多,但操作会更慢。类仍然会实现与 `SPEED` 模式下完全相同的公共 API。此模式在包含大量.proto
文件且不需要所有文件都快如闪电的应用中最为有用。LITE_RUNTIME
:protocol buffer 编译器将生成仅依赖于“lite”运行时库(libprotobuf-lite
而不是libprotobuf
)的类。lite 运行时比完整库小得多(大约小一个数量级),但省略了某些功能,如描述符和反射。这对于在受限平台(如手机)上运行的应用特别有用。编译器仍将像在 `SPEED` 模式下一样生成所有方法的快速实现。生成的类在每种语言中只会实现 `MessageLite` 接口,该接口仅提供完整 `Message` 接口方法的一个子集。
option optimize_for = CODE_SIZE;
cc_generic_services
,java_generic_services
,py_generic_services
(文件选项): 通用服务已被弃用。 protocol buffer 编译器是否应分别基于 C++、Java 和 Python 中的服务定义生成抽象服务代码。由于历史原因,这些默认为 `true`。然而,自 2.3.0 版本(2010年1月)起,RPC 实现更倾向于提供代码生成器插件来生成更特定于每个系统的代码,而不是依赖于“抽象”服务。// This file relies on plugins to generate service code. option cc_generic_services = false; option java_generic_services = false; option py_generic_services = false;
cc_enable_arenas
(文件选项): 为 C++ 生成的代码启用 arena 分配。objc_class_prefix
(文件选项): 设置 Objective-C 类前缀,该前缀会添加到此 .proto 生成的所有 Objective-C 类和枚举之前。没有默认值。您应该使用 3-5 个大写字符的前缀,正如 Apple 推荐的那样。请注意,所有 2 个字母的前缀都由 Apple 保留。message_set_wire_format
(消息选项): 如果设置为 `true`,该消息将使用一种不同的二进制格式,旨在与 Google 内部使用的一种名为 `MessageSet` 的旧格式兼容。Google 以外的用户可能永远不需要使用此选项。该消息必须完全按如下方式声明:message Foo { option message_set_wire_format = true; extensions 4 to max; }
packed
(字段选项): 如果在一个基本数字类型的重复字段上设置为 `true`,它会导致使用更紧凑的编码。不使用此选项的唯一原因是如果您需要与 2.3.0 版本之前的解析器兼容。这些旧的解析器在不期望打包数据时会忽略它。因此,不可能在不破坏有线兼容性的情况下将现有字段更改为打包格式。在 2.3.0 及更高版本中,此更改是安全的,因为可打包字段的解析器将始终接受两种格式,但如果您必须处理使用旧 protobuf 版本的旧程序,请务必小心。repeated int32 samples = 4 [packed = true];
deprecated
(字段选项): 如果设置为 `true`,表示该字段已弃用,新代码不应使用。在大多数语言中,这没有实际效果。在 Java 中,它会变成一个 `@Deprecated` 注解。对于 C++,clang-tidy 将在使用已弃用字段时生成警告。将来,其他特定语言的代码生成器可能会在该字段的访问器上生成弃用注解,这反过来又会在编译试图使用该字段的代码时发出警告。如果该字段无人使用,并且您想阻止新用户使用它,请考虑用保留语句替换该字段声明。optional int32 old_field = 6 [deprecated = true];
枚举值选项
支持枚举值选项。你可以使用 deprecated
选项来指示某个值不应再使用。你还可以使用扩展创建自定义选项。
下面的例子展示了添加这些选项的语法:
import "google/protobuf/descriptor.proto";
extend google.protobuf.EnumValueOptions {
optional string string_name = 123456789;
}
enum Data {
DATA_UNSPECIFIED = 0;
DATA_SEARCH = 1 [deprecated = true];
DATA_DISPLAY = 2 [
(string_name) = "display_value"
];
}
读取 string_name
选项的 C++ 代码可能看起来像这样:
const absl::string_view foo = proto2::GetEnumDescriptor<Data>()
->FindValueByName("DATA_DISPLAY")->options().GetExtension(string_name);
请参阅自定义选项,了解如何将自定义选项应用于枚举值和字段。
自定义选项
Protocol Buffers 还允许您定义和使用自己的选项。请注意,这是一个大多数人不需要的**高级功能**。由于选项是由 `google/protobuf/descriptor.proto` 中定义的消息(如 `FileOptions` 或 `FieldOptions`)定义的,因此定义自己的选项仅仅是扩展这些消息的问题。例如:
import "google/protobuf/descriptor.proto";
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
message MyMessage {
option (my_option) = "Hello world!";
}
这里我们通过扩展 `MessageOptions` 定义了一个新的消息级选项。当我们使用该选项时,选项名称必须用括号括起来,以表明它是一个扩展。我们现在可以在 C++ 中像这样读取 `my_option` 的值:
string value = MyMessage::descriptor()->options().GetExtension(my_option);
这里,`MyMessage::descriptor()->options()` 返回 `MyMessage` 的 `MessageOptions` 协议消息。从中读取自定义选项就像读取任何其他扩展一样。
同样,在 Java 中我们会这样写:
String value = MyProtoFile.MyMessage.getDescriptor().getOptions()
.getExtension(MyProtoFile.myOption);
在 Python 中会是这样:
value = my_proto_file_pb2.MyMessage.DESCRIPTOR.GetOptions()
.Extensions[my_proto_file_pb2.my_option]
可以为 Protocol Buffers 语言中的每种构造定义自定义选项。这里是一个使用每种选项的例子:
import "google/protobuf/descriptor.proto";
extend google.protobuf.FileOptions {
optional string my_file_option = 50000;
}
extend google.protobuf.MessageOptions {
optional int32 my_message_option = 50001;
}
extend google.protobuf.FieldOptions {
optional float my_field_option = 50002;
}
extend google.protobuf.OneofOptions {
optional int64 my_oneof_option = 50003;
}
extend google.protobuf.EnumOptions {
optional bool my_enum_option = 50004;
}
extend google.protobuf.EnumValueOptions {
optional uint32 my_enum_value_option = 50005;
}
extend google.protobuf.ServiceOptions {
optional MyEnum my_service_option = 50006;
}
extend google.protobuf.MethodOptions {
optional MyMessage my_method_option = 50007;
}
option (my_file_option) = "Hello world!";
message MyMessage {
option (my_message_option) = 1234;
optional int32 foo = 1 [(my_field_option) = 4.5];
optional string bar = 2;
oneof qux {
option (my_oneof_option) = 42;
string quux = 3;
}
}
enum MyEnum {
option (my_enum_option) = true;
FOO = 1 [(my_enum_value_option) = 321];
BAR = 2;
}
message RequestType {}
message ResponseType {}
service MyService {
option (my_service_option) = FOO;
rpc MyMethod(RequestType) returns(ResponseType) {
// Note: my_method_option has type MyMessage. We can set each field
// within it using a separate "option" line.
option (my_method_option).foo = 567;
option (my_method_option).bar = "Some string";
}
}
请注意,如果您想在定义它的包之外的包中使用自定义选项,您必须在选项名称前加上包名,就像您对类型名称所做的那样。例如:
// foo.proto
import "google/protobuf/descriptor.proto";
package foo;
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
// bar.proto
import "foo.proto";
package bar;
message MyMessage {
option (foo.my_option) = "Hello world!";
}
最后一件事:由于自定义选项是扩展,它们必须像任何其他字段或扩展一样被分配字段编号。在前面的例子中,我们使用了 50000-99999 范围内的字段编号。这个范围是为个别组织内部使用而保留的,所以您可以自由地在内部应用中使用这个范围内的数字。但是,如果您打算在公共应用中使用自定义选项,那么确保您的字段编号是全局唯一的是很重要的。要获得全局唯一的字段编号,请发送一个请求,将条目添加到 protobuf 全局扩展注册表。通常您只需要一个扩展编号。您可以通过将多个选项放在一个子消息中来用一个扩展编号声明多个选项:
message FooOptions {
optional int32 opt1 = 1;
optional string opt2 = 2;
}
extend google.protobuf.FieldOptions {
optional FooOptions foo_options = 1234;
}
// usage:
message Bar {
optional int32 a = 1 [(foo_options).opt1 = 123, (foo_options).opt2 = "baz"];
// alternative aggregate syntax (uses TextFormat):
optional int32 b = 2 [(foo_options) = { opt1: 123 opt2: "baz" }];
}
另外,请注意每种选项类型(文件级、消息级、字段级等)都有自己的编号空间,所以,例如,您可以声明具有相同编号的 FieldOptions 和 MessageOptions 的扩展。
选项保留
选项有一个*保留期 (retention)* 的概念,它控制一个选项是否在生成的代码中被保留。选项默认具有*运行时保留期*,意味着它们在生成的代码中被保留,因此在运行时在生成的描述符池中是可见的。但是,您可以设置 `retention = RETENTION_SOURCE` 来指定一个选项(或选项中的字段)在运行时不得被保留。这被称为*源码保留期*。
选项保留是一个高级功能,大多数用户不需要担心,但如果您想使用某些选项而不想为在二进制文件中保留它们付出代码大小的代价,它可能很有用。具有源保留的选项对于 `protoc` 和 `protoc` 插件仍然可见,因此代码生成器可以使用它们来自定义其行为。
保留可以直接在选项上设置,如下所示:
extend google.protobuf.FileOptions {
optional int32 source_retention_option = 1234
[retention = RETENTION_SOURCE];
}
它也可以设置在一个普通字段上,在这种情况下,它仅在该字段出现在选项内部时生效:
message OptionsMessage {
optional int32 source_retention_field = 1 [retention = RETENTION_SOURCE];
}
您可以设置 `retention = RETENTION_RUNTIME`,但这没有效果,因为这是默认行为。当一个消息字段被标记为 `RETENTION_SOURCE` 时,它的全部内容都会被丢弃;它内部的字段不能通过尝试设置 `RETENTION_RUNTIME` 来覆盖这一点。
注意
截至 Protocol Buffers 22.0,选项保留的支持仍在进行中,目前只支持 C++ 和 Java。Go 从 1.29.0 版本开始支持。Python 的支持已完成,但尚未发布。选项目标
字段有一个 `targets` 选项,它控制该字段在用作选项时可以应用于的实体类型。例如,如果一个字段有 `targets = TARGET_TYPE_MESSAGE`,那么该字段就不能在枚举(或任何其他非消息实体)的自定义选项中设置。Protoc 会强制执行这一点,并且如果违反目标约束,会引发错误。
乍一看,这个功能似乎没有必要,因为每个自定义选项都是特定实体选项消息的扩展,这已经将选项限制在该一个实体上。然而,在您有一个应用于多种实体类型的共享选项消息,并且您想控制该消息中各个字段的用法时,选项目标就很有用了。例如:
message MyOptions {
optional string file_only_option = 1 [targets = TARGET_TYPE_FILE];
optional int32 message_and_enum_option = 2 [targets = TARGET_TYPE_MESSAGE,
targets = TARGET_TYPE_ENUM];
}
extend google.protobuf.FileOptions {
optional MyOptions file_options = 50000;
}
extend google.protobuf.MessageOptions {
optional MyOptions message_options = 50000;
}
extend google.protobuf.EnumOptions {
optional MyOptions enum_options = 50000;
}
// OK: this field is allowed on file options
option (file_options).file_only_option = "abc";
message MyMessage {
// OK: this field is allowed on both message and enum options
option (message_options).message_and_enum_option = 42;
}
enum MyEnum {
MY_ENUM_UNSPECIFIED = 0;
// Error: file_only_option cannot be set on an enum.
option (enum_options).file_only_option = "xyz";
}
生成你的类
要生成您需要使用的 Java、Kotlin、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` 选项来指定多个导入目录;它们将按顺序被搜索。`-I=_IMPORT_PATH_` 可以用作 `--proto_path` 的简写形式。
注意: 相对于其 `proto_path` 的文件路径在给定的二进制文件中必须是全局唯一的。例如,如果您有 `proto/lib1/data.proto` 和 `proto/lib2/data.proto`,这两个文件不能与 `-I=proto/lib1 -I=proto/lib2` 一起使用,因为 `import "data.proto"` 的含义将不明确。相反,应该使用 `-Iproto/`,全局名称将是 `lib1/data.proto` 和 `lib2/data.proto`。
如果您正在发布一个库,并且其他用户可能会直接使用您的消息,您应该在他们预期使用的路径中包含一个唯一的库名,以避免文件名冲突。如果您在一个项目中有多个目录,最好的做法是倾向于将一个 `-I` 设置为项目的顶层目录。
你可以提供一个或多个输出指令:
--cpp_out
在DST_DIR
中生成 C++ 代码。更多信息请参阅 C++ 生成代码参考。--java_out
在DST_DIR
中生成 Java 代码。更多信息请参阅 Java 生成代码参考。--kotlin_out
在DST_DIR
中生成额外的 Kotlin 代码。更多信息请参阅Kotlin 生成代码参考。--python_out
在DST_DIR
中生成 Python 代码。更多信息请参阅 Python 生成代码参考。--go_out
在DST_DIR
中生成 Go 代码。更多信息请参阅Go 生成代码参考。--ruby_out
在DST_DIR
中生成 Ruby 代码。更多信息请参阅 Ruby 生成代码参考。--objc_out
在DST_DIR
中生成 Objective-C 代码。更多信息请参阅 Objective-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
中,以便编译器可以确定其规范名称。
文件位置
最好不要将 .proto
文件与其他语言的源文件放在同一个目录中。考虑在你的项目根包下为 .proto
文件创建一个子包 proto
。
位置应与语言无关
在处理 Java 代码时,将相关的 `.proto` 文件放在与 Java 源代码相同的目录中很方便。但是,如果任何非 Java 代码使用相同的 proto,路径前缀将不再有意义。因此,通常应将 proto 放在与语言无关的相关目录中,例如 `//myteam/mypackage`。
这条规则的例外是当很明显 protos 只会在 Java 上下文中使用时,例如用于测试。
支持的平台
有关信息
- 支持的操作系统、编译器、构建系统和 C++ 版本,请参阅 基础 C++ 支持政策。
- 支持的 PHP 版本,请参阅支持的 PHP 版本。