语言指南 (proto 3)

涵盖如何在您的项目中使用 Protocol Buffers 语言的 proto3 修订版。

本指南介绍如何使用协议缓冲区语言来构建您的协议缓冲区数据,包括 .proto 文件语法以及如何从您的 .proto 文件生成数据访问类。它涵盖了协议缓冲区语言的 **proto3** 修订版。

有关 **版本** 语法的详细信息,请参阅 Protobuf 版本语言指南

有关 **proto2** 语法的详细信息,请参阅 Proto2 语言指南

这是一个参考指南 - 有关使用本文档中描述的许多功能的分步示例,请参阅您选择的语言的 教程

定义消息类型

首先让我们看一个非常简单的例子。假设您想定义一个搜索请求消息格式,其中每个搜索请求都包含一个查询字符串、您感兴趣的特定结果页面以及每页的结果数。以下是您用于定义消息类型的 .proto 文件。

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
}
  • 文件的首行指定您正在使用 protobuf 语言规范的 proto3 修订版。

    • 版本(或 proto2/proto3 的 语法)必须是文件中的第一行非空、非注释行。
    • 如果未指定 版本语法,则协议缓冲区编译器将假设您正在使用 proto2
  • SearchRequest 消息定义指定了三个字段(名称/值对),每个字段对应于您想要包含在此类型消息中的每个数据片段。每个字段都有一个名称和一个类型。

指定字段类型

在前面的示例中,所有字段都是 标量类型:两个整数 (page_numberresults_per_page) 和一个字符串 (query)。您还可以为您的字段指定 枚举 和复合类型,例如其他消息类型。

分配字段编号

您必须为消息定义中的每个字段指定一个介于 1536,870,911 之间的数字,并遵循以下限制

  • 给定的数字在该消息的所有字段中 **必须唯一**。
  • 数字 19,00019,999 保留给 Protocol Buffers 实现。如果您在消息中使用这些保留的字段编号之一,协议缓冲区编译器将发出警告。
  • 您不能使用任何先前 保留 的字段编号或已分配给 扩展 的任何字段编号。

一旦您的消息类型投入使用,此数字 **就不能更改**,因为它在 消息线格式 中标识字段。“更改”字段编号等同于删除该字段并使用相同类型但新编号创建一个新字段。有关如何正确执行此操作,请参阅 删除字段

字段编号 **永远不应重复使用**。切勿从 保留 列表中取出字段编号以供与新的字段定义一起重复使用。请参阅 重复使用字段编号的后果

您应该将字段编号 1 到 15 用于最常设置的字段。较低的字段编号值在线格式中占用更少的空间。例如,范围 1 到 15 内的字段编号需要一个字节来编码。范围 16 到 2047 内的字段编号需要两个字节。您可以在 协议缓冲区编码 中了解更多相关信息。

重复使用字段编号的后果

重复使用字段编号会使解码线格式消息变得模糊。

protobuf 线格式简洁,无法检测使用一个定义编码并使用另一个定义解码的字段。

使用一个定义编码字段,然后使用不同的定义解码同一字段会导致

  • 开发人员花费时间调试
  • 解析/合并错误(最佳情况)
  • PII/SPII 泄漏
  • 数据损坏

字段编号重复使用的常见原因

  • 重新编号字段(有时是为了为字段获得更美观的数字顺序)。重新编号有效地删除并重新添加了参与重新编号的所有字段,从而导致线格式发生不兼容的更改。
  • 删除字段且未 保留 该编号以防止将来重复使用。

字段编号限制为 29 位而不是 32 位,因为三位用于指定字段的线格式。有关此内容的更多信息,请参阅 编码主题

指定字段基数

