注意:此网站已被弃用。该网站将在 2023 年 1 月 31 日后关闭,而流量将重定向到位于 https://protobuf.dev 的新网站。在此期间,我们仅会针对 protobuf.dev 进行更新。

语言指南

使用集合让一切井井有条 根据您的偏好保存内容并对其进行分类。

本指南介绍了如何使用协议缓冲区语言(包括 .proto 文件语法)构建协议缓冲区数据,以及如何从 .proto 文件生成数据访问类。它涵盖协议缓冲区语言的 proto2 版本:如需了解 proto3 语法,请参阅 Proto3 语言指南

这是一个参考指南,对于使用本文档中介绍的许多功能的分步示例,请参阅所选语言的教程

定义消息类型

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


message SearchRequest {
  required string query = 1;
  optional int32 page_number = 2;
  optional int32 result_per_page = 3;
}

SearchRequest 消息定义指定三个字段(名称/值对),每个字段对应于要包含在此类消息中的每段数据。每个字段都有一个名称和类型。

指定字段类型

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

分配字段编号

如您所见,消息定义中的每个字段都有一个唯一编号。这些数字用于标识消息二进制格式中的字段,且不应在您的消息类型被使用后更改。范围 1 到 15 中的字段编号使用一个字节进行编码,包括字段编号和字段类型(如需了解详情,请参阅协议缓冲区编码)。16 至 2047 范围内的字段编号占用两个字节。因此,您应为非常频繁的消息元素保留字段编号 1 到 15。请务必为将来可能添加的常见元素预留一些空间。

您可以指定的最小字段编号为 1,最大值为 229 - 1,即 536870911。您也无法使用 19000 到 19999 之间的数字(FieldDescriptor::kFirstReservedNumberFieldDescriptor::kLastReservedNumber),因为它们专用于协议缓冲区实现。如果您在 .proto 中使用其中一个预留数字,协议缓冲区编译器会抱怨。同样,您也不能使用以前预留的任何字段编号。

指定字段规则

您可以指定消息字段是以下消息之一:

  • required:格式正确的消息必须包含此字段中的一个。
  • optional:格式正确的消息可以有零个或一个字段(但不能超过一个)。
  • repeated:在格式正确的消息中,此字段可以重复任意次数(包括零)。重复值的顺序将保留。

由于历史原因,标量数字类型的 repeated 字段(例如 int32int64enum)编码的效率不如预期。新代码应使用特殊选项 [packed = true] 来提高编码效率。例如:

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

如需详细了解 packed 编码,请参阅协议缓冲区编码

必需是永久。您应非常谨慎地将字段标记为 required。如果您希望在某个位置停止写入或发送必填字段,将该字段更改为可选字段会出现问题 - 旧读者会认为不包含此字段的消息不完整,并且可能会在无意中拒绝或删除它们。您应考虑改为为缓冲区编写应用特定的自定义验证例程。

当有人向枚举添加值时,系统会显示必填字段的第二个问题。在这种情况下,无法识别的枚举值会被视为缺少该值,这也会导致所需的值检查失败。

添加更多消息类型

可以在单个 .proto 文件中定义多个消息类型。如果您要定义多条相关消息,这会非常有用。例如,如果您要定义与 SearchResponse 消息类型对应的回复消息格式,您可将其添加到同一 .proto 中:


message SearchRequest {
  required string query = 1;
  optional int32 page_number = 2;
  optional int32 result_per_page = 3;
}

message SearchResponse {
 ...
}

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

添加评论

如需为 .proto 文件添加注释,请使用 C/C++ 样式的 ///* ... */ 语法。


/* SearchRequest represents a search query, with pagination options to
 * indicate which results to include in the response. */

message SearchRequest {
  required string query = 1;
  optional int32 page_number = 2;  // Which page number do we want?
  optional int32 result_per_page = 3;  // Number of results to return per page.
}

保留字段

如果您通过完全移除或注释掉某个字段来更新消息类型,则将来的用户可以自行更新该字段编号以对该类型进行更新。如果用户日后加载同一 .proto 的旧版本(包括数据损坏、隐私 bug 等),这可能会导致严重问题。确保不会发生这种情况的一种方法是,指定已删除字段的字段编号(和/或名称,也可能导致 JSON 序列化问题)为 reserved。如果任何未来用户尝试使用这些字段标识符,协议缓冲区编译器就会抱怨。

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}

