语言指南 (proto 2)

涵盖如何在您的项目中使用 Protocol Buffers 语言的 proto2 版本。

本指南介绍如何使用 Protocol Buffer 语言来构建您的 Protocol Buffer 数据,包括 .proto 文件语法以及如何从您的 .proto 文件生成数据访问类。它涵盖了 Protocol Buffer 语言的 **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 版本。

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

指定字段类型

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

分配字段编号

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

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

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

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

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

重复使用字段编号的后果

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

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

使用一个定义编码字段,然后使用另一个定义解码同一字段可能导致

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

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

  • 重新编号字段(有时是为了实现字段更美观的数字顺序)。重新编号实际上会删除并重新添加参与重新编号的所有字段,从而导致不兼容的线格式更改。

  • 删除字段且未 保留 该编号以防止将来重复使用。

    • 由于以下几个原因,在使用 扩展字段 时,这很容易出错。扩展声明 提供了一种保留扩展字段的机制。

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

指定字段基数

消息字段可以是以下之一

  • 单数:

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

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

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

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

    • required:**不要使用。** required 字段问题很多,因此已从 proto3 和版本中删除。required 字段的语义应在应用程序层实现。当它确实被使用时,格式良好的消息必须恰好包含此字段的一个实例。

  • repeated:此字段类型可以在格式良好的消息中重复零次或多次。重复值的顺序将保留。

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

对新的重复字段使用打包编码

出于历史原因,标量数值类型(例如,int32int64enum)的 repeated 字段的编码效率并不像它们本可以的那样高。新代码应使用特殊选项 [packed = true] 以获得更高效的编码。例如

repeated int32 samples = 4 [packed = true];
repeated ProtoEnum results = 5 [packed = true];

您可以在 Protocol Buffer 编码 中了解更多关于 packed 编码的信息。

强烈建议弃用 Required

当有人向枚举添加值时,就会出现关于必填字段的第二个问题。在这种情况下,无法识别的枚举值会被视为缺失,这也会导致必填值检查失败。

格式良好的消息

当应用于 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文件添加注释

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

  • 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字段,则应首先将该字段标记为optionaldeprecated,并确保所有以任何方式观察消息的系统都已部署了新模式。然后,您可以考虑删除该字段(但请注意,这仍然是一个容易出错的过程)。

当您不再需要一个非required字段时,首先从客户端代码中删除对该字段的所有引用,然后从消息中删除该字段定义。但是,您**必须**保留已删除的字段编号。如果您不保留字段编号,开发人员将来可能会重用该编号并导致中断。

您还应保留字段名称,以允许继续解析消息的 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 类型
doubledoubledoublefloat*float64Floatdoublefloatdoublef64
floatfloatfloatfloat*float32Floatfloatfloatdoublef32
int32使用可变长度编码。对编码负数效率低下——如果您的字段可能包含负值,请改用 sint32。int32intintint32Fixnum 或 Bignum(根据需要)intinteger*int32i32
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(根据需要)intinteger*int32i32
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始终为四个字节。int32intint*int32Fixnum 或 Bignum(根据需要)intintegerinti32
sfixed64始终为八个字节。int64longint/long[4]*int64Bignumlonginteger/string[6]Int64i64
boolboolbooleanbool*boolTrueClass/FalseClassboolbooleanboolbool
string字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,并且不能超过 232stringStringunicode(Python 2)或 str(Python 3)*stringString(UTF-8)stringstringStringProtoString
bytes可以包含任何任意字节序列,不超过 232stringByteStringbytes[]byteString(ASCII-8BIT)ByteStringstringListProtoBytes

[1] Kotlin 使用 Java 中的对应类型,即使对于无符号类型也是如此,以确保在混合 Java/Kotlin 代码库中的兼容性。

[2] 在 Java 中,无符号 32 位和 64 位整数使用其有符号对应部分表示,最高位简单地存储在符号位中。