消息字段可以是以下之一

  • 单数:

    在 proto3 中,有两种类型的单数字段

    • optional:(推荐) optional 字段处于两种可能状态之一

      • 该字段已设置,并包含显式设置或从线格式解析的值。它将被序列化到线格式。
      • 该字段未设置,并将返回默认值。它不会被序列化到线格式。

      您可以检查该值是否已显式设置。

      对于与 protobuf 版本和 proto2 的最大兼容性,建议使用 optional 而不是隐式字段。

    • 隐式:(不推荐) 隐式字段没有显式基数标签,其行为如下

      • 如果字段是消息类型,则其行为与 optional 字段完全相同。

      • 如果字段不是消息,则它有两种状态

        • 该字段设置为非默认(非零)值,该值是显式设置或从线格式解析的。它将被序列化到线格式。
        • 该字段设置为默认(零)值。它不会被序列化到线格式。事实上,您无法确定默认(零)值是设置的还是从线格式解析的,或者根本没有提供。有关此主题的更多信息,请参阅 字段存在性
  • repeated:此字段类型可以在格式良好的消息中重复零次或多次。重复值的顺序将被保留。

  • map:这是一个成对的键/值字段类型。有关此字段类型的更多信息,请参阅 映射

重复字段默认情况下被打包

在 proto3 中,标量数字类型的 repeated 字段默认使用 packed 编码。

您可以在 协议缓冲区编码 中了解更多有关 packed 编码的信息。

消息类型字段始终具有字段存在性

在 proto3 中,消息类型字段已经具有字段存在性。因此,添加 optional 修饰符不会更改字段的字段存在性。

以下代码示例中 Message2Message3 的定义为所有语言生成相同的代码,并且二进制、JSON 和 TextFormat 中的表示形式没有区别

syntax="proto3";

package foo.bar;

message Message1 {}

message Message2 {
  Message1 foo = 1;
}

message Message3 {
  optional Message1 bar = 1;
}

格式良好的消息

术语“格式良好”在应用于 protobuf 消息时,指的是序列化/反序列化的字节。protoc 解析器验证给定的 proto 定义文件是否可解析。

单数字段可以在线格式字节中多次出现。解析器将接受输入,但只有该字段的最后一个实例可以通过生成的绑定访问。有关此主题的更多信息,请参阅 最后一次获胜

添加更多消息类型

可以在单个 .proto 文件中定义多个消息类型。如果您正在定义多个相关消息,这将很有用 - 因此,例如,如果您想定义与您的 SearchResponse 消息类型相对应的回复消息格式,您可以将其添加到同一个 .proto

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
}

message SearchResponse {
 ...
}

**组合消息会导致膨胀** 虽然可以在单个 .proto 文件中定义多个消息类型(例如消息、枚举和服务),但当在单个文件中定义大量具有不同依赖项的消息时,也会导致依赖项膨胀。建议每个 .proto 文件包含尽可能少的的消息类型。

添加注释

要向您的 .proto 文件添加注释

  • 在 .proto 代码元素之前的行上首选 C/C++/Java 行尾样式注释“//”

  • 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 到 119、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.hpbobjc.m文件,每个文件中描述的消息类型对应一个类。
  • 对于C#,编译器从每个.proto生成一个.cs文件,每个文件中描述的消息类型对应一个类。
  • 对于PHP,编译器为每个文件中描述的消息类型生成一个.php消息文件,并为每个编译的.proto文件生成一个.php元数据文件。元数据文件用于将有效的消息类型加载到描述符池中。
  • 对于Dart,编译器生成一个.pb.dart文件,其中包含文件中每个消息类型的类。

您可以通过按照您选择的语言的教程了解更多关于使用每种语言的 API 的信息。有关更多 API 详细信息,请参阅相关的API 参考

标量值类型

标量消息字段可以具有以下类型之一——表格显示了.proto文件中指定的类型以及自动生成的类中的对应类型