保留的字段编号范围包含边界值(9 to 119, 10, 11 相同)。请注意,您不能在同一个 reserved 语句中混用字段名称和字段编号。

根据您的 .proto 生成的内容

当您在 .proto 上运行协议缓冲区编译器时,编译器会以您选择的语言生成代码,因此您需要处理文件中描述的消息类型,包括获取和设置字段值、将消息序列化到输出流以及解析来自输入流的消息。

  • 对于 C++,编译器会从每个 .proto 生成一个 .h.cc 文件,并针对文件中所述的每种消息类型提供一个类。
  • 对于 Java,编译器会生成 .java 文件,其中包含每种消息类型的类,以及用于创建消息类实例的特殊 Builder 类。
  • Python 稍有不同 - Python 编译器会在 .proto 中为每个模块类型生成一个静态描述符,然后该模块将与 metaclass 搭配使用,在运行时创建必要的 Python 数据访问类。
  • 对于 Go,编译器会生成 .pb.go 文件,其中包含文件中每种消息类型的类型。

如需详细了解如何为每种语言使用 API,请按照所选语言的教程进行操作。如需了解更多 API 详情,请参阅相关 API 参考文档

标量值类型

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

.proto 类型 备注 C++ 类型 Java 类型 Python 类型[2] Go 类型
双精度 double 双精度 float *浮点数 64
float float float float *浮点数 32
int32 使用可变长度的编码。对负数的编码效率低下 - 如果您的字段可能包含负值,请改用 sint32。 int32 int int *整数 32
int64 使用可变长度的编码。对负数的编码效率低下 - 如果字段可能有负值,请改用 sint64。 int64 long 整数/长整型[3] *int64
30 秒 使用可变长度的编码。 30 秒 整数 [1] 整数/长整型[3] *uint32
uint64 使用可变长度的编码。 uint64 [1] 整数/长整型[3] *uint64
Sint32 使用可变长度的编码。有符号整数值。与常规 int32 相比,这些函数可以更高效地对负数进行编码。 int32 int int *整数 32
Sint64 使用可变长度的编码。有符号整数值。与常规 int64 相比,这些函数可以更高效地对负数进行编码。 int64 long 整数/长整型[3] *int64
固定 32 始终为 4 个字节。如果值通常大于 228,则比 uint32 更高效。 30 秒 整数 [1] 整数/长整型[3] *uint32
固定 64 始终为 8 个字节。如果值通常大于 256,则比 uint64 更高效。 uint64 [1] 整数/长整型[3] *uint64
固定 32 始终为 4 个字节。 int32 int int *整数 32
固定 64 始终为 8 个字节。 int64 long 整数/长整型[3] *int64
bool bool 布尔值 bool *布尔值
字符串 字符串必须始终包含 UTF-8 编码文本。 字符串 字符串 unicode (Python 2) 或 str (Python 3) *字符串
字节 可以包含任意字节序列。 字符串 ByteString 字节 []字节

协议缓冲区编码中对序列化消息时,您可以详细了解这些类型的编码方式。

[1] 在 Java 中,无符号的 32 位和 64 位整数使用其对应的符号来表示,顶部位仅存储在符号位中。

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

[3] 64 位或无符号 32 位整数在解码时始终表示为长整数,但如果在设置字段时指定了整数,则可能是整数。在所有情况下,该值都必须符合所设置的类型。请参阅 [2]。

可选字段和默认值

如上所述,消息说明中的元素可以带有 optional 标签。格式正确的消息可能包含可选元素,也可能不包含。解析消息后,如果消息不包含可选元素,则访问已解析对象中的相应字段会返回该字段的默认值。默认值可以在消息说明中指定。例如,假设您希望为 SearchRequestresult_per_page 值提供默认值 10。

optional int32 result_per_page = 3 [default = 10];

如果没有为可选元素指定默认值,则使用类型专用默认值:对于字符串,默认值为空字符串。对于字节,默认值为空字节字符串。对于布尔值,默认值为 false。对于数值类型,默认值为零。对于枚举,默认值是枚举类型定义中列出的第一个值。这意味着,在向枚举值列表开头添加值时必须小心谨慎。如需了解如何安全地更改定义,请参阅更新消息类型部分。