[3] 在所有情况下,将值设置为字段都会执行类型检查以确保其有效。

[4] 64 位或无符号 32 位整数在解码时始终表示为 long,但如果在设置字段时给定 int,则可以为 int。在所有情况下,设置时值必须适合表示的类型。参见 [2]。

[5] Proto2 通常从不检查字符串字段的 UTF-8 有效性。尽管语言之间行为有所不同,但不应在字符串字段中存储无效的 UTF-8 数据。

[6] 在 64 位机器上使用 Integer,在 32 位机器上使用 string。

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

默认字段值

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

  • 对于字符串,默认值为空字符串。
  • 对于字节,默认值为空字节。
  • 对于布尔值,默认值为 false。
  • 对于数字类型,默认值为零。
  • 对于消息字段,该字段未设置。其确切值取决于语言。有关详细信息,请参阅您语言的生成代码指南
  • 对于枚举,默认值为第一个定义的枚举值,它应该是 0(推荐用于与 proto3 兼容)。请参阅枚举默认值

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

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

覆盖默认标量值

在 proto2 中,您可以为单数非消息字段指定显式默认值。例如,假设您希望为SearchRequest.result_per_page字段提供默认值 10

optional 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”返回发送方值。

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

由于枚举的默认值为第一个定义的枚举值,因此在枚举值列表的开头添加值时请注意。有关如何安全地更改定义的指南,请参阅更新消息类型部分。

枚举

在定义消息类型时,您可能希望其字段之一仅具有预定义值列表之一。例如,假设您希望为每个SearchRequest添加一个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 {
  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值在网络上使用变长整数编码,因此负值效率低下,因此不建议使用。您可以在消息定义中定义enum,如前面的示例所示,或者在外部定义——这些enum可以在您的.proto文件中的任何消息定义中重复使用。您还可以使用在一个消息中声明的enum类型作为另一个消息中字段的类型,使用语法_MessageType_._EnumType_

当您在使用enum.proto上运行协议缓冲区编译器时,生成的代码将具有与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 "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标志设置为项目的根目录,并对所有导入使用完全限定名称。

使用 proto3 消息类型

可以导入proto3消息类型并在您的 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;
    }
  }
}

请注意,组功能已弃用,创建新的消息类型时不应使用。请改用嵌套消息类型。

组是另一种在消息定义中嵌套信息的方式。例如,指定包含多个ResultSearchResponse的另一种方法如下所示。

message SearchResponse {
  repeated group Result = 1 {
    optional string url = 1;
    optional string title = 2;
    repeated string snippets = 3;
  }
}

组只是将嵌套消息类型和字段组合到一个声明中。在您的代码中,您可以像对待具有名为resultResult类型字段的消息一样对待此消息(后者名称转换为小写,以避免与前者冲突)。因此,此示例与前面的SearchResponse完全等效,只是消息具有不同的线格式