.proto 类型注释C++ 类型Java/Kotlin 类型[1]Python 类型[3]Go 类型Ruby 类型C# 类型PHP 类型Dart 类型Rust 类型
doubledoubledoublefloatfloat64Floatdoublefloatdoublef64
floatfloatfloatfloatfloat32Floatfloatfloatdoublef32
int32使用可变长度编码。对编码负数效率低下——如果您的字段可能包含负值,请改用 sint32。int32intintint32Fixnum 或 Bignum(根据需要)intintegerinti32
int64使用可变长度编码。对编码负数效率低下——如果您的字段可能包含负值,请改用 sint64。int64longint/long[4]int64Bignumlonginteger/string[6]Int64i64
uint32使用可变长度编码。uint32int[2]int/long[4]uint32Fixnum 或 Bignum(根据需要)uintintegerintu32
uint64使用可变长度编码。uint64long[2]int/long[4]uint64Bignumulonginteger/string[6]Int64u64
sint32使用可变长度编码。有符号整数类型。这些类型比常规 int32 更有效地编码负数。int32intintint32Fixnum 或 Bignum(根据需要)intintegerinti32
sint64使用可变长度编码。有符号整数类型。这些类型比常规 int64 更有效地编码负数。int64longint/long[4]int64Bignumlonginteger/string[6]Int64i64
fixed32始终为四个字节。如果值通常大于 228,则比 uint32 更有效。uint32int[2]int/long[4]uint32Fixnum 或 Bignum(根据需要)uintintegerintu32
fixed64始终为八个字节。如果值通常大于 256,则比 uint64 更有效。uint64long[2]int/long[4]uint64Bignumulonginteger/string[6]Int64u64
sfixed32始终为四个字节。int32intintint32Fixnum 或 Bignum(根据需要)intintegerinti32
sfixed64始终为八个字节。int64longint/long[4]int64Bignumlonginteger/string[6]Int64i64
boolboolbooleanboolboolTrueClass/FalseClassboolbooleanboolbool
string字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,并且长度不能超过 232stringStringstr/unicode[5]stringString (UTF-8)stringstringStringProtoString
bytes可以包含任何任意字节序列,长度不超过 232stringByteStringstr (Python 2)
bytes (Python 3)
[]byteString (ASCII-8BIT)ByteStringstringListProtoBytes

[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。

您可以在协议缓冲区编码中了解有关序列化消息时如何编码这些类型的更多信息。

默认字段值

解析消息时,如果编码的消息字节不包含特定字段,则访问解析对象中的该字段将返回该字段的默认值。默认值是特定于类型的。

  • 对于字符串,默认值为空字符串。
  • 对于字节,默认值为空字节。
  • 对于布尔值,默认值为 false。
  • 对于数字类型,默认值为零。
  • 对于消息字段,该字段未设置。其确切值取决于语言。有关详细信息,请参阅生成的代码指南
  • 对于枚举,默认值为**第一个定义的枚举值**,该值必须为 0。请参阅枚举默认值

重复字段的默认值为为空(通常是相应语言中的空列表)。

映射字段的默认值为为空(通常是相应语言中的空映射)。

请注意,对于隐式存在的标量字段,一旦解析消息,就无法判断该字段是否显式设置为默认值(例如,布尔值是否设置为false)或根本未设置:在定义消息类型时应牢记这一点。例如,如果不想默认发生该行为,则不要在设置为false时启用某些行为的布尔值。另请注意,如果标量消息字段**确实**设置为其默认值,则该值不会在网络上序列化。如果浮点数或双精度数值设置为 +0,则不会序列化,但 -0 被视为不同的值,并将被序列化。

有关默认值在生成代码中如何工作的更多详细信息,请参阅您选择的语言的生成的代码指南

枚举

在定义消息类型时,您可能希望其字段之一仅具有预定义值列表之一。例如,假设您想为每个SearchRequest添加一个corpus字段,其中 corpus 可以是UNIVERSALWEBIMAGESLOCALNEWSPRODUCTSVIDEO。您可以通过向消息定义添加一个enum来非常简单地做到这一点,其中每个可能的值对应一个常量。

在以下示例中,我们添加了一个名为Corpusenum,其中包含所有可能的值,以及一个类型为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,因为它是枚举中定义的第一个值。

在 proto3 中,枚举定义中第一个定义的值**必须**为零,并且应该命名为ENUM_TYPE_NAME_UNSPECIFIEDENUM_TYPE_NAME_UNKNOWN。这是因为

  • 必须存在一个零值,以便我们可以使用 0 作为数字默认值
  • 零值需要作为第一个元素,以与proto2语义兼容,在 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值在网络上使用varint 编码,因此负值效率低下,因此不建议使用。您可以在消息定义内定义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 "myproject/other_protos.proto";

默认情况下,您只能使用直接导入的.proto文件中的定义。但是,有时您可能需要将.proto文件移动到新位置。与其直接移动.proto文件并在单个更改中更新所有调用站点,不如在旧位置放置一个占位符.proto文件,以使用import public概念将所有导入转发到新位置。

请注意,Java、Kotlin、TypeScript、JavaScript、GCL 以及使用 protobuf 静态反射的 C++ 目标中不提供公共导入功能。

import public依赖项可以被导入包含import public语句的 proto 的任何代码传递依赖。例如

// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto

协议编译器在协议编译器命令行上使用-I/--proto_path标志指定的一组目录中搜索导入的文件。如果未给出标志,它会在编译器调用的目录中查找。通常,您应该将--proto_path标志设置为项目的根目录,并对所有导入使用完全限定名称。

使用 proto2 消息类型

可以导入proto2消息类型并在 proto3 消息中使用它们,反之亦然。但是,proto2 枚举不能直接在 proto3 语法中使用(如果导入的 proto2 消息使用它们则可以)。

嵌套类型

您可以在其他消息类型内部定义和使用消息类型,如下例所示 - 在这里,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;
    }
  }
}

