语言指南 (版本)
本指南介绍了如何使用协议缓冲区语言来构建协议缓冲区数据,包括 .proto
文件语法以及如何从 .proto
文件生成数据访问类。它涵盖了协议缓冲区语言的 **2023 版本**。有关版本与 proto2 和 proto3 在概念上的区别,请参阅Protobuf 版本概览。
有关 **proto2** 语法的信息,请参阅Proto2 语言指南。
有关 **proto3** 语法的信息,请参阅Proto3 语言指南。
这是一份参考指南 – 有关使用本文档中描述的许多功能的逐步示例,请参阅您所选语言的教程。
定义消息类型
首先来看一个非常简单的例子。假设您想定义一种搜索请求消息格式,其中每个搜索请求包含一个查询字符串、您感兴趣的特定结果页以及每页结果的数量。下面是用于定义此消息类型的 .proto
文件。
edition = "2023";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
文件的第一行指定您正在使用 protobuf 语言规范的 2023 版本。
edition
(或 proto2/proto3 的syntax
)必须是文件中第一个非空、非注释的行。- 如果未指定
edition
或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 线格式精简,不提供检测使用一种定义编码而使用另一种定义解码的字段的方法。
使用一种定义编码字段,然后使用不同定义解码同一字段,可能导致
- 开发人员浪费时间进行调试
- 解析/合并错误(最佳情况)
- 个人身份信息/敏感个人身份信息泄露
- 数据损坏
重复使用字段编号的常见原因
重新编号字段(有时是为了使字段编号顺序更具美观性)。重新编号实际上删除了所有涉及重新编号的字段并重新添加它们,导致线格式发生不兼容的更改。
删除字段但未保留其编号以防止将来重复使用。
字段编号限制为 29 位而不是 32 位,因为三位用于指定字段的线格式。有关更多信息,请参阅编码主题。
指定字段基数
消息字段可以是以下之一
Singular:
一个 singular 字段没有显式的基数标签。它有两种可能的状态
- 字段已设置,并包含一个显式设置或从线上解析的值。它将被序列化到线上。
- 字段未设置,将返回默认值。它不会被序列化到线上。
您可以检查该值是否已显式设置。
已迁移到版本的 Proto3 implicit 字段将使用设置为
IMPLICIT
值的field_presence
特性。已迁移到版本的 Proto2
required
字段也将使用field_presence
特性,但设置为LEGACY_REQUIRED
。repeated
:此字段类型在格式良好的消息中可以重复零次或多次。重复值的顺序将被保留。map
:这是一个键/值对字段类型。有关此字段类型的更多信息,请参阅映射。
重复字段默认采用 Packed 编码
在 proto 版本中,标量数值类型的 repeated
字段默认使用 packed
编码。
您可以在协议缓冲区编码中了解更多关于 packed
编码的信息。
格式良好的消息
术语“格式良好”应用于 protobuf 消息时,指的是序列化/反序列化后的字节。protoc 解析器会验证给定的 proto 定义文件是否可解析。
Singular 字段可以在线格式字节中出现多次。解析器会接受输入,但只有该字段的最后一个实例可以通过生成的绑定访问。有关此主题的更多信息,请参阅最后一个优先。
添加更多消息类型
可以在单个 .proto
文件中定义多种消息类型。如果您正在定义多个相关消息,这很有用——例如,如果您想定义与 SearchResponse
消息类型对应的回复消息格式,您可以将其添加到同一个 .proto
文件中。
message SearchRequest {
string query = 1;
int32 page_number = 2;
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 {
string query = 1;
// Which page number do we want?
int32 page_number = 2;
// Number of results to return per page.
int32 results_per_page = 3;
}
删除字段
如果操作不当,删除字段可能会导致严重问题。
当您不再需要某个字段并且所有客户端代码中的引用都已删除时,您可以从消息中删除该字段定义。但是,您必须保留已删除的字段编号。如果您不保留该字段编号,将来开发人员可能会重复使用该编号。
您还应该保留字段名称,以便您的消息的 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
文件运行协议缓冲区编译器时,编译器会生成您所选语言的代码,用于处理您在文件中描述的消息类型,包括获取和设置字段值、将消息序列化到输出流以及从输入流解析消息。
- 对于 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 | int | 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 | int | 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 | str/unicode[5] | string | String (UTF-8) | string | string | String | ProtoString |
bytes | string | ByteString | str (Python 2), bytes (Python 3) | []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] Python 字符串在解码时表示为 unicode,但如果给出的是 ASCII 字符串,则可以是 str(这可能会有变化)。
[6] 在 64 位机器上使用 Integer,在 32 位机器上使用 string。
您可以在协议缓冲区编码中了解更多关于这些类型在序列化您的消息时如何编码的信息。
字段默认值
解析消息时,如果编码的消息字节不包含特定字段,则在解析的对象中访问该字段将返回该字段的默认值。默认值是类型特定的
- 对于字符串,默认值为空字符串。
- 对于字节,默认值为空字节序列。
- 对于 bool 类型,默认值为 false。
- 对于数值类型,默认值为零。
- 对于消息字段,该字段未设置。其确切值取决于语言。有关详情,请参阅生成代码指南。
- 对于枚举,默认值是第一个定义的枚举值,其值必须是 0。请参阅枚举默认值。
重复字段的默认值为空(通常在相应的语言中是空列表)。
映射字段的默认值为空(通常在相应的语言中是空映射)。
覆盖默认标量值
在 protobuf 版本中,您可以为 singular 非消息字段指定显式的默认值。例如,假设您想为 SearchRequest.result_per_page
字段提供默认值 10
int32 result_per_page = 3 [default = 10];
如果发送方未指定 result_per_page
,接收方将观察到以下状态
- result_per_page 字段不存在。也就是说,
has_result_per_page()
(hazzer 方法)将返回false
。 result_per_page
的值(从“getter”返回)是10
。
如果发送方确实发送了 result_per_page
的值,则默认值 10 会被忽略,从“getter”返回发送方的值。
有关在生成代码中默认值如何工作的更多详细信息,请参阅您所选语言的生成代码指南。
对于 field_presence
特性设置为 IMPLICIT
的字段,不能指定显式默认值。
枚举
定义消息类型时,您可能希望其某个字段只能具有预定义值列表中的一个。例如,假设您想为每个 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 {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
Corpus corpus = 4;
}
枚举默认值
SearchRequest.corpus
字段的默认值是 CORPUS_UNSPECIFIED
,因为它是枚举中定义的第一个值。
在 2023 版本中,枚举定义中定义的第一个值必须为零,并且应命名为 ENUM_TYPE_NAME_UNSPECIFIED
或 ENUM_TYPE_NAME_UNKNOWN
。这是因为
- 零值需要作为第一个元素,以便与 proto2 的语义兼容,在 proto2 中,除非显式指定了不同的值,否则第一个枚举值是默认值。
- 必须有一个零值,以便与 proto3 的语义兼容,在 proto3 中,零值用作使用此枚举类型的所有隐式存在字段的默认值。
还建议这个第一个默认值除了表示“未指定该值”之外,没有其他语义含义。
像 SearchRequest.corpus
这样的枚举字段的默认值可以像这样显式覆盖
Corpus corpus = 4 [default = CORPUS_UNIVERSAL];
如果枚举类型是使用 option features.enum_type = CLOSED;
从 proto2 迁移过来的,则对枚举中的第一个值没有限制。不建议更改这些类型枚举的第一个值,因为它会更改使用该枚举类型且没有显式字段默认值的任何字段的默认值。
枚举值别名
通过为不同的枚举常量分配相同的值,您可以定义别名。为此,您需要将 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
值在线上使用变长编码,负值效率低下,因此不推荐。您可以在消息定义中定义 enum
,如前面的示例所示,也可以在消息定义之外定义 – 这些 enum
可以在您的 .proto
文件中的任何消息定义中重复使用。您还可以使用在一个消息中声明的 enum
类型作为不同消息中字段的类型,使用语法 _MessageType_._EnumType_
。
语言特定的枚举实现
当您对使用 enum
的 .proto
文件运行协议缓冲区编译器时,生成的代码将为 Java、Kotlin 或 C++ 包含相应的 enum
,或为 Python 包含一个特殊的 EnumDescriptor
类,该类用于在运行时生成的类中创建一组具有整数值的符号常量。
重要提示
生成的代码可能受限于语言特定的枚举器数量限制(某种语言可能有数千个)。请查阅您计划使用的语言的限制。在反序列化期间,无法识别的枚举值将保留在消息中,尽管在反序列化消息时如何表示这取决于语言。在支持带有超出指定符号范围的值的开放枚举类型的语言中,例如 C++ 和 Go,未知枚举值仅存储为其基础整数表示。在具有封闭枚举类型的语言中,例如 Java,使用枚举中的一个 case 来表示无法识别的值,并且可以通过特殊访问器访问基础整数。无论哪种情况,如果消息被序列化,无法识别的值仍然会与消息一起序列化。
重要提示
有关枚举应如何工作以及它们在不同语言中当前如何工作的对比信息,请参阅枚举行为。有关如何在您的应用程序中使用消息 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 {
string url = 1;
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
语句的 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
标志设置为项目的根目录,并对所有导入使用完全限定名称。
使用 proto2 和 proto3 消息类型
可以导入 proto2 和 proto3 消息类型并在您的 2023 版本消息中使用它们,反之亦然。
嵌套类型
您可以在其他消息类型内部定义和使用消息类型,如下面的示例所示——此处 Result
消息定义在 SearchResponse
消息内部
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果您想在其父消息类型之外重用此消息类型,您可以将其引用为 _Parent_._Type_
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
您可以根据需要嵌套消息,深度不限。在下面的示例中,请注意名为 Inner
的两个嵌套类型完全独立,因为它们定义在不同的消息中
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
}
更新消息类型
如果现有的消息类型不再完全满足您的需求——例如,您希望消息格式有一个额外的字段——但您仍然想使用用旧格式创建的代码,请不要担心!当您使用二进制线格式时,更新消息类型非常简单,不会破坏任何现有代码。
注意
如果您使用 JSON 或proto 文本格式来存储协议缓冲区消息,则在您的 proto 定义中可以进行的更改有所不同。查阅Proto 最佳实践和以下规则
- 不要更改任何现有字段的字段编号。“更改”字段编号等同于删除该字段并添加一个具有相同类型的新字段。如果您想重新编号字段,请参阅删除字段的说明。
- 如果您添加新字段,使用您“旧”消息格式的代码序列化的任何消息仍然可以被您的新生成代码解析。您应该记住这些元素的默认值,以便新代码可以正确地与旧代码生成的消息交互。同样,您的新代码创建的消息可以被您的旧代码解析:旧的二进制文件在解析时会忽略新字段。有关详情,请参阅未知字段部分。
- 字段可以被移除,只要字段编号在您更新的消息类型中不再被使用。您可以考虑重命名该字段,例如添加前缀“OBSOLETE_”,或者将字段编号保留,以便您的
.proto
文件的未来用户不会意外重复使用该编号。 int32
,uint32
,int64
,uint64
和bool
都兼容——这意味着您可以将字段从这些类型之一更改为另一种,而不会破坏前向或后向兼容性。如果从线上解析出一个数字不适合对应的类型,您将获得与在 C++ 中将该数字强制转换为该类型相同的效果(例如,如果将 64 位数字读取为 int32,它将被截断为 32 位)。sint32
和sint64
彼此兼容,但与其他的整数类型不兼容。如果写入的值在 INT_MIN 和 INT_MAX 之间(包含),则使用任一类型解析都会得到相同的值。如果写入的 sint64 值超出该范围并解析为 sint32,则变长整数会被截断为 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
值将保留在消息中,但在反序列化消息时如何表示这取决于语言。Int 字段总是只保留其值。- 将单个
optional
字段或扩展更改为新的oneof
的成员是二进制兼容的,但对于某些语言(特别是 Go),生成的代码的 API 会以不兼容的方式更改。因此,Google 在其公共 API 中不进行此类更改,如 AIP-180 中所述。在源代码兼容性方面注意相同的事项,如果您确定没有代码会同时设置多个字段,则将多个字段移动到新的oneof
中可能是安全的。将字段移动到现有的oneof
中是不安全的。同样,将单个字段oneof
更改为optional
字段或扩展是安全的。 - 在
map
和相应的repeated
消息字段之间更改字段是二进制兼容的(有关消息布局和其他限制,请参阅下面的映射)。但是,更改的安全性取决于应用程序:在反序列化和重新序列化消息时,使用repeated
字段定义的客户端将产生语义上相同的结果;然而,使用map
字段定义的客户端可能会重新排序条目并丢弃具有重复键的条目。
未知字段
未知字段是格式良好的协议缓冲区序列化数据,代表解析器无法识别的字段。例如,当旧的二进制文件解析带有新字段的新二进制文件发送的数据时,这些新字段在旧的二进制文件中就变成了未知字段。
版本消息会保留未知字段,并在解析和序列化输出中包含它们,这与 proto2 和 proto3 的行为一致。
保留未知字段
某些操作可能导致未知字段丢失。例如,如果您执行以下操作之一,未知字段将丢失
- 将 proto 序列化为 JSON。
- 迭代消息中的所有字段以填充新消息。
为避免丢失未知字段,请执行以下操作
- 使用二进制;避免使用文本格式进行数据交换。
- 使用面向消息的 API,例如
CopyFrom()
和MergeFrom()
,来复制数据,而不是逐字段复制。
TextFormat 有点特殊。序列化到 TextFormat 会使用它们的字段编号打印未知字段。但是,如果 TextFormat 数据中包含使用字段编号的条目,则将其解析回二进制 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 {
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 {
Photo puppy_photo = 127;
}
...
}
然而,没有要求具有消息类型的扩展必须在该类型内部定义。您也可以使用标准定义模式
import "media/user_content.proto";
package puppies;
message Photo {
...
}
// This can even be in a different file.
extend media.UserContent {
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
如果您的消息有许多 singular 字段,并且最多只有一个字段会在同一时间被设置,您可以使用 oneof 特性来强制执行此行为并节省内存。
Oneof 字段类似于 singular 字段,不同之处在于 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
字段和 repeated
字段。如果需要将重复字段添加到 oneof 中,可以使用包含该重复字段的消息。
在生成的代码中,oneof 字段具有与常规字段相同的 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++ 中,如果您
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 的成员,因此无法区分这两种情况。
标签重复使用问题
- 将 singular 字段移入或移出 oneof:消息序列化和解析后,您可能会丢失部分信息(某些字段将被清除)。但是,您可以安全地将单个字段移入新的 oneof,并且如果已知一次只设置一个字段,则可能可以移动多个字段。有关更多详细信息,请参阅更新消息类型。
- 删除 oneof 字段并重新添加:消息序列化和解析后,这可能会清除您当前设置的 oneof 字段。
- 拆分或合并 oneof:这与移动 singular 字段有类似的问题。
映射
如果您想在数据定义中创建关联映射,协议缓冲区提供了一个方便的快捷语法
map<key_type, value_type> map_field = N;
...其中 key_type
可以是任何整数或字符串类型(即任何标量类型,除了浮点类型和 bytes
)。请注意,枚举或 proto 消息对于 key_type
都是无效的。value_type
可以是除另一个 map 之外的任何类型。
例如,如果您想创建一个项目映射,其中每个 Project
消息都关联一个字符串键,您可以像这样定义它
map<string, Project> projects = 3;
映射特性
- 映射不支持扩展。
- 映射字段不能是
repeated
。 - 映射值的线格式排序和映射迭代顺序是未定义的,因此您不能依赖映射项具有特定顺序。
- 为
.proto
文件生成文本格式时,映射按键排序。数值键按数值排序。 - 从线上解析或合并时,如果存在重复的映射键,则使用最后看到的键。从文本格式解析映射时,如果存在重复键,解析可能会失败。
- 如果您为映射字段提供了键但没有值,则字段序列化时的行为取决于语言。在 C++、Java、Kotlin 和 Python 中,会序列化类型的默认值,而在其他语言中则不序列化任何内容。
- 与映射
foo
在同一作用域中不能存在符号FooEntry
,因为FooEntry
已被映射的实现使用。
生成的映射 API 目前可用于所有支持的语言。您可以在相关的API 参考中找到有关您所选语言的映射 API 的更多信息。
向后兼容性
映射语法在线上等同于以下内容,因此不支持映射的协议缓冲区实现仍然可以处理您的数据
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
任何支持映射的协议缓冲区实现都必须生成和接受可以被前面定义接受的数据。
包
您可以向 .proto
文件添加一个可选的 package
指定符,以防止协议消息类型之间的名称冲突。
package foo.bar;
message Open { ... }
然后您可以在定义消息类型的字段时使用包指定符
message Foo {
...
foo.bar.Open open = 1;
...
}
包指定符影响生成代码的方式取决于您所选的语言
- 在 C++ 中,生成的类被包装在 C++ 命名空间内。例如,
Open
将位于foo::bar
命名空间中。 - 在 Java 和 Kotlin 中,包用作 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 中,包在转换为 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);
}
与协议缓冲区配合使用最直接的 RPC 系统是 gRPC:一个由 Google 开发的语言和平台无关的开源 RPC 系统。gRPC 与协议缓冲区配合得特别好,并允许您使用特殊的协议缓冲区编译器插件直接从您的 .proto
文件生成相关的 RPC 代码。
如果您不想使用 gRPC,也可以将协议缓冲区与您自己的 RPC 实现一起使用。您可以在Proto2 语言指南中找到更多信息。
还有许多正在进行中的第三方项目,用于开发 Protocol Buffers 的 RPC 实现。有关我们知道的项目链接列表,请参阅第三方附加组件 wiki 页面。
JSON 映射
标准 protobuf 二进制线格式是使用 protobuf 的两个系统之间通信的首选序列化格式。对于与使用 JSON 而不是 protobuf 线格式的系统通信,Protobuf 支持 ProtoJSON 中的规范编码。
选项
.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
(默认):协议缓冲区编译器将为您的消息类型生成用于序列化、解析和执行其他常见操作的代码。此代码经过高度优化。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 保留。packed
(字段选项):在 protobuf 版本中,此选项锁定为true
。要使用 unpacked 线格式,您可以使用版本特性覆盖此选项。这提供了与 2.3.0 版本之前解析器(很少需要)的兼容性,如下例所示repeated int32 samples = 4 [features.repeated_field_encoding = EXPANDED];
deprecated
(字段选项):如果设置为true
,表示该字段已弃用,新代码不应使用。在大多数语言中,这没有实际影响。在 Java 中,这会成为@Deprecated
注解。对于 C++,clang-tidy 在使用弃用字段时会生成警告。将来,其他语言特定的代码生成器可能会在字段的访问器上生成弃用注解,这反过来会在编译尝试使用该字段的代码时发出警告。如果该字段无人使用并且您想阻止新用户使用它,请考虑用保留语句替换字段声明。int32 old_field = 6 [deprecated = true];
枚举值选项
支持枚举值选项。您可以使用 deprecated
选项来指示某个值不应再使用。您还可以使用扩展创建自定义选项。
以下示例显示了添加这些选项的语法
import "google/protobuf/descriptor.proto";
extend google.protobuf.EnumValueOptions {
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 还允许您定义和使用自己的选项。请注意,这是一项大多数人不需要的高级功能。如果您确实认为需要创建自己的选项,请参阅Proto2 语言指南了解详细信息。请注意,创建自定义选项需要使用扩展。
选项保留
选项有一个保留的概念,它控制选项是否保留在生成的代码中。选项默认具有运行时保留,这意味着它们保留在生成的代码中,因此在运行时在生成的描述符池中可见。但是,您可以设置 retention = RETENTION_SOURCE
来指定一个选项(或选项中的字段)在运行时不得保留。这称为源代码保留。
选项保留是一项高级功能,大多数用户无需担心,但如果您想使用某些选项而不支付将其保留在二进制文件中的代码大小成本,它可能很有用。具有源代码保留的选项仍然对 protoc
和 protoc
插件可见,因此代码生成器可以使用它们来定制其行为。
保留可以直接在选项上设置,如下所示
extend google.protobuf.FileOptions {
int32 source_retention_option = 1234
[retention = RETENTION_SOURCE];
}
它也可以设置在普通字段上,在这种情况下,它仅在该字段出现在选项内部时生效
message OptionsMessage {
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 {
string file_only_option = 1 [targets = TARGET_TYPE_FILE];
int32 message_and_enum_option = 2 [targets = TARGET_TYPE_MESSAGE,
targets = TARGET_TYPE_ENUM];
}
extend google.protobuf.FileOptions {
MyOptions file_options = 50000;
}
extend google.protobuf.MessageOptions {
MyOptions message_options = 50000;
}
extend google.protobuf.EnumOptions {
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 仓库中找到它和安装说明。
协议编译器的调用方式如下
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
。
此规则的例外是当明确 proto 文件仅在 Java 环境中使用时,例如用于测试。
支持的平台
有关以下信息
- 支持的操作系统、编译器、构建系统和 C++ 版本,请参阅 Foundational C++ Support Policy。
- 支持的 PHP 版本,请参阅 Supported PHP versions。