更新消息类型

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

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

  • 不要更改任何现有字段的字段编号。“更改”字段编号等同于删除字段并添加一个具有相同类型的新字段。如果要重新编号字段,请参阅有关删除字段的说明。
  • 您添加的任何新字段都应为optionalrepeated。这意味着使用您的“旧”消息格式序列化的任何消息仍然可以通过您的新生成代码进行解析,因为它们不会缺少任何required元素。您应该牢记这些元素的默认值,以便新代码能够正确地与旧代码生成的消息进行交互。类似地,由新代码创建的消息可以通过旧代码进行解析:旧二进制文件在解析时只需忽略新字段。但是,未知字段不会被丢弃,如果稍后序列化消息,则未知字段将与其一起序列化——因此,如果消息传递给新代码,则新字段仍然可用。有关详细信息,请参阅未知字段部分。

  • 非必需字段可以移除,只要该字段编号在更新的消息类型中不再被使用。您可能希望重命名该字段,例如添加前缀“OBSOLETE_”,或者将字段编号保留,以便将来使用您的.proto的用户不会意外地重复使用该编号。
  • 非必需字段可以转换为扩展,反之亦然,只要类型和编号保持不变。
  • int32uint32int64uint64bool都是兼容的——这意味着您可以将一个字段从这些类型中的一个更改为另一个,而不会破坏向前或向后兼容性。如果从网络解析出的数字不适合相应的类型,您将获得与在 C++ 中将数字强制转换为该类型相同的效果(例如,如果将 64 位数字读取为 int32,它将被截断为 32 位)。
  • sint32sint64彼此兼容,但与其他整数类型兼容。
  • stringbytes兼容,只要字节是有效的 UTF-8。
  • 如果字节包含消息的编码实例,则嵌入式消息与bytes兼容。
  • fixed32sfixed32兼容,fixed64sfixed64兼容。
  • 对于stringbytes和消息字段,单数与repeated兼容。给定重复字段的序列化数据作为输入,如果预期该字段为单数的客户端将在它是原始类型字段时获取最后一个输入值,或者如果它是消息类型字段则合并所有输入元素。请注意,这通常对于数字类型(包括布尔值和枚举)不安全。数字类型的重复字段可能以打包格式序列化,当预期单数字段时,该格式将无法正确解析。
  • 更改默认值通常是可以的,只要您记住默认值永远不会通过网络发送。因此,如果程序接收一条消息,其中某个特定字段未设置,则程序将看到该程序的协议版本中定义的默认值。它不会看到发送方代码中定义的默认值。
  • 就线格式而言,enumint32uint32int64uint64兼容(请注意,如果值不适合,它们将被截断)。但是,请注意,当消息被反序列化时,客户端代码可能会以不同的方式处理它们。值得注意的是,当消息被反序列化时,无法识别的enum值会被丢弃,这会导致字段的has..访问器返回false,并且其 getter 返回enum定义中列出的第一个值,或者如果指定了默认值则返回默认值。对于重复的枚举字段,任何无法识别的值都会从列表中删除。但是,整数字段将始终保留其值。因此,在将整数升级为enum时,您需要非常小心接收网络上的超出范围的枚举值。
  • 在当前的 Java 和 C++ 实现中,当无法识别的enum值被删除时,它们会与其他未知字段一起存储。请注意,如果将此数据序列化然后由识别这些值的客户端重新解析,这可能会导致奇怪的行为。对于可选字段,即使在原始消息反序列化后写入了一个新值,识别它的客户端仍然会读取旧值。对于重复字段,旧值将出现在任何已识别和新添加的值之后,这意味着顺序将不会保留。
  • 将单个optional字段或扩展更改为oneof的成员是二进制兼容的,但是对于某些语言(特别是 Go),生成的代码的 API 将以不兼容的方式更改。出于这个原因,Google 不会在其公共 API 中进行此类更改,如AIP-180中所述。对于源代码兼容性,如果确定没有代码一次设置多个字段,则将多个字段移动到新的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.Videorepeated扩展字段使用。要了解有关扩展声明的更多信息,请参阅扩展声明

请注意,容器消息的文件(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];
}

增加起始字段编号或减小结束字段编号以移动或缩小扩展范围是不安全的。这些更改可能会使现有扩展无效。

最好将字段编号 1 到 15 用于在大多数 proto 实例中填充的标准字段。不建议将这些数字用于扩展。

如果您的编号约定可能涉及扩展具有非常大的字段编号,则可以使用max关键字指定您的扩展范围达到最大可能的字段编号。

message Foo {
  extensions 1000 to max;
}

max为 229 - 1,或 536,870,911。

选择扩展编号

扩展只是可以在其容器消息外部指定的字段。所有相同的分配字段编号规则都适用于扩展字段编号。重复使用字段编号的后果也适用于重复使用扩展字段编号。

如果容器消息使用扩展声明,则选择唯一的扩展字段编号非常简单。在定义新扩展时,选择高于容器消息中定义的最高扩展范围的所有其他声明的最低字段编号。例如,如果容器消息定义如下