枚举

在定义消息类型时,您可能希望它的某个字段仅具有一个预定义的值列表。例如,假设您想要为每个 SearchRequest 添加一个 corpus 字段,其中正文可以是 UNIVERSALWEBIMAGESLOCALNEWSPRODUCTSVIDEO。您只需在消息定义中添加 enum 即可实现此目的 - 具有 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 {
  required string query = 1;
  optional int32 page_number = 2;
  optional int32 result_per_page = 3 [default = 10];
  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 compile error inside Google and a warning message outside.
  ENAA_FINISHED = 2;
}

枚举器常量必须在 32 位整数范围内。由于 enum 值使用线上的变体编码,因此负值效率低下,因此不推荐使用。您可以在消息定义内外定义 enum - 这些 enum 可以在 .proto 文件中的任何消息定义中重复使用。您还可以使用语法 _MessageType_._EnumType_ 将一条消息中声明的 enum 类型用作其他消息中的字段类型。

在使用 enum.proto 上运行协议缓冲区编译器时,生成的代码将具有用于 Java 或 C++ 的相应 enum,或者具有适用于 Python 的特殊 EnumDescriptor 类,该类用于在运行时生成的类中创建一组具有整数值的符号常量。

如需详细了解如何在应用中使用消息 enum,请参阅所选代码指南(针对您选择的语言)。

预留值

如果您通过完全移除某个枚举条目或将其注释掉以更新某个枚举类型,那么将来的用户在对该类型进行更新时可以重复使用相应的数值。如果用户日后加载同一 .proto 的旧版本(包括数据损坏、隐私 bug 等),这可能会导致严重问题。确保不会发生这种情况的一种方法是,指定已删除条目的数值(和/或名称,也可能导致 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 result = 1;
}

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

导入定义

在上面的示例中,Result 消息类型与 SearchResponse 在同一文件中定义 - 如果要用作字段类型的消息类型已经在其他 .proto 文件中定义,该怎么办?

您可以通过导入其他 .proto 文件中的定义来使用这些文件。如需导入另一个 .proto 的定义,您可以在文件顶部添加 import 语句:

import "myproject/other_protos.proto";

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

请注意,公开导入功能在 Java 中不可用。

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

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

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

使用 proto3 消息类型

您可以导入 proto3 消息类型,并在 proto2 消息中使用它们,反之亦然。不过,proto2 语法中不能使用 proto2 枚举。

嵌套类型

您可以在其他消息类型中定义和使用消息类型,如以下示例所示:Result 消息在 SearchResponse 消息中定义:

message SearchResponse {
  message Result {
    required string url = 1;
    optional string title = 2;
    repeated string snippets = 3;
  }
  repeated Result result = 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 string name = 1;
      optional bool   flag = 2;
    }
  }
}

群组

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

群组是在邮件定义中嵌套信息的另一种方式。例如,另一种指定包含大量 ResultSearchResponse 的方法如下:

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

群组只是将嵌套消息类型和字段合并到单个声明中。在代码中,您可以像处理包含名为 resultResult 类型字段那样处理此消息(后者名称将转换为小写形式,以免与前一种名称发生冲突)。因此,此示例的消息与上面的 SearchResponse 完全相同,只是消息具有不同的传输格式

更新消息类型

如果现有消息类型不再满足您的所有需求(例如,您希望消息格式有一个额外的字段),但您仍想使用以旧格式创建的代码,不用担心!在使用二进制传输格式时,在不破坏任何现有代码的情况下更新消息类型非常简单。