更新消息类型

如果现有的消息类型不再满足您的所有需求 - 例如,您希望消息格式具有额外的字段 - 但您仍然希望使用使用旧格式创建的代码,请不要担心!当您使用二进制线格式时,更新消息类型而不会破坏任何现有代码非常简单。

检查Proto 最佳实践和以下规则

  • 不要更改任何现有字段的字段编号。“更改”字段编号等同于删除字段并添加一个具有相同类型的新字段。如果要重新编号字段,请参阅有关删除字段的说明。
  • 如果您添加新字段,则使用“旧”消息格式序列化的任何消息仍然可以由您的新生成代码进行解析。您应该牢记这些元素的默认值,以便新代码可以正确地与旧代码生成的 message 交互。类似地,由您的新代码创建的消息可以由您的旧代码进行解析:旧二进制文件在解析时只会忽略新字段。有关详细信息,请参阅未知字段部分。
  • 可以删除字段,只要字段编号在更新的消息类型中不再使用即可。您可能希望重命名字段,也许添加前缀“OBSOLETE_”,或者使字段编号保留,以便您.proto的未来用户不会意外地重复使用该编号。
  • int32uint32int64uint64bool都兼容 - 这意味着您可以将字段从这些类型中的一个更改为另一个,而不会破坏向前或向后兼容性。如果从网络解析的数字不适合相应的类型,您将获得与在 C++ 中将数字强制转换为该类型相同的效果(例如,如果将 64 位数字读取为 int32,它将被截断为 32 位)。
  • sint32sint64彼此兼容,但与其他整数类型**不**兼容。
  • 只要字节是有效的 UTF-8,stringbytes就是兼容的。
  • 如果字节包含消息的编码实例,则嵌入式消息与bytes兼容。
  • fixed32sfixed32兼容,fixed64sfixed64兼容。
  • 对于stringbytes和消息字段,单数与repeated兼容。给定重复字段的序列化数据作为输入,期望此字段为单数的客户端将在它是基本类型字段时采用最后一个输入值,或者如果它是消息类型字段则合并所有输入元素。请注意,这对于数字类型(包括布尔值和枚举)通常**不安全**。数字类型的重复字段默认情况下以packed格式序列化,当期望单数字段时,将无法正确解析。
  • enum在线格式方面与int32uint32int64uint64兼容(请注意,如果值不适合,则会将其截断)。但是,请注意,当消息反序列化时,客户端代码可能会以不同的方式处理它们:例如,将保留无法识别的 proto3 enum值,但消息反序列化时如何表示取决于语言。Int 字段始终只保留其值。
  • 将单个optional字段或扩展名更改为**新**oneof的成员是二进制兼容的,但是对于某些语言(特别是 Go),生成的代码的 API 将以不兼容的方式更改。出于这个原因,Google 不会在其公共 API 中进行此类更改,如AIP-180中所述。对于源代码兼容性的相同注意事项,如果确定没有代码一次设置多个字段,则将多个字段移动到新的 oneof可能是安全的。将字段移动到现有的oneof是不安全的。同样,将单个字段oneof更改为optional字段或扩展名是安全的。
  • map<K, V>和相应的repeated消息字段之间更改字段是二进制兼容的(有关消息布局和其他限制,请参阅下面的Maps)。但是,更改的安全性取决于应用程序:在反序列化和重新序列化消息时,使用repeated字段定义的客户端将产生语义上相同的结果;但是,使用map字段定义的客户端可能会重新排序条目并删除具有重复键的条目。