message Container {
  // Legacy range that was using an unverified allocation scheme
  extensions 1000 to 524999999;
  // Current range that uses extension declarations. (highest extension range)
  extensions 525000000 to max  [
    declaration = {
      number: 525000001,
      full_name: ".bar.baz_ext",
      type: ".bar.Baz"
    }
    // 525,000,002 is the lowest field number above all other declarations
  ];
}

Container的下一个扩展应添加一个新的声明,编号为525000002

未经验证的扩展编号分配(不推荐)

容器消息的所有者可以选择放弃扩展声明,转而使用他们自己的未经验证的扩展编号分配策略。

未经验证的分配方案使用 protobuf 生态系统外部的机制在选定的扩展范围内分配扩展字段编号。一个示例可能是使用单存储库的提交编号。从 protobuf 编译器的角度来看,此系统是“未经验证的”,因为无法检查扩展是否正在使用正确获取的扩展字段编号。

与扩展声明等已验证系统相比,未经验证系统的优势在于能够在不与容器消息所有者协调的情况下定义扩展。

未经验证系统的缺点是 protobuf 编译器无法保护参与者免于重复使用扩展字段编号。

不建议使用未经验证的扩展字段编号分配策略,因为重复使用字段编号的后果将落在消息的所有扩展程序上(而不仅仅是未遵循建议的开发人员)。如果您的用例需要非常低的协调,请考虑改用Any消息

未经验证的扩展字段编号分配策略仅限于 1 到 524,999,999 的范围。字段编号 525,000,000 及以上只能与扩展声明一起使用。

指定扩展类型

扩展可以是任何字段类型,除了oneofmap

嵌套扩展(不推荐)

您可以在另一个消息的范围内声明扩展。

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_countpuppies.Photo的范围内定义。

这是一个常见的混淆来源:在消息类型内部嵌套声明extend并不意味着外部类型和扩展类型之间有任何关系。特别是,前面的示例并不意味着PhotoUserProfile的任何子类。它仅仅意味着符号likes_countPhoto的范围内声明;它只是一个静态成员。

一个常见的模式是在扩展字段类型的范围内定义扩展 - 例如,这是一个对类型为puppies.Photomedia.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字段之外的任何类型的字段,但不能使用requiredoptionalrepeated关键字。如果您需要向 oneof 添加重复字段,可以使用包含重复字段的消息。

在生成的代码中,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 不能是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字段存在类似的问题。

映射

如果您想在数据定义中创建关联映射,协议缓冲区提供了一个方便的快捷语法

map<key_type, value_type> map_field = N;

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

因此,例如,如果您想创建项目映射,其中每个Project消息都与字符串键关联,则可以这样定义它

map<string, Project> projects = 3;

映射特性

  • 映射不支持扩展。
  • 映射不能是repeatedoptionalrequired
  • 映射值的线格式排序和映射迭代排序未定义,因此您不能依赖于映射项以特定顺序排列。
  • 当为.proto生成文本格式时,映射按键排序。数字键按数字排序。
  • 当从网络解析或合并时,如果存在重复的映射键,则使用最后看到的键。当从文本格式解析映射时,如果存在重复的键,则解析可能会失败。
  • 如果您提供了一个键但没有提供映射字段的值,则该字段序列化时的行为取决于语言。在 C++、Java、Kotlin 和 Python 中,会序列化该类型的默认值,而在其他语言中则不会序列化任何内容。
  • 在与映射foo相同的范围内,不能存在符号FooEntry,因为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命名空间中。
  • 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);
}

默认情况下,协议编译器将生成一个名为 SearchService 的抽象接口和一个相应的“存根”实现。存根将所有调用转发到 RpcChannelRpcChannel 本身是一个抽象接口,您必须根据自己的 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 来定义 gRPC 服务。您可以在 Proto3 语言指南 中了解更多关于 proto3 语法的信息。如果您确实想将 proto2 与 gRPC 一起使用,则需要使用 3.0.0 或更高版本的协议缓冲区编译器和库。

