语言指南 (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 传输格式非常精简,没有提供一种方法来检测使用一个定义编码然后使用另一个定义解码的字段。
使用一个定义对字段进行编码,然后使用不同的定义对同一字段进行解码,可能会导致:
- 开发人员花费时间进行调试
- 解析/合并错误(最佳情况)
- PII/SPII 泄露
- 数据损坏
字段编号重复使用的常见原因
重新编号字段(有时为了使字段的编号顺序更具美感而进行)。重新编号实际上会删除并重新添加所有涉及重新编号的字段,导致不兼容的传输格式更改。
删除字段后未保留该编号,以防止将来重复使用。
字段编号限制为 29 位而不是 32 位,因为有三位用于指定字段的传输格式。有关更多信息,请参阅编码主题。
指定字段基数
消息字段可以是以下类型之一:
单数:
在 proto2 中,有两种类型的单数字段:
optional
:(推荐)一个optional
字段有两种可能的状态:- 字段已设置,包含一个通过显式设置或从传输中解析而来的值。它将被序列化到传输中。
- 字段未设置,将返回默认值。它不会被序列化到传输中。
您可以检查该值是否已显式设置。
required
:不要使用。 必填字段问题太多,已从 proto3 和 editions 中移除。必填字段的语义应在应用层实现。当它确实使用时,格式良好的消息必须且仅有一个此字段。
repeated
:这种字段类型在格式良好的消息中可以重复零次或多次。重复值的顺序将得到保留。map
:这是一种键/值对字段类型。有关此字段类型的更多信息,请参阅Map。
对新的重复字段使用紧凑编码(Packed Encoding)
出于历史原因,标量数字类型(例如 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
安全地更改为 optional
几乎是不可能的。如果存在任何过时的读取器,它将认为不包含此字段的消息是不完整的,并可能拒绝或丢弃它们。必填字段的第二个问题出现在有人向枚举添加值时。在这种情况下,无法识别的枚举值将被视为缺失,这也会导致必填值检查失败。
格式良好的消息
“格式良好”(well-formed)一词用于 protobuf 消息时,指序列化/反序列化的字节。protoc 解析器会验证给定的 proto 定义文件是否可解析。
单数字段在传输格式字节中可以出现多次。解析器会接受输入,但只有该字段的最后一个实例可以通过生成的绑定访问。有关此主题的更多信息,请参阅后出现的 wins。
添加更多消息类型
可以在单个 .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 到 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] Kotlin 使用 Java 中的对应类型,即使对于无符号类型也是如此,以确保在混合 Java/Kotlin 代码库中的兼容性。
[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 编码中找到更多关于序列化消息时这些类型的编码方式的信息。
默认字段值
解析消息时,如果编码消息字节不包含特定字段,则在解析对象中访问该字段会返回该字段的默认值。默认值是类型特定的:
- 对于字符串,默认值是空字符串。
- 对于字节,默认值是空字节。
- 对于 bool,默认值是 false。
- 对于数字类型,默认值为零。
- 对于消息字段,该字段未设置。其确切值取决于语言。有关详细信息,请参阅您所选语言的生成代码指南。
- 对于枚举,默认值是第一个定义的枚举值,该值应为 0(建议这样做是为了与开放枚举兼容)。请参阅枚举默认值。
重复字段的默认值为空(通常是相应语言中的空列表)。
Map 字段的默认值为空(通常是相应语言中的空 Map)。
覆盖默认标量值
在 proto2 中,您可以为单数非消息字段指定显式的默认值。例如,假设您想为 SearchRequest.results_per_page
字段提供默认值 10:
optional int32 results_per_page = 3 [default = 10];
如果发送方未指定 results_per_page
,接收方将观察到以下状态:
results_per_page
字段不存在。也就是说,has_results_per_page()
(haszer 方法) 将返回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
值在传输中使用变长编码,负值效率低下,因此不建议使用。您可以在消息定义中定义 enum
(如前面的示例所示),也可以在消息定义之外定义——这些 enum
可以在 .proto
文件中的任何消息定义中重复使用。您还可以使用在一个消息中声明的 enum
类型作为不同消息中字段的类型,使用语法 _MessageType_._EnumType_
。
当您对使用 enum
的 .proto
文件运行 protocol buffer 编译器时,生成的代码将包含 Java、Kotlin 或 C++ 中相应的 enum
,或者 Python 中用于在运行时生成的类中创建具有整数值的符号常量集的特殊 EnumDescriptor
类。
重要
生成代码可能受到特定语言对枚举器数量的限制(对于某种语言来说是几千)。请查阅您计划使用的语言的限制。重要
有关枚举应如何工作以及它们目前在不同语言中如何工作的对比信息,请参阅枚举行为。删除枚举值对于持久化的 protos 来说是一个破坏性更改。与其删除一个值,不如使用 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
概念将所有导入转发到新位置。
请注意,公共导入功能在 Java、Kotlin、TypeScript、JavaScript、GCL 以及使用 protobuf 静态反射的 C++ 目标中不可用。
任何导入包含 import public
语句的 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
protocol 编译器在命令行中使用 -I
/--proto_path
标志指定的一组目录中搜索导入的文件。如果未给出任何标志,则在调用编译器的目录中查找。通常,您应该将 --proto_path
标志设置为项目根目录,并对所有导入使用完全限定名称。
使用 proto3 消息类型
可以导入 proto3 和 2023 edition 的消息类型并在您的 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)是另一种在消息定义中嵌套信息的方式。例如,另一种指定包含多个 Result
的 SearchResponse
的方式如下:
message SearchResponse {
repeated group Result = 1 {
optional string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
}
组(group)只是将嵌套的消息类型和字段合并到一个声明中。在您的代码中,您可以像处理一个名为 result
(后者的名称会转换为小写,以免与前者冲突)的 Result
类型字段一样处理此消息。因此,此示例与之前的 SearchResponse
完全等价,只是该消息的传输格式不同。
更新消息类型
如果现有的消息类型不再满足您的所有需求——例如,您希望消息格式有一个额外的字段——但您仍然想使用用旧格式创建的代码,请不要担心!当您使用二进制传输格式时,更新消息类型而不会破坏任何现有代码是非常简单的。
注意
如果您使用 JSON 或 proto text format 来存储您的 protocol buffer 消息,您可以在 proto 定义中进行的更改是不同的。查看Proto 最佳实践以及以下规则:
- 不要更改任何现有字段的字段编号。“更改”字段编号等同于删除该字段并添加一个具有相同类型的新字段。如果您想重新编号字段,请参阅删除字段的说明。
- 您添加的任何新字段都应为
optional
或repeated
。这意味着使用“旧”消息格式的代码序列化的任何消息仍然可以由您的新生成代码解析,因为它们不会缺少任何required
元素。您应该记住这些元素的默认值,以便新代码可以正确地与旧代码生成的消息交互。同样,您的新代码创建的消息可以由您的旧代码解析:旧二进制文件在解析时会简单地忽略新字段。然而,未知字段不会被丢弃,如果稍后序列化该消息,未知字段会随之序列化——因此,如果该消息传递给新代码,新字段仍然可用。有关详细信息,请参阅未知字段部分。 - 非必填字段可以移除,只要在更新后的消息类型中不再使用该字段编号即可。您可能希望改名该字段,例如添加前缀“OBSOLETE_”,或者将该字段编号保留,以免您的
.proto
的未来用户意外重复使用该编号。 - 非必填字段可以转换为扩展,反之亦然,只要类型和编号保持不变即可。
int32
、uint32
、int64
、uint64
和bool
都是兼容的——这意味着您可以在这些类型之间更改字段而不会破坏前向或后向兼容性。如果从传输中解析的数字不适合对应的类型,您将获得与在 C++ 中将该数字强制转换为该类型相同的效果(例如,如果将一个 64 位数字读取为 int32,它将被截断为 32 位)。sint32
和sint64
相互兼容,但与不其他整数类型兼容。如果写入的值在 INT_MIN 和 INT_MAX 之间(包含边界),则使用任一类型解析都会得到相同的值。如果写入的 sint64 值超出该范围并解析为 sint32,则 varint 会被截断为 32 位,然后进行 zigzag 解码(这将导致观察到不同的值)。string
和bytes
兼容,只要字节是有效的 UTF-8。- 嵌入消息与
bytes
兼容,如果字节包含该消息的编码实例。 fixed32
与sfixed32
兼容,fixed64
与sfixed64
兼容。- 对于
string
、bytes
和消息字段,singular 与repeated
兼容。给定重复字段的序列化数据作为输入,期望该字段为 singular 的客户端如果该字段是原始类型字段,则将取最后一个输入值;如果是消息类型字段,则会合并所有输入元素。请注意,这对于数字类型(包括 bool 和枚举)通常不安全。数字类型的重复字段可能以packed格式序列化,当期望 singular 字段时,将无法正确解析。 - 更改默认值通常是可以的,只要记住默认值永远不会通过传输发送即可。因此,如果程序接收到一条消息,其中某个特定字段未设置,程序将看到在该程序版本的协议中定义的默认值。它将不会看到发送方代码中定义的默认值。
enum
在传输格式上与int32
、uint32
、int64
和uint64
兼容(注意,如果值不适合,将被截断)。但是,请注意,客户端代码在反序列化消息时可能会区别对待它们。值得注意的是,当消息被反序列化时,无法识别的enum
值将被丢弃,这将导致字段的has..
访问器返回 false,并且其 getter 返回enum
定义中列出的第一个值,或者如果指定了默认值,则返回默认值。在重复的 enum 字段的情况下,任何无法识别的值都会从列表中删除。但是,整数字段将始终保留其值。因此,在将整数升级为enum
时,对于在传输中接收越界的 enum 值,您需要非常小心。- 在当前的 Java 和 C++ 实现中,当无法识别的
enum
值被剥离时,它们会与其他未知字段一起存储。请注意,如果此数据被序列化然后由识别这些值的客户端重新解析,这可能会导致奇怪的行为。在 optional 字段的情况下,即使在原始消息被反序列化后写入了新值,识别它的客户端仍将读取旧值。在重复字段的情况下,旧值将出现在任何已识别和新添加的值之后,这意味着顺序将不会保留。 - 将单个
optional
字段或扩展变更为新的oneof
的成员是二进制兼容的,但对于某些语言(特别是 Go),生成的代码 API 将以不兼容的方式改变。因此,Google 在其公共 API 中不进行此类更改,如 AIP-180 中所述。同样需要注意的是源代码兼容性,如果您确定没有代码一次设置多个字段,将多个字段移动到新的oneof
中可能是安全的。将字段移动到现有的oneof
中不安全。同样,将单个字段的oneof
更改为optional
字段或扩展是安全的。 - 将字段在
map<K, V>
与对应的repeated
消息字段之间更改是二进制兼容的(有关消息布局和其他限制,请参见下文Map)。然而,更改的安全性取决于应用程序:在反序列化和重新序列化消息时,使用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
},
// Ensures all field numbers in this extension range are declarations.
verification = DECLARATION
];
}
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 生态系统之外的机制来在选定的扩展范围内分配扩展字段编号。一个例子可能是使用单存储库(monorepo)的提交编号。从 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,该 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
如果您的消息包含许多 optional 字段,并且同一时间最多只设置一个字段,您可以通过使用 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++,请确保您的代码不会导致内存崩溃。以下示例代码将崩溃,因为调用
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++ 中,如果您对两个包含 oneof 的消息执行
Swap()
操作,每个消息将最终拥有对方的 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
字段存在类似的问题。
Map
如果您想在数据定义中创建关联 Map,protocol buffers 提供了一个方便的快捷语法:
map<key_type, value_type> map_field = N;
...其中 key_type
可以是任何整型或字符串类型(也就是说,除了浮点类型和 bytes
之外的任何标量类型)。注意,枚举和 proto 消息都不能用作 key_type
。value_type
可以是除另一个 Map 之外的任何类型。
因此,例如,如果您想创建一个项目 Map,其中每个 Project
消息都与一个字符串键关联,您可以这样定义它:
map<string, Project> projects = 3;
Map 特性
- Map 不支持扩展。
- Map 不能是
repeated
、optional
或required
。 - Map 值的传输格式顺序和 Map 迭代顺序是未定义的,因此您不能依赖您的 Map 项按特定顺序排列。
- 为
.proto
生成文本格式时,Map 按键排序。数字键按数值排序。 - 从传输解析或合并时,如果存在重复的 Map 键,则使用最后一个遇到的键。从文本格式解析 Map 时,如果存在重复键,解析可能会失败。
- 如果为 map 字段提供了键但未提供值,则字段序列化时的行为取决于语言。在 C++、Java、Kotlin 和 Python 中,该类型的默认值会被序列化,而在其他语言中则不会序列化任何内容。
- 在与 Map
foo
相同的范围内不能存在符号FooEntry
,因为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;
任何支持 Map 的 protocol buffers 实现必须能够生成和接受可以被先前定义接受的数据。
包
您可以向 .proto
文件添加可选的 package
说明符,以防止 protocol 消息类型之间发生名称冲突。
package foo.bar;
message Open { ... }
然后,您可以在定义消息类型的字段时使用 package 说明符:
message Foo {
...
optional foo.bar.Open open = 1;
...
}
package 说明符对生成代码的影响取决于您选择的语言:
- 在 C++ 中,生成的类被封装在 C++ 命名空间中。例如,
Open
将位于foo::bar
命名空间中。 - 在 Java 和 Kotlin 中,package 用作 Java 包,除非您在
.proto
文件中显式提供了option java_package
。 - 在 Python 中,
package
指令被忽略,因为 Python 模块是根据它们在文件系统中的位置组织的。 - 在 Go 中,
package
指令被忽略,生成的.pb.go
文件位于与相应go_proto_library
Bazel 规则同名的包中。对于开源项目,您必须提供go_package
选项或设置 Bazel-M
标志。 - 在 Ruby 中,生成的类被封装在嵌套的 Ruby 命名空间中,并转换为所需的 Ruby 大写风格(首字母大写;如果第一个字符不是字母,则在其前面加上
PB_
)。例如,Open
将位于Foo::Bar
命名空间中。 - 在 PHP 中,package 在转换为 PascalCase 后用作命名空间,除非您在
.proto
文件中显式提供option php_namespace
。例如,Open
将位于Foo\Bar
命名空间中。 - 在 C# 中,package 在转换为 PascalCase 后用作命名空间,除非您在
.proto
文件中显式提供option csharp_namespace
。例如,Open
将位于Foo.Bar
命名空间中。
请注意,即使 package
指令不直接影响生成的代码(例如在 Python 中),仍然强烈建议为 .proto
文件指定 package,否则可能导致描述符中的命名冲突,并使 proto 在其他语言中不可移植。
包和名称解析
protocol buffer 语言中的类型名称解析类似于 C++:首先搜索最内层作用域,然后是次内层,依此类推,每个包都被视为其父包的“内层”。前导的“.”(例如,.foo.bar.Baz
)意味着从最外层作用域开始。
protocol buffer 编译器通过解析导入的 .proto
文件来解析所有类型名称。每种语言的代码生成器知道如何在该语言中引用每种类型,即使其作用域规则不同。
定义服务
如果您想将您的消息类型与 RPC(远程过程调用)系统一起使用,您可以在 .proto
文件中定义一个 RPC 服务接口,protocol buffer 编译器将为您所选的语言生成服务接口代码和存根。因此,例如,如果您想定义一个带有方法(该方法接受您的 SearchRequest
并返回一个 SearchResponse
)的 RPC 服务,您可以在 .proto
文件中定义如下:
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
默认情况下,protocol 编译器将生成一个名为 SearchService
的抽象接口和一个相应的“stub”实现。stub 将所有调用转发给 RpcChannel
,而 RpcChannel
是一个抽象接口,您必须根据自己的 RPC 系统自行定义。例如,您可以实现一个 RpcChannel
,它序列化消息并通过 HTTP 将其发送到服务器。换句话说,生成的 stub 提供了一个类型安全的接口,用于进行基于 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 或 2023 edition 定义 gRPC 服务。您可以在Proto3 语言指南中找到有关 proto3 语法的更多信息,并在2023 Edition 语言指南中找到有关 2023 edition 的更多信息。
除了 gRPC 之外,还有许多正在进行中的第三方项目正在开发 Protocol Buffers 的 RPC 实现。有关我们已知项目的链接列表,请参阅第三方附加组件 wiki 页面。
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
: 协议缓冲区编译器将生成最小的类,并依赖共享的、基于反射的代码来实现序列化、解析和各种其他操作。因此,生成的代码将比使用SPEED
模式时小得多,但操作速度会较慢。类仍将实现与SPEED
模式下完全相同的公共 API。此模式最适用于包含大量.proto
文件且并非所有文件都需要极高速度的应用。LITE_RUNTIME
: 协议缓冲区编译器将生成仅依赖“lite”运行时库(libprotobuf-lite
而非libprotobuf
)的类。lite 运行时比完整库小得多(大约小一个数量级),但省略了描述符和反射等某些功能。这对于在移动电话等受限平台上运行的应用特别有用。编译器仍将像在SPEED
模式下一样,生成所有方法的快速实现。生成的类将只实现每种语言中的MessageLite
接口,该接口仅提供完整Message
接口方法的一个子集。
option optimize_for = CODE_SIZE;
cc_generic_services
,java_generic_services
,py_generic_services
(文件选项): 通用服务已废弃。指定协议缓冲区编译器是否应分别在 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 版本之前的解析器兼容。这些旧解析器在未预期到 packed 数据时会忽略它。因此,在不破坏线路兼容性的情况下,无法将现有字段更改为 packed 格式。在 2.3.0 及更高版本中,此更改是安全的,因为可 packing 字段的解析器总是接受两种格式,但如果您必须处理使用旧 protobuf 版本的老程序,请务必小心。repeated int32 samples = 4 [packed = true];
deprecated
(字段选项): 如果设置为true
,表示该字段已废弃,不应被新代码使用。在大多数语言中,这没有实际效果。在 Java 中,这会成为一个@Deprecated
注解。对于 C++,只要使用废弃字段,clang-tidy 就会生成警告。将来,其他特定于语言的代码生成器可能会在字段的访问器上生成废弃注解,这反过来会导致编译尝试使用该字段的代码时发出警告。如果该字段不再被任何人使用,并且您想阻止新用户使用它,请考虑用 reserved 语句替换字段声明。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);
请参阅自定义选项了解如何将自定义选项应用于枚举值和字段。
自定义选项
协议缓冲区还允许您定义和使用自己的选项。请注意,这是一项高级功能,大多数人不需要。由于选项是由 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]
可以为协议缓冲区语言中的每种构造定义自定义选项。以下是使用所有类型选项的示例:
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)概念,它控制选项是否保留在生成的代码中。选项默认具有运行时保留(runtime retention),这意味着它们保留在生成的代码中,因此在运行时在生成的描述符池中可见。但是,您可以设置 retention = RETENTION_SOURCE
来指定某个选项(或选项中的字段)不得在运行时保留。这称为源保留(source retention)。
选项保留是一项高级功能,大多数用户不需要担心,但如果您想使用某些选项而不支付在二进制文件中保留它们的代码大小成本,它会很有用。具有源保留的选项对 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";
}
生成类
要生成使用 .proto
文件中定义的消息类型所需的 Java、Kotlin、Python、C++、Go、Ruby、Objective-C 或 C# 代码,您需要对 .proto
文件运行协议缓冲区编译器 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 规范要求的 manifest 文件。请注意,如果输出存档已存在,它将被覆盖。您必须提供一个或多个
.proto
文件作为输入。可以同时指定多个.proto
文件。尽管文件名相对于当前目录命名,但每个文件必须驻留在某个IMPORT_PATH
中,以便编译器能够确定其规范名称。
文件位置
尽量不要将 .proto
文件放在与其他语言源代码相同的目录中。考虑在项目的根包下创建一个名为 proto
的子包来存放 .proto
文件。
位置应与语言无关
在使用 Java 代码时,将相关的 .proto
文件放在与 Java 源文件相同的目录中很方便。但是,如果任何非 Java 代码使用相同的 proto,则路径前缀将不再有意义。因此,通常情况下,将 proto 放在相关的、语言无关的目录中,例如 //myteam/mypackage
。
此规则的例外是,当 proto 显然只用于 Java 上下文时,例如用于测试。
支持的平台
有关以下内容的信息:
- 支持的操作系统、编译器、构建系统和 C++ 版本,请参阅Foundational C++ Support Policy。
- 支持的 PHP 版本,请参阅Supported PHP versions。