未知字段

未知字段是表示解析器无法识别的字段的格式良好的协议缓冲区序列化数据。例如,当旧二进制文件解析由具有新字段的新二进制文件发送的数据时,这些新字段成为旧二进制文件中的未知字段。

Proto3 消息保留未知字段并在解析和序列化输出期间包含它们,这与 proto2 行为相匹配。

保留未知字段

某些操作可能导致未知字段丢失。例如,如果您执行以下操作之一,则未知字段将丢失

  • 将 proto 序列化为 JSON。
  • 遍历消息中的所有字段以填充新消息。

为了避免丢失未知字段,请执行以下操作

  • 使用二进制格式;避免使用文本格式进行数据交换。
  • 使用面向消息的 API,例如 CopyFrom()MergeFrom(),来复制数据,而不是逐字段复制。

TextFormat 是一种特殊情况。序列化为 TextFormat 会使用字段编号打印未知字段。但是,如果解析的 TextFormat 数据中包含使用字段编号的条目,则将其解析回二进制 proto 将失败。

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 ...
  }
}

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 字段和 repeated 字段。如果您需要将重复字段添加到 oneof,则可以使用包含重复字段的消息。

在您的生成代码中,oneof 字段具有与常规字段相同的 getter 和 setter。您还可以获得一个用于检查 oneof 中设置了哪个值(如果有)的特殊方法。您可以在相关的API 参考中找到有关您选择的语言的 oneof API 的更多信息。