除了 gRPC 之外,还有许多正在进行的第三方项目来开发协议缓冲区的 RPC 实现。有关我们已知项目的链接列表,请参阅 第三方插件维基页面

JSON 映射

标准的 protobuf 二进制线格式是使用 protobuf 的两个系统之间通信的首选序列化格式。对于与使用 JSON 而不是 protobuf 线格式的系统通信,Protobuf 支持 JSON 中的规范编码。

选项

.proto 文件中的单个声明可以使用多个选项进行注释。选项不会更改声明的整体含义,但可能会影响它在特定上下文中的处理方式。可用选项的完整列表在 /google/protobuf/descriptor.proto 中定义。

一些选项是文件级选项,这意味着它们应该写在顶级作用域,而不是在任何消息、枚举或服务定义内部。一些选项是消息级选项,这意味着它们应该写在消息定义内部。一些选项是字段级选项,这意味着它们应该写在字段定义内部。选项也可以写在枚举类型、枚举值、oneof 字段、服务类型和服务方法上;但是,目前还没有任何有用的选项适用于这些。

以下是一些最常用的选项

  • java_package(文件选项):您要用于生成的 Java/Kotlin 类的包。如果在 .proto 文件中没有给出显式的 java_package 选项,则默认情况下将使用 proto 包(在 .proto 文件中使用“package”关键字指定)。但是,proto 包通常不是好的 Java 包,因为 proto 包不需要以反向域名开头。如果不生成 Java 或 Kotlin 代码,则此选项无效。

    option java_package = "com.example.foo";
    
  • java_outer_classname(文件选项):您要生成的包装 Java 类的类名(以及文件名)。如果在 .proto 文件中没有指定显式的 java_outer_classname,则类名将通过将 .proto 文件名转换为驼峰式大小写来构造(因此 foo_bar.proto 变成 FooBar.java)。如果 java_multiple_files 选项被禁用,则为 .proto 文件生成的 所有其他类/枚举等都将在该外部包装 Java 类中作为嵌套类/枚举等生成。如果不生成 Java 代码,则此选项无效。

    option java_outer_classname = "Ponycopter";
    
  • java_multiple_files(文件选项):如果为 false,则仅为该 .proto 文件生成一个 .java 文件,并且为顶级消息、服务和枚举生成的 所有 Java 类/枚举等都将嵌套在外部类中(请参阅 java_outer_classname)。如果为 true,则将为为顶级消息、服务和枚举生成的每个 Java 类/枚举等生成单独的 .java 文件,并且为该 .proto 文件生成的包装 Java 类将不包含任何嵌套类/枚举等。这是一个布尔选项,默认为 false。如果不生成 Java 代码,则此选项无效。

    option java_multiple_files = true;
    
  • optimize_for(文件选项):可以设置为 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 保留。

  • message_set_wire_format(消息选项):如果设置为 true,则消息使用不同的二进制格式,旨在与 Google 内部使用的旧格式 MessageSet 兼容。Google 以外的用户可能永远不需要使用此选项。消息必须按如下方式声明

    message Foo {
      option message_set_wire_format = true;
      extensions 4 to max;
    }
    
  • packed(字段选项):如果在基本数字类型的重复字段上设置为 true,则会导致使用更紧凑的 编码。不使用此选项的唯一原因是,如果您需要与 2.3.0 之前的版本的解析器兼容。当这些旧解析器在预期之外遇到打包数据时会忽略它。因此,不可能将现有字段更改为打包格式而不会破坏线兼容性。在 2.3.0 及更高版本中,此更改是安全的,因为可打包字段的解析器将始终接受这两种格式,但在必须处理使用旧 protobuf 版本的旧程序时要小心。

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

    optional int32 old_field = 6 [deprecated=true];
    

枚举值选项