如果您使用的是二进制传输格式,请检查以下规则:

  • 请勿更改任何现有字段的字段编号。
  • 您添加的所有新字段都应为 optionalrepeated。这意味着,使用“旧”消息格式将代码序列化的任何消息都可以由新生成的代码解析,因为它们不会缺少任何 required 元素。您应该为这些元素设置合理的默认值,以便新代码可以与旧代码生成的消息正确交互。同样,新代码创建的消息也可以用旧代码解析:旧二进制文件在解析时会直接忽略新字段。不过,未知字段不会被舍弃,如果消息已序列化,则未知字段也会随之序列化 - 因此,如果消息传递给新代码,则新字段仍然可用。
  • 您可以移除非必填字段,只要未在更新后的消息类型中再次使用该字段编号即可。建议您重命名该字段,添加前缀“OBSOLETE_”,或将该字段编号预留,以免 .proto 的将来用户不小心重复使用该编号。
  • 非必填字段可以转换为扩展项,反之亦然,前提是类型和编号保持不变。
  • int32uint32int64uint64bool 都兼容,这意味着您可以将字段从一种类型更改为另一种类型,而不会破坏向前或向后兼容性。如果从不适合相应类型的有线电缆解析数字,则其效果与在 C++ 中将该数字转换为类型时相同(例如,如果将 64 位数字读取为 int32 值,该数字将被截断为 32 位)。
  • sint32sint64 彼此兼容,但与其他整数类型不兼容。
  • 只要字节是有效的 UTF-8,stringbytes 就兼容。
  • 如果字节包含编码版本的消息,则嵌入式消息与 bytes 兼容。
  • fixed32 兼容 sfixed32fixed64sfixed64 兼容。
  • 对于 stringbytes 和消息字段,optionalrepeated 兼容。给定重复字段的序列化数据作为输入,如果该字段是基元类型字段,则预期该字段为 optional 将获取最后一个输入值;如果该字段是消息类型字段,则它会合并所有输入元素。请注意,这对于数字类型(包括布尔值和枚举值)通常安全。数字类型的重复字段可以采用 packed 格式进行序列化,以便在预计 optional 字段存在时无法正确解析。
  • 更改默认值通常没有问题,但请记住,绝不能通过网络发送默认值。因此,如果某个程序收到一条未设置特定字段的消息,该程序会看到该程序的协议版本中定义的默认值。 但不会看到发件人代码中定义的默认值。
  • 在传输格式方面,enumint32uint32int64uint64 兼容(请注意,如果值不适合,它们会被截断),但请注意,在反序列化消息时,客户端代码可能会以不同的方式处理它们。值得注意的是,在对消息进行反序列化时,系统会舍弃无法识别的 enum 值,这会导致字段的 has.. 访问器返回 false,且其 getter 返回 enum 定义中列出的第一个值,如果指定了值,则返回默认值。对于重复的枚举字段,系统会从列表中删除任何无法识别的值。不过,整数字段将始终保留其值。因此,在将整数升级到 enum 时,您需要非常小心地接收线上超出范围的枚举值。
  • 在当前 Java 和 C++ 实现中,当去掉无法识别的 enum 值时,这些值会与其他未知字段一起存储。请注意,如果此数据被识别这些值的客户端序列化然后解析,则可能会导致异常行为。对于可选字段,即使在原始消息反序列化后写入了新值,识别该值的客户端仍会读取旧值。对于重复字段,旧值将显示在任何已识别和新添加的值之后,这意味着顺序将不会保留。
  • 将单个 optional 字段或扩展更改为 oneof 的成员与二进制文件兼容,但对于某些语言(特别是 Go),生成的代码的 API 将以不兼容的方式更改。因此,如 AIP-180 中所述,Google 不会在其公共 API 中做出此类更改。同样,在确定源代码兼容性时,如果您确定一次一次不设置多个代码,将多个字段移至新的 oneof 可能是安全的。将字段移到现有的 oneof 是不安全的。同样,将单个字段 oneof 更改为 optional 字段或扩展也是安全的。
  • 更改 map<K, V> 与对应的 repeated 消息字段之间的字段与二进制文件兼容(如需了解消息布局和其他限制,请参阅下文的映射部分)。但是,更改的安全性取决于应用:对消息进行反序列化和反序列化时,使用 repeated 字段定义的客户端将生成语义相同的结果;但是,使用 map 字段定义的客户端可以对具有重复键的条目进行排序和删除条目。

扩展程序

通过扩展程序,您可以声明消息中的一系列字段编号适用于第三方扩展程序。扩展程序是原始 .proto 文件未定义类型的占位符。这样,其他 .proto 文件便可以定义包含这些字段编号的部分或所有字段的类型,从而将其添加到您的消息定义中。让我们看一个示例:

message Foo {
  // ...
  extensions 100 to 199;
}

这表示 Foo 中的字段编号范围 [100, 199] 是为扩展预留的。其他用户现在可以使用自己指定范围内的字段编号在他们自己的 .proto 文件中导入 Foo,从而将新字段添加到 Foo 中,例如:

extend Foo {
  optional int32 bar = 126;
}

这将名为 bar 且字段编号为 126 的字段添加到 Foo 的原始定义中。

当用户的 Foo 消息经过编码时,传输格式与用户在 Foo 内定义新字段完全相同。不过,在应用代码中访问扩展字段的方式与访问常规字段的方式略有不同,因为生成的数据访问代码具有用于处理扩展程序的特殊访问器。例如,以下是在 C++ 中设置 bar 值的方法:

Foo foo;
foo.SetExtension(bar, 15);

同样,Foo 类定义了模板化访问器 HasExtension()ClearExtension()GetExtension()MutableExtension()AddExtension()。它们的语义都与针对普通字段生成的对应访问器相匹配。如需详细了解如何使用扩展程序,请参阅针对所选语言生成的代码参考。

请注意,扩展程序可以是任何字段类型(包括消息类型),但不能是单类型或映射。

嵌套扩展程序

您可以在其他类型的范围内声明扩展:

message Baz {
  extend Foo {
    optional int32 bar = 126;
  }
  ...
}

在这种情况下,用于访问此扩展程序的 C++ 代码为:

Foo foo;
foo.SetExtension(Baz::bar, 15);

换言之,唯一影响是 barBaz 的范围内定义。

这是导致混淆的常见原因:声明嵌套在消息类型中的 extend 代码块并不意味着外部类型与扩展类型之间有任何关系。特别是,以上示例并不意味着 BazFoo 的任何类型的子类。这表示,bar 符号在 Baz 的范围内声明;它只是一个静态成员。

一种常见模式是在扩展字段类型的范围内定义扩展 - 例如,以下是 Foo 类型的扩展,类型为 Baz,其中扩展被定义为 Baz 的一部分:

message Baz {
  extend Foo {
    optional Baz foo_ext = 127;
  }
  ...
}

不过,并不要求在消息类型中定义扩展。您还可以执行以下操作:

message Baz {
  ...
}

// This can even be in a different file.
extend Foo {
  optional Baz foo_baz_ext = 127;
}

事实上,为避免混淆,最好使用这种语法。如上所述,对于尚不熟悉扩展的用户,嵌套语法通常会被误认为是子类化。

选择分机号

请务必确保两个用户不会使用相同的字段编号向同一消息类型添加扩展程序,因为如果扩展程序被意外解读为错误类型,可能会导致数据损坏。为解决此问题,您可能需要考虑为项目定义扩展程序编号规范。

如果您的编号规范可能涉及到字段编号非常大的扩展程序,您可以使用 max 关键字指定您的扩展范围,使其涵盖可能的最大字段编号:

message Foo {
  extensions 1000 to max;
}

max 是 229 - 1,即 536870911。

通常,在选择字段编号时,您的编号规范也需要避免使用字段编号 19000 到 19999(FieldDescriptor::kFirstReservedNumberFieldDescriptor::kLastReservedNumber),因为它们专用于协议缓冲区实现。您可以定义包含此范围的扩展范围,但协议编译器不允许您使用这些数字定义实际的扩展。

单曲

如果您的消息包含许多可选字段,并且最多只能同时设置一个字段,则可以使用 oneof 功能强制执行此行为并节省内存。

其中一个字段类似于可选字段,但一个共享内存中的所有字段除外,并且最多只能同时设置一个字段。设置任意一个成员后,系统会自动清除所有其他成员。您可以使用特殊的 case()WhichOneof() 方法查看其中一个中设置的值(如果有),具体取决于您选择的语言。

使用 Oneof

如需在 .proto 中定义单一项,您可以使用 oneof 关键字,后跟您的名称,在本例中为 test_oneof

message SampleMessage {
  oneof test_oneof {
     string name = 4;
     SubMessage sub_message = 9;
  }
}

然后,将您的 oneof 字段添加到 oneof 定义中。您可以添加任何类型的字段,但不能使用 requiredoptionalrepeated 关键字。如果您需要向其中一个字段添加重复字段,可以使用包含重复字段的消息。

在生成的代码中,其中一个字段的 getter 和 setter 与常规 optional 方法相同。此外,您还将获得一种特殊方法,用于检查在任一项中设置的值(如有)。您可以在所选 API 参考文档中详细了解适用于所选语言的 oneof API。