Oneof 特性

  • 设置 oneof 字段将自动清除 oneof 的所有其他成员。因此,如果您设置了几个 oneof 字段,则只有您设置的*最后一个*字段仍将具有值。

    SampleMessage message;
    message.set_name("name");
    CHECK_EQ(message.name(), "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.name().empty());
    
  • 如果解析器在网络上遇到同一 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_EQ(msg2.name(), "name");
    

向后兼容性问题

在添加或删除 oneof 字段时要小心。如果检查 oneof 的值返回 None/NOT_SET,则可能意味着尚未设置 oneof 或已将其设置为 oneof 的不同版本中的字段。无法区分两者,因为无法知道网络上的未知字段是否是 oneof 的成员。

标签重用问题

  • 将单数字段移入或移出 oneof:在消息序列化和解析后,您可能会丢失一些信息(某些字段将被清除)。但是,您可以安全地将单个字段移入一个新的 oneof,并且如果已知一次最多只设置一个字段,则可以移动多个字段。有关更多详细信息,请参阅更新消息类型
  • 删除一个 oneof 字段并将其添加回来:这可能会在消息序列化和解析后清除您当前设置的 oneof 字段。
  • 拆分或合并 oneof:这与移动单数字段有类似的问题。

映射

如果要创建关联映射作为数据定义的一部分,协议缓冲区提供了一个方便的快捷语法

map<key_type, value_type> map_field = N;

…其中 key_type 可以是任何整数或字符串类型(因此,除了浮点类型和 bytes 之外的任何标量类型)。请注意,枚举或 proto 消息对于 key_type 无效。value_type 可以是任何类型,除了另一个映射。

因此,例如,如果您想创建一个项目的映射,其中每个 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 命名空间中。
  • JavaKotlin中,包用作 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 语言指南中找到更多相关信息。

还有一些正在进行的第三方项目来开发用于协议缓冲区的 RPC 实现。有关我们了解的项目的链接列表,请参阅第三方插件维基页面

JSON 映射

标准 protobuf 二进制线格式是使用 protobufs 的两个系统之间通信的首选序列化格式。为了与使用 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(文件选项):可以设置为SPEEDCODE_SIZELITE_RUNTIME。这将以以下方式影响 C++ 和 Java 代码生成器(以及可能的三方生成器)

    • SPEED(默认):协议缓冲区编译器将生成用于序列化、解析和对消息类型执行其他常见操作的代码。此代码经过高度优化。
    • CODE_SIZE:协议缓冲区编译器将生成最小的类,并将依靠共享的基于反射的代码来实现序列化、解析和各种其他操作。因此,生成的代码将比SPEED 模式下的代码小得多,但操作速度会更慢。类仍然会完全实现与SPEED 模式下相同的公共 API。此模式在包含大量.proto 文件且不需要所有文件都非常快的应用程序中最有用。
    • LITE_RUNTIME:协议缓冲区编译器将生成仅依赖于“精简”运行时库(libprotobuf-lite 而不是libprotobuf)的类。精简运行时库比完整库小得多(大约小一个数量级),但省略了描述符和反射等某些功能。这对于在移动电话等受限平台上运行的应用程序特别有用。编译器仍将像在SPEED 模式下一样生成所有方法的快速实现。生成的类将仅在每种语言中实现MessageLite 接口,该接口仅提供完整Message 接口方法的子集。
    option optimize_for = CODE_SIZE;
    
  • cc_generic_servicesjava_generic_servicespy_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(字段选项):在基本数字类型的重复字段上默认为true,导致使用更紧凑的编码。要使用未打包的线格式,可以将其设置为false。这提供了与 2.3.0 版之前的解析器(很少需要)的兼容性,如下例所示

    repeated int32 samples = 4 [packed = false];
    
  • deprecated(字段选项):如果设置为true,则表示该字段已弃用,不应由新代码使用。在大多数语言中,这实际上没有任何影响。在 Java 中,这将成为一个@Deprecated 注释。对于 C++,clang-tidy 将在使用已弃用字段时生成警告。将来,其他特定于语言的代码生成器可能会在字段的访问器上生成弃用注释,这反过来会导致在编译尝试使用该字段的代码时发出警告。如果该字段无人使用,并且您想阻止新用户使用它,请考虑使用reserved 语句替换字段声明。

    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 还允许您定义和使用自己的选项。请注意,这是一个高级功能,大多数人不需要。如果您确实认为需要创建自己的选项,请参阅Proto2 语言指南 获取详细信息。请注意,创建自定义选项使用扩展,这些扩展仅在 proto3 中允许用于自定义选项。

选项保留

选项具有保留的概念,它控制选项是否保留在生成的代码中。选项默认具有运行时保留,这意味着它们保留在生成的代码中,因此在生成的描述符池中在运行时可见。但是,您可以设置retention = RETENTION_SOURCE 以指定选项(或选项中的字段)不得在运行时保留。这称为源保留

选项保留是一个高级功能,大多数用户无需担心,但如果您希望在不为保留它们在二进制文件中支付代码大小成本的情况下使用某些选项,则它可能很有用。具有源保留的选项仍然对protocprotoc 插件可见,因此代码生成器可以使用它们来自定义其行为。

保留可以直接设置在选项上,如下所示

extend google.protobuf.FileOptions {
  optional 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 来覆盖这一点。

选项目标

字段有一个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 {
  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 的简写形式。

  • 您可以提供一个或多个输出指令

    作为一个额外的便利,如果 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 上下文中使用时,例如用于测试。

支持的平台

有关