支持枚举值选项。您可以使用 deprecated 选项指示不再应使用某个值。您还可以使用扩展创建自定义选项。

以下示例显示了添加这些选项的语法

import "google/protobuf/descriptor.proto";

extend google.protobuf.EnumValueOptions {
  optional string string_name = 123456789;
}

enum Data {
  DATA_UNSPECIFIED = 0;
  DATA_SEARCH = 1 [deprecated = true];
  DATA_DISPLAY = 2 [
    (string_name) = "display_value"
  ];
}

读取 string_name 选项的 C++ 代码可能如下所示

const absl::string_view foo = proto2::GetEnumDescriptor<Data>()
    ->FindValueByName("DATA_DISPLAY")->options().GetExtension(string_name);

请参阅 自定义选项,了解如何将自定义选项应用于枚举值和字段。

自定义选项

协议缓冲区还允许您定义和使用自己的选项。请注意,这是一个高级功能,大多数人不需要。由于选项由 google/protobuf/descriptor.proto 中定义的消息(如 FileOptionsFieldOptions)定义,因此定义您自己的选项只是 扩展这些消息的问题。例如

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() 返回 MyMessageMessageOptions 协议消息。从中读取自定义选项就像读取任何其他 扩展 一样。

类似地,在 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 以指定在运行时不应保留选项(或选项内的字段)。这称为“源保留”。

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

可以像这样直接在选项上设置保留:

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 来覆盖它。

选项目标

字段有一个 targets 选项,该选项控制在用作选项时字段可能应用于的实体类型。例如,如果一个字段具有 targets = TARGET_TYPE_MESSAGE,则该字段不能在枚举(或任何其他非消息实体)上的自定义选项中设置。Protoc 会强制执行此操作,如果违反目标约束,则会引发错误。

乍一看,鉴于每个自定义选项都是特定实体的选项消息的扩展,这已经将选项限制为该实体,因此此功能似乎没有必要。但是,在将共享选项消息应用于多种实体类型并希望控制该消息中各个字段的使用时,选项目标很有用。例如:

message MyOptions {
  optional string file_only_option = 1 [targets = TARGET_TYPE_FILE];
  optional int32 message_and_enum_option = 2 [targets = TARGET_TYPE_MESSAGE,
                                              targets = TARGET_TYPE_ENUM];
}

extend google.protobuf.FileOptions {
  optional MyOptions file_options = 50000;
}

extend google.protobuf.MessageOptions {
  optional MyOptions message_options = 50000;
}

extend google.protobuf.EnumOptions {
  optional MyOptions enum_options = 50000;
}

// OK: this field is allowed on file options
option (file_options).file_only_option = "abc";

message MyMessage {
  // OK: this field is allowed on both message and enum options
  option (message_options).message_and_enum_option = 42;
}

enum MyEnum {
  MY_ENUM_UNSPECIFIED = 0;
  // Error: file_only_option cannot be set on an enum.
  option (enum_options).file_only_option = "xyz";
}

生成您的类

要生成处理 .proto 文件中定义的消息类型所需 Java、Kotlin、Python、C++、Go、Ruby、Objective-C 或 C# 代码,需要在 .proto 文件上运行协议缓冲区编译器 protoc。如果您尚未安装编译器,请 下载软件包 并按照 README 中的说明操作。对于 Go,您还需要安装编译器的特殊代码生成器插件;您可以在 GitHub 上的 golang/protobuf 存储库中找到此插件和安装说明。

协议编译器的调用方式如下:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
  • IMPORT_PATH 指定一个目录,在解析 import 指令时在其中查找 .proto 文件。如果省略,则使用当前目录。可以通过多次传递 --proto_path 选项来指定多个导入目录;将按顺序搜索它们。-I=_IMPORT_PATH_ 可以用作 --proto_path 的简写形式。

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

    为了方便起见,如果 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 上下文中使用时,例如用于测试。

支持的平台

有关