其中一项功能

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

    SampleMessage message;
    message.set_name("name");
    CHECK(message.has_name());
    message.mutable_sub_message();   // Will clear name field.
    CHECK(!message.has_name());
    
  • 如果解析器在连接线上遇到同一成员的多个成员,则解析后的消息只会使用最后一位成员。

  • 其中一个不支持扩展。

  • 其中一个不能为 repeated

  • 反射 API 适用于其中一个字段。

  • 如果您将 oneof 字段设置为默认值(例如,将 int32 oneof 字段设置为 0),系统将设置该字段的“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() 处理两条消息,每个消息都采用同一种情况,那么下例就是 msg1 的值为 sub_messagemsg2 的值为 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());
    

向后兼容性问题

添加或移除某个字段时请务必小心。如果检查 oneone 的值返回 None/NOT_SET,则可能意味着未设置 one of the one 或它已被设置为其他版本之一的字段。无法区分这两者,因为无法获知线上的未知字段是否是某个字段的成员。

代码重用问题

  • 将可选字段移入或移出其中一个字段:在对消息进行序列化和解析后,您可能会丢失部分信息(部分字段将被清除)。不过,您可以放心地将单个字段移动到其中一个,如果知道只有一个字段被设置,可以移动多个字段。有关详情,请参阅更新消息类型
  • 删除 oneof 字段并重新添加:这可能会在序列化和解析消息后清除您当前设置的 oneof 字段。
  • 拆分或合并其中一个:这与移动常规 optional 字段具有类似问题。

地图

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

map<key_type, value_type> map_field = N;

...其中 key_type 可以是任何整数或字符串类型(因此,除浮点类型和 bytes 以外的任何标量类型)。请注意,枚举不是有效的 key_typevalue_type 可以是除其他映射外的任何类型。

例如,如果您要创建项目映射,其中每条 Project 消息都与一个字符串键关联,您可以进行如下定义:

map<string, Project> projects = 3;

生成的地图 API 目前可用于所有 proto2 支持的语言。如需详细了解您选择的语言的地图 API,请参阅相关的 API 参考文档

地图功能

  • 地图不支持扩展。
  • 地图不能为 repeatedoptionalrequired
  • 映射值的线格式格式和映射迭代顺序尚未定义,因此您不能指代地图项的顺序是特定的。
  • .proto 生成文本格式时,映射按键排序。数字键按数字顺序排序。
  • 通过有线连接或合并时,如果存在重复的映射键,则使用最后一个看到的键。从文本格式解析映射时,如果存在重复的键,解析可能会失败。

向后兼容性

此映射语法等效于线上传输的内容,因此不支持映射的协议缓冲区实现仍然可以处理您的数据:

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 {
  ...
  required foo.bar.Open open = 1;
  ...
}

软件包说明符对生成的代码的影响取决于您选择的语言:

  • C++ 中,生成的类会封装在 C++ 命名空间内。例如,Open 将位于命名空间 foo::bar 中。
  • Java 中,除非您在 .proto 文件中明确提供 option java_package,否则该软件包将用作 Java 软件包。
  • Python 中,package 指令会被忽略,因为 Python 模块是根据它们在文件系统中的位置组织的。
  • Go 中,package 指令会被忽略,生成的 .pb.go 文件位于以相应 go_proto_library 规则命名的软件包中。

请注意,即使 package 指令不会直接影响生成的代码(例如在 Python 中),仍强烈建议为 .proto 文件指定软件包,否则可能会导致描述符命名冲突,使 proto 无法移植到其他语言。

软件包和名称解析

协议缓冲区语言中的类型名称解析的工作原理类似于 C++:首先搜索最内作用域,然后搜索下一个最内作用域,以此类推,其中每个软件包都被认为是其父级软件包的“内部”。前导“.”(例如,.foo.bar.Baz)表示从最外层的范围开始。

协议缓冲区编译器通过解析导入的 .proto 文件来解析所有类型名称。每种语言的代码生成器都知道如何引用该语言中的每种类型,即使它具有不同的作用域规则也是如此。

定义服务

如果要将消息类型与 RPC(远程过程调用)系统配合使用,您可以在 .proto 文件中定义 RPC 服务接口,协议缓冲区编译器会生成以您选择的语言编写的服务接口代码和存根。例如,如果您想使用接受 SearchRequest 并返回 SearchResponse 的方法定义 RPC 服务,可以在 .proto 文件中定义它,如下所示:

