语言指南 (proto 2)
本指南介绍如何使用协议缓冲区语言来构建您的协议缓冲区数据,包括 .proto
文件语法以及如何从 .proto
文件生成数据访问类。它涵盖了协议缓冲区语言的 proto2 版本。
有关 版本 语法的更多信息,请参阅Protobuf 版本语言指南。
有关 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
,协议缓冲区编译器将假定您正在使用 proto2。
SearchRequest
消息定义指定了三个字段(名称/值对),每个字段对应于您要在此消息类型中包含的一条数据。每个字段都有一个名称和一个类型。
指定字段类型
在前面的示例中,所有字段都是标量类型:两个整数 (page_number
和 results_per_page
) 和一个字符串 (query
)。您还可以为字段指定枚举和复合类型(如其他消息类型)。
分配字段编号
您必须为消息定义中的每个字段指定一个介于 1
和 536,870,911
之间的数字,并具有以下限制
- 对于该消息的所有字段,给定的数字必须是唯一的。
- 字段编号
19,000
到19,999
保留供 Protocol Buffers 实现使用。如果您在消息中使用这些保留字段编号之一,协议缓冲区编译器将报错。 - 您不能使用任何先前保留的字段编号或已分配给扩展的任何字段编号。
一旦您的消息类型投入使用,就不能更改此编号,因为它标识了消息线路格式中的字段。“更改”字段编号等同于删除该字段并创建一个具有相同类型但编号不同的新字段。有关如何正确执行此操作,请参阅删除字段。
字段编号绝不应该被重用。永远不要从保留列表中取出字段编号以用于新的字段定义。请参阅重用字段编号的后果。
您应该将字段编号 1 到 15 用于最常设置的字段。较低的字段编号值在线路格式中占用的空间更少。例如,范围 1 到 15 中的字段编号需要一个字节来编码。范围 16 到 2047 中的字段编号需要两个字节。您可以在协议缓冲区编码中找到有关此内容的更多信息。
重用字段编号的后果
重用字段编号会使解码线路格式消息变得模棱两可。
protobuf 线路格式很精简,并且没有提供一种方法来检测使用一个定义编码并使用另一个定义解码的字段。
使用一个定义编码字段,然后使用不同的定义解码同一字段可能会导致
- 开发人员浪费时间进行调试
- 解析/合并错误(最佳情况)
- PII/SPII 泄露
- 数据损坏
字段编号重用的常见原因
重新编号字段(有时这样做是为了实现字段更美观的编号顺序)。重新编号实际上会删除并重新添加重新编号中涉及的所有字段,从而导致不兼容的线路格式更改。
删除字段并且不保留编号以防止将来重用。
字段编号限制为 29 位而不是 32 位,因为三位用于指定字段的线路格式。有关详细信息,请参阅编码主题。
指定字段基数
消息字段可以是以下之一
单数:
在 proto2 中,有两种类型的单数字段
optional
:(推荐)optional
字段处于两种可能的状态之一- 字段已设置,并且包含显式设置或从线路解析的值。它将被序列化到线路。
- 字段未设置,并将返回默认值。它不会被序列化到线路。
您可以检查以查看值是否已显式设置。
required
:请勿使用。 必需字段问题太多,已从 proto3 和版本中删除。必需字段的语义应在应用程序层实现。当使用时,格式良好的消息必须恰好包含此字段之一。
repeated
:此字段类型可以在格式良好的消息中重复零次或多次。重复值的顺序将被保留。map
:这是一种成对的键/值字段类型。有关此字段类型的更多信息,请参阅Maps。
对新的重复字段使用打包编码
由于历史原因,标量数值类型的 repeated
字段(例如,int32
、int64
、enum
)的编码效率不如它们本可以达到的效率。新代码应使用特殊选项 [packed = true]
以获得更高效的编码。例如
repeated int32 samples = 4 [packed = true];
repeated ProtoEnum results = 5 [packed = true];
您可以在协议缓冲区编码中找到有关 packed
编码的更多信息。
强烈建议弃用 Required
重要提示
Required 是永久性的 如前所述,required
不得用于新字段。必需字段的语义应改为在应用程序层实现。现有的 required
字段应视为消息定义的永久、不可变元素。几乎不可能安全地将字段从 required
更改为 optional
。如果有可能存在过时的读取器,它会将没有此字段的消息视为不完整,并可能拒绝或丢弃它们。必需字段的第二个问题出现在有人向枚举添加值时。在这种情况下,无法识别的枚举值被视为缺失,这也导致必需值检查失败。
格式良好的消息
术语“格式良好”应用于 protobuf 消息时,指的是序列化/反序列化的字节。protoc 解析器验证给定的 proto 定义文件是否可解析。
单数字段在线路格式字节中可能会出现多次。解析器将接受输入,但只有该字段的最后一个实例可以通过生成的绑定访问。有关此主题的更多信息,请参阅后胜出。
添加更多消息类型
可以在单个 .proto
文件中定义多种消息类型。如果您要定义多个相关消息,这非常有用 - 例如,如果您想定义与您的 SearchResponse
消息类型相对应的回复消息格式,您可以将其添加到同一个 .proto
中
message SearchRequest {
optional string query = 1;
optional int32 page_number = 2;
optional int32 results_per_page = 3;
}
message SearchResponse {
...
}
组合消息会导致膨胀 虽然可以在单个 .proto
文件中定义多种消息类型(例如消息、枚举和服务),但当在单个文件中定义大量具有不同依赖关系的消息时,也可能导致依赖关系膨胀。建议每个 .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
,并确保以任何方式观察消息的所有系统都已使用新架构部署。然后您可以考虑删除该字段(但请注意,这仍然是一个容易出错的过程)。
当您不再需要不是 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
上运行协议缓冲区编译器时,编译器会以您选择的语言生成代码,您需要使用该代码来处理您在文件中描述的消息类型,包括获取和设置字段值、将消息序列化到输出流以及从输入流解析消息。
- 对于 C++,编译器会从每个
.proto
生成一个.h
和.cc
文件,其中每个消息类型在文件中都有一个类。 - 对于 Java,编译器会生成一个
.java
文件,其中每个消息类型都有一个类,以及一个用于创建消息类实例的特殊Builder
类。 - 对于 Kotlin,除了 Java 生成的代码外,编译器还会为每个消息类型生成一个
.kt
文件,其中包含改进的 Kotlin API。这包括一个简化创建消息实例的 DSL、一个可为空的字段访问器和一个复制函数。 - Python 有点不同 - Python 编译器会生成一个模块,其中包含
.proto
中每个消息类型的静态描述符,然后该描述符与元类一起使用,以便在运行时创建必要的 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 | 使用可变长度编码。有符号 int 值。与常规 int32 相比,这些值可以更有效地编码负数。 |
sint64 | 使用可变长度编码。有符号 int 值。与常规 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] Integer 用于 64 位计算机,字符串用于 32 位计算机。
您可以在协议缓冲区编码中找到有关在序列化消息时如何编码这些类型的更多信息。
默认字段值
解析消息时,如果编码的消息字节不包含特定字段,则访问已解析对象中的该字段将返回该字段的默认值。默认值是特定于类型的
- 对于字符串,默认值为空字符串。
- 对于字节,默认值为空字节。
- 对于布尔值,默认值为 false。
- 对于数值类型,默认值为零。
- 对于消息字段,该字段未设置。其确切值取决于语言。有关详细信息,请参阅您语言的生成的代码指南。
- 对于枚举,默认值是第一个定义的枚举值,它应为 0(建议与开放枚举兼容)。请参阅枚举默认值。
重复字段的默认值为空(通常是相应语言中的空列表)。
映射字段的默认值为空(通常是相应语言中的空映射)。
覆盖默认标量值
在 proto2 中,您可以为单数非消息字段指定显式默认值。例如,假设您要为 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
字段,其中语料库可以是 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
。否则,当找到别名时,协议缓冲区编译器会生成警告消息。虽然所有别名值对于序列化都是有效的,但反序列化时仅使用第一个值。
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_
。
当您在 .proto
上运行协议缓冲区编译器(该 .proto
使用 enum
)时,生成的代码将具有 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
。如果未来的用户尝试使用这些标识符,协议缓冲区编译器将报错。您可以使用 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
文件放在旧位置,以使用 import public
概念将所有导入转发到新位置,而不是直接移动 .proto
文件并在单个更改中更新所有调用站点。
请注意,公共导入功能在 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
协议编译器在协议编译器命令行中使用 -I
/--proto_path
标志指定的一组目录中搜索导入的文件。如果未给出标志,它将在调用编译器的目录中查找。通常,您应该将 --proto_path
标志设置为项目的根目录,并对所有导入使用完全限定的名称。
使用 proto3 消息类型
可以导入proto3 和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;
}
}
}
组
请注意,组功能已弃用,创建新消息类型时不应使用。请改用嵌套消息类型。
组是在消息定义中嵌套信息的另一种方法。例如,指定包含多个 Result
的 SearchResponse
的另一种方法如下所示
message SearchResponse {
repeated group Result = 1 {
optional string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
}
组只是将嵌套消息类型和字段组合到一个声明中。在您的代码中,您可以将此消息视为就像它有一个名为 result
的 Result
类型字段一样(后一个名称转换为小写,以便它不与前者冲突)。因此,此示例与之前的 SearchResponse
完全等效,只是该消息具有不同的线路格式。
更新消息类型
如果现有消息类型不再满足您的所有需求 - 例如,您希望消息格式具有额外的字段 - 但您仍然希望使用使用旧格式创建的代码,请不要担心!当您使用二进制线路格式时,更新消息类型而不会破坏任何现有代码非常简单。
注意
如果您使用 JSON 或proto 文本格式存储协议缓冲区消息,则可以在 proto 定义中所做的更改有所不同。查看Proto 最佳实践和以下规则
- 不要更改任何现有字段的字段编号。“更改”字段编号等同于删除字段并添加具有相同类型的新字段。如果您要重新编号字段,请参阅删除字段的说明。
- 您添加的任何新字段都应为
optional
或repeated
。这意味着使用“旧”消息格式的代码序列化的任何消息仍然可以由您新生成的代码解析,因为它们不会缺少任何required
元素。您应该记住这些元素的默认值,以便新代码可以与旧代码生成的消息正确交互。同样,旧代码可以解析由新代码创建的消息:旧二进制文件在解析时只需忽略新字段。但是,未知字段不会被丢弃,如果消息稍后被序列化,则未知字段也会随之序列化 - 因此,如果消息传递到新代码,则新字段仍然可用。有关详细信息,请参阅未知字段部分。 - 可以删除非必需字段,只要在更新的消息类型中不再使用该字段编号即可。您可能希望改为重命名字段,可能添加前缀“OBSOLETE_”,或使字段编号保留,以便将来您的
.proto
用户不会意外地重用该编号。 - 非必需字段可以转换为扩展,反之亦然,只要类型和编号保持不变即可。
int32
、uint32
、int64
、uint64
和bool
都是兼容的 —— 这意味着您可以将字段从这些类型中的一种更改为另一种,而不会破坏向前或向后兼容性。如果从线路解析的数字不适合相应的类型,您将获得与在 C++ 中将数字强制转换为该类型相同的效果(例如,如果将 64 位数字读取为 int32,则它将被截断为 32 位)。sint32
和sint64
彼此兼容,但与其他整数类型不兼容。string
和bytes
只要字节是有效的 UTF-8 就兼容。- 如果嵌入的消息包含消息的编码实例,则嵌入的消息与
bytes
兼容。 fixed32
与sfixed32
兼容,fixed64
与sfixed64
兼容。- 对于
string
、bytes
和消息字段,单数与repeated
兼容。给定作为输入的 repeated 字段的序列化数据,期望此字段为单数的客户端将获取最后一个输入值(如果它是原始类型字段),或者合并所有输入元素(如果它是消息类型字段)。请注意,这对于数值类型(包括布尔值和枚举)通常不安全。数值类型的 repeated 字段可以以 packed 格式序列化,当期望单数字段时,将无法正确解析该格式。 - 更改默认值通常是可以的,只要您记住默认值永远不会通过线路发送。因此,如果程序收到一条消息,其中未设置特定字段,则程序将看到默认值,就像该程序的协议版本中定义的那样。它将不会看到发送者代码中定义的默认值。
- 就线路格式而言,
enum
与int32
、uint32
、int64
和uint64
兼容(请注意,如果值不适合,则会被截断)。但是,请注意,当消息被反序列化时,客户端代码可能会以不同的方式处理它们。值得注意的是,当消息被反序列化时,无法识别的enum
值会被丢弃,这使得字段的has..
访问器返回 false,并且其 getter 返回enum
定义中列出的第一个值,或者如果指定了默认值,则返回默认值。对于 repeated 枚举字段,任何无法识别的值都会从列表中删除。但是,整数字段将始终保留其值。因此,当将整数升级为enum
时,您需要非常小心,以防在线路上接收到超出范围的枚举值。 - 在当前的 Java 和 C++ 实现中,当无法识别的
enum
值被删除时,它们会与其他未知字段一起存储。请注意,如果此数据被序列化,然后被识别这些值的客户端重新解析,则可能导致奇怪的行为。在可选字段的情况下,即使在原始消息反序列化后写入了新值,识别它的客户端仍将读取旧值。在 repeated 字段的情况下,旧值将出现在任何已识别和新添加的值之后,这意味着顺序将不会被保留。 - 将单个
optional
字段或扩展名更改为新的oneof
的成员是二进制兼容的,但是对于某些语言(特别是 Go),生成的代码的 API 将以不兼容的方式更改。因此,正如 AIP-180 中记录的那样,Google 不会在其公共 API 中进行此类更改。关于源代码兼容性的相同警告,如果您确定没有代码一次设置多个字段,则将多个字段移动到新的oneof
中可能是安全的。将字段移动到现有的oneof
中是不安全的。同样,将单个字段oneof
更改为optional
字段或扩展名是安全的。 - 在
map<K, V>
和相应的repeated
消息字段之间更改字段是二进制兼容的(有关消息布局和其他限制,请参见下面的 映射)。但是,更改的安全性取决于应用程序:当反序列化和重新序列化消息时,使用repeated
字段定义的客户端将产生语义上相同的结果;但是,使用map
字段定义的客户端可能会重新排序条目并删除具有重复键的条目。
未知字段
未知字段是格式良好的协议缓冲区序列化数据,表示解析器无法识别的字段。例如,当旧的二进制文件解析新的二进制文件(带有新字段)发送的数据时,这些新字段将成为旧的二进制文件中的未知字段。
最初,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
将由完全限定名称为 .kittens.kitten_videos
和完全限定类型为 .kittens.Video
的 repeated
扩展字段使用。要了解有关扩展声明的更多信息,请参见 扩展声明。
请注意,容器消息的文件 (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
的范围内声明;它只是一个静态成员。
一种常见的模式是在扩展的字段类型的范围内定义扩展 - 例如,这是一个类型为 puppies.Photo
的 media.UserContent
的扩展,其中扩展被定义为 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 字段类似于可选字段,只是一个 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
关键字。如果需要将 repeated 字段添加到 oneof,则可以使用包含 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()
交换两个带有 oneof 的消息,则每个消息最终都将具有另一个消息的 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 的成员。
标签重用问题
- 将可选字段移入或移出 oneof:在消息序列化和解析后,您可能会丢失某些信息(某些字段将被清除)。但是,您可以安全地将单个字段移动到新的 oneof 中,并且如果已知一次只设置一个字段,则可以移动多个字段。有关更多详细信息,请参见 更新消息类型。
- 删除 oneof 字段并将其重新添加:这可能会在消息序列化和解析后清除您当前设置的 oneof 字段。
- 拆分或合并 oneof:这与移动
optional
字段有类似的问题。
Maps
如果您想创建关联映射作为数据定义的一部分,则协议缓冲区提供了一个方便的快捷方式语法
map<key_type, value_type> map_field = N;
…其中 key_type
可以是任何整数或字符串类型(因此,任何 标量 类型,浮点类型和 bytes
除外)。请注意,枚举和 proto 消息都对 key_type
无效。value_type
可以是除另一个 map 之外的任何类型。
因此,例如,如果您想创建一个项目映射,其中每个 Project
消息都与一个字符串键关联,则可以像这样定义它
map<string, Project> projects = 3;
Maps 特性
- 映射不支持扩展。
- 映射不能是
repeated
、optional
或required
。 - 映射值的线路格式排序和映射迭代排序是未定义的,因此您不能依赖于映射项以特定顺序排列。
- 为
.proto
生成文本格式时,映射按键排序。数字键按数字排序。 - 从线路解析或合并时,如果有重复的映射键,则使用看到的最后一个键。从文本格式解析映射时,如果存在重复的键,则解析可能会失败。
- 如果您为映射字段提供键但没有值,则字段序列化时的行为取决于语言。在 C++、Java、Kotlin 和 Python 中,类型的默认值会被序列化,而在其他语言中,则不会序列化任何内容。
- 符号
FooEntry
不能与映射foo
存在于同一作用域中,因为FooEntry
已被映射的实现使用。
生成的映射 API 当前可用于所有受支持的语言。您可以在相关的 API 参考 中找到有关您选择的语言的映射 API 的更多信息。
向后兼容性
映射语法在线路上等效于以下内容,因此不支持映射的协议缓冲区实现仍然可以处理您的数据
message MapFieldEntry {
optional key_type key = 1;
optional value_type value = 2;
}
repeated MapFieldEntry map_field = N;
任何支持映射的协议缓冲区实现都必须生成和接受可以被早期定义接受的数据。
包
您可以向 .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 中,包在转换为 PascalCase 后用作命名空间,除非您在
.proto
文件中显式提供option php_namespace
。例如,Open
将在命名空间Foo\Bar
中。 - 在 C# 中,包在转换为 PascalCase 后用作命名空间,除非您在
.proto
文件中显式提供option csharp_namespace
。例如,Open
将在命名空间Foo.Bar
中。
请注意,即使 package
指令不直接影响生成的代码(例如在 Python 中),仍然强烈建议为 .proto
文件指定包,否则可能会导致描述符中的命名冲突,并使 proto 不可移植到其他语言。
包和名称解析
协议缓冲区语言中的类型名称解析的工作方式类似于 C++:首先搜索最内层作用域,然后搜索下一个最内层作用域,依此类推,每个包都被视为“内部”到其父包。前导 ‘.’(例如,.foo.bar.Baz
)表示从最外层作用域开始。
协议缓冲区编译器通过解析导入的 .proto
文件来解析所有类型名称。每种语言的代码生成器都知道如何以该语言引用每种类型,即使它具有不同的作用域规则。
定义服务
如果您想将消息类型与 RPC(远程过程调用)系统一起使用,则可以在 .proto
文件中定义 RPC 服务接口,并且协议缓冲区编译器将在您选择的语言中生成服务接口代码和存根。因此,例如,如果您想定义一个 RPC 服务,该服务的方法接受您的 SearchRequest
并返回 SearchResponse
,则可以在您的 .proto
文件中按如下方式定义它
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
默认情况下,协议编译器将生成一个名为 SearchService
的抽象接口和一个相应的“存根”实现。存根将所有调用转发到一个 RpcChannel
,而 RpcChannel
本身是一个抽象接口,您必须根据自己的 RPC 系统定义它。例如,您可能会实现一个 RpcChannel
,该通道序列化消息并通过 HTTP 将其发送到服务器。换句话说,生成的存根为进行基于协议缓冲区的 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 与协议缓冲区配合使用效果特别好,并允许您使用特殊的协议缓冲区编译器插件直接从 .proto
文件生成相关的 RPC 代码。但是,由于使用 proto2 和 proto3 生成的客户端和服务器之间可能存在兼容性问题,我们建议您使用 proto3 或 edition 2023 来定义 gRPC 服务。您可以在 Proto3 语言指南中找到有关 proto3 语法的更多信息,在 Edition 2023 语言指南中找到有关 edition 2023 的更多信息。
除了 gRPC 之外,还有许多正在进行的第三方项目来开发协议缓冲区的 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
: 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 版本之前的解析器兼容。这些旧解析器在未预期的情况下会忽略 packed 数据。因此,如果不破坏线路兼容性,就不可能将现有字段更改为 packed 格式。在 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_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";
}
生成您的类
要生成使用 .proto
文件中定义的消息类型所需的 Java、Kotlin、Python、C++、Go、Ruby、Objective-C 或 C# 代码,您需要在 .proto
文件上运行 protocol buffer 编译器 protoc
。如果您尚未安装编译器,请下载软件包并按照 README 中的说明进行操作。对于 Go,您还需要为编译器安装一个特殊的代码生成器插件;您可以在 GitHub 上的 golang/protobuf 存储库中找到此插件和安装说明。
Protocol Compiler 的调用方式如下
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
的简写形式。您可以提供一个或多个输出指令
--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 代码曾经使用相同的 protos,则路径前缀将不再有意义。因此,通常,将 protos 放在相关的语言无关的目录中,例如 //myteam/mypackage
。
此规则的例外情况是当明确 protos 将仅在 Java 上下文中使用时,例如用于测试。
支持的平台
有关以下信息
- 支持的操作系统、编译器、构建系统和 C++ 版本,请参阅 Foundational C++ Support Policy。
- 支持的 PHP 版本,请参阅 Supported PHP versions。