service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}

默认情况下,协议编译器会生成一个名为 SearchService 的抽象接口和相应的“桩”实现。桩会将所有调用转发到 RpcChannel,而该接口则是必须根据您自己的 RPC 系统自行定义的抽象接口。例如,您可以实现 RpcChannel 来序列化消息并通过 HTTP 将其发送到服务器。换句话说,生成的存根提供了一个类型安全的接口,用于进行基于协议缓冲区的 RPC 调用,而不会受限于任何特定的 RPC 实现。因此,在 C++ 中,您最终可能会获得如下代码:

using google::protobuf;

protobuf::RpcChannel* channel;
protobuf::RpcController* controller;
SearchService* service;
SearchRequest request;
SearchResponse response;

void DoSearch() {
  // You provide classes MyRpcChannel and MyRpcController, which implement
  // the abstract interfaces protobuf::RpcChannel and protobuf::RpcController.
  channel = new MyRpcChannel("somehost.example.com:1234");
  controller = new MyRpcController;

  // The protocol compiler generates the SearchService class based on the
  // definition given above.
  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 实现。如需查看我们已知项目的链接列表,请参阅第三方插件 Wiki 页面

选项

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

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

下面是一些最常用的选项:

  • java_package(文件选项):要用于生成的 Java 类的软件包。如果 .proto 文件中未提供明确的 java_package 选项,则默认情况下将使用 proto 包(使用 .proto 文件中的“package”关键字指定)。但是,proto 软件包通常不构成良好的 Java 软件包,因为 proto 软件包不应以反向域名开头。如果不生成 Java 代码,则此选项无效。

    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 代码,则此选项无效。

    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++ 生成的代码启用区域分配

  • 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 都会生成警告。日后,特定于语言的代码生成器可能会在字段访问器上生成弃用注释,这反过来会导致在编译尝试使用该字段的代码时发出警告。如果该字段未被任何人使用,而您想阻止新用户使用该字段,请考虑将该字段声明替换为预留语句。

    optional int32 old_field = 6 [deprecated=true];
    

自定义选项

协议缓冲区甚至允许您定义和使用自己的选项。请注意,这是一项高级功能,大多数用户都不需要。由于选项由 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]

您可以使用协议缓冲区语言为每种类型的结构定义自定义选项。以下示例使用了各种选项:

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 的扩展。

生成类

如需生成使用 .proto 文件中定义的消息类型的 Java、Python 或 C++ 代码,您需要在 .proto 上运行协议缓冲区编译器 protoc。如果您尚未安装编译器,请下载软件包并按照自述文件中的说明操作。

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

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto
  • IMPORT_PATH 指定在解析 import 指令时要寻找 .proto 文件的目录。如果省略此参数,则系统会使用当前目录。 您可以通过多次传递 --proto_path 选项来指定多个导入目录;它们会按顺序搜索。-I=_IMPORT_PATH_ 可用作 --proto_path 的简短形式。
  • 您可以提供一个或多个输出指令

    为方便起见,如果 DST_DIR.zip.jar 结尾,编译器会将输出写入具有给定名称的单个 ZIP 格式的归档文件。根据 Java JAR 规范的要求,系统将为 .jar 输出提供一个清单文件。请注意,如果输出归档已存在,将被覆盖;编译器不够智能,无法将文件添加到现有归档。

  • 您必须提供一个或多个 .proto 文件作为输入。您可以同时指定多个 .proto 文件。虽然这些文件是相对于当前目录命名的,但每个文件都必须位于某个 IMPORT_PATH 中,以便编译器确定其规范名称。

文件位置

最好不要将 .proto 文件放在其他语言来源所在的目录中。考虑在项目的根软件包下为 .proto 文件创建子软件包 proto

地理位置应该与语言无关

使用 Java 代码时,将相关的 .proto 文件放在 Java 源代码所在的同一目录中会非常方便。不过,如果任何非 Java 代码使用相同的 proto,则路径前缀不再有意义。因此,通常应将 proto 放在与语言无关的目录中,例如 //myteam/mypackage

此规则的例外情况是明确了 proto 将仅用于 Java 上下文,例如用于测试。

支持的平台

相关信息如下: