凌云的博客

行胜于言

Protobuf 语言指南 (2023 修订版)[译]

分类:proto| 发布时间:2025-01-20 21:50:00

原文: Language Guide (editions)

介绍如何在项目中使用 Protocol Buffers 2023 修订版

本指南介绍如何使用 protocol buffer 语言来构造 protocol buffer 数据,包括 .proto 文件语法以及如何从 .proto 文件生成数据访问类。 它涵盖了 protocol buffer 的 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;
}
  • 文件的首行指定您正在使用 2023 版的 protobuf 语言规范。
    • edition(或 proto2/proto3 的 syntax)必须是文件中第一个非空、非注释的行。
    • 如果没有指定 edition 或 syntax,协议缓冲区编译器将默认您使用 proto2
  • SearchRequest 消息定义指定了三个字段(名称/值对),每个字段对应您想要包含在该类型消息中的一个数据片段。 每个字段都有一个名称和一个类型。

指定字段类型

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

分配字段编号

您必须为消息定义中的每个字段分配一个介于 1 和 536,870,911 之间的编号,并遵守以下限制:

  • 给定的编号在该消息的所有字段中 必须是唯一的
  • 编号 19,000 到 19,999 是为 Protocol Buffers 实现保留的。如果您在消息中使用这些保留字段号之一,Protocol Buffers 编译器将报错。
  • 您不能使用任何先前保留的编号或已分配给扩展的任何编号。

一旦你在消息中为某个字段分配了编号,则这个编号无法更改,因为它在 消息底层传输格式 中标识该字段。 “更改”编号号等同于删除该字段并创建一个具有相同类型但有不同编号的新字段。 有关如何正确执行此操作,请参阅 删除字段

字段编号绝不应重复使用。 切勿将字段编号从保留列表中取出,然后将其与新的字段定义一起重复使用。 请参阅 字段编号重复使用后果

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

字段编号重复使用后果

重复使用字段编号会使解码消息的底层传输格式是变得有歧义。

protobuf 底层传输格式简洁高效,无法检测到使用一种定义编码而使用另一种定义解码的字段。

使用一种格式编码但是使用另一种格式进行解码字段会导致:

  • 开发者浪费时间进行调试
  • 解析或者合并消息出错(最后的情况)
  • PII/SPII 泄露
  • 数据损坏

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

  • 重新编号字段(有时为了实现更美观的字段编号顺序而进行)。 重新对字段进行编号实际上会删除并重新添加所有涉及的字段的编号,从而导致不兼容的传输格式更改。
  • 删除一个字段而不保留该编号以防止将来重复使用。
    • 由于多种原因,对于扩展字段来说,这是一个很容易犯的错误。扩展声明提供了一种保留扩展字段的机制。

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

指定字段基数

消息字段可以是以下之一:

  • 单数:
    单数字段没有明确的基数标签。它有两种可能的状态:
    • 字段已设置,并包含显式设置或从传输格式解析的值。它将被序列化到传输格式中。
    • 字段未设置,将返回默认值。它不会被序列化到传输格式中。

      您可以检查该值是否已显式设置。
      已迁移到 editions 的 Proto3 隐式字段将使用 field_presence 特性设置为 IMPLICIT 值。
      已迁移到 editions 的 Proto2 required 字段也将使用 field_presence 特性,但设置为 LEGACY_REQUIRED
  • repeated: 这种字段类型可以在格式良好的消息中重复零次或多次。将保留其顺序。
  • map: 这是一个成对的键/值字段类型。有关此字段类型的更多信息,请参阅 映射
repeacted 字段默认采用 Packed 编码

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

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

格式良好的消息

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

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

添加更多消息类型

可以在单个 .proto 文件中定义多个消息类型。 如果您要定义多个相关消息,这很有用——例如,如果您想定义与 SearchRequest 消息类型对应的回复消息格式,可以将其添加到同一个 .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 to 11 与 9, 10, 11 相同)。

保留字段名称

稍后重用旧字段名称通常是安全的,但使用 TextProto 或 JSON 编码时除外,因为字段名称会被序列化。 为了避免此风险,您可以将已删除的字段名称添加到 reserved 列表中。

保留名称仅影响 protoc 编译器的行为,而不会影响运行时行为,但有一个例外:TextProto 实现可能会在解析时丢弃具有保留名称的未知字段(而不会像其他未知字段那样引发错误)(目前只有 C++ 和 Go 实现这样做)。 运行时 JSON 解析不受保留名称的影响。

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

注意,不能在同一 reserved 语句中混合字段名和字段号。

从您的 .proto 文件中生成了什么?

当您在 .proto 文件上运行 protocol buffer 编译器时,编译器会生成您选择的语言中的代码,您将需要使用在文件中描述的消息类型,包括获取和设置字段值、将消息序列化为输出流以及从输入流中解析消息。

  • C++: 编译器从每个 .proto 文件生成一个 .h 和 .cc 文件,其中包含文件中描述的每种消息类型的类。
  • Java: 编译器生成一个 .java 文件,其中包含每种消息类型的类,以及一个用于创建消息类实例的特殊 Builder 类。
  • Kotlin: 除了生成的 Java 代码之外,编译器还为每种消息类型生成一个 .kt 文件,其中包含改进的 Kotlin API。这包括简化消息实例创建的 DSL、可为空的字段访问器和复制函数。
  • Python: Python 编译器生成一个模块,其中包含 .proto 文件中每种消息类型的静态描述符,然后使用元类在运行时创建必要的 Python 数据访问类。
  • Go: 编译器生成一个 .pb.go 文件,其中包含文件中每种消息类型的类型。
  • Ruby: 编译器生成一个 .rb 文件,其中包含一个 Ruby 模块,该模块包含您的消息类型。
  • Objective-C: 编译器从每个 .proto 文件生成一个 pbobjc.h 和 pbobjc.m 文件,其中包含文件中描述的每种消息类型的类。
  • C#: 编译器从每个 .proto 文件生成一个 .cs 文件,其中包含文件中描述的每种消息类型的类。
  • PHP: 编译器为文件中描述的每种消息类型生成一个 .php 消息文件,以及为编译的每个 .proto 文件生成一个 .php 元数据文件。元数据文件用于将有效的消息类型加载到描述符池中。
  • Dart: 编译器生成一个 .pb.dart 文件,其中包含文件中每种消息类型的类。

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

标量值类型

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

Proto 类型 说明
double
float
int32 使用可变长度编码。此编码对负数效率低下,因此如果您的字段可能具有负值,请使用 sint32 代替。
int64 使用可变长度编码。此编码对负数效率低下,因此如果您的字段可能具有负值,请使用 sint64 代替。
uint32 使用可变长度编码。
uint64 使用可变长度编码。
sint32 使用可变长度编码。有符号整数。对于负数来说,本类型编码效率比 int32 更高。
sint64 使用可变长度编码。有符号整数。对于负数来说,本类型编码效率比 int64 更高。
fixed32 总是四个字节。如果你的值经常大于 228,本类型编码比 uint32 效率更高。
fixed64 总是八个字节。如果你的值经常大于 256,本类型编码比 uint64 效率更高。
sfixed32 总是四个字节。
sfixed64 总是八个字节。
bool
string 字符串必须是 UTF-8 编码或者 7-bit 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 or Bignum(as required) int integer int i32
int64 int64_t long int/long[4] int64 Bignum long interger/string[6] Int64 i64
uint32 uint32_t int[2] int/long[4] uint32 Fixnum or Bignum(as required) int integer int u32
uint64 uint64_t long[2] int/long[4] uint64 Bignum ulong interger/string[6] Int64 u64
sint32 int32_t int int int32 Fixnum or Bignum(as required) 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 or Bignum(as required) 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 or Bignum(as required) 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 位机器上使用整数,在 32 位机器上使用字符串。

您可以在 Protocol Buffer Encoding 中了解更多关于序列化消息时如何编码这些类型的信息。

默认字段值

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

  • string: 默认值为空字符串。
  • bytes: 默认值为空字节。
  • bool: 默认值为 false。
  • 数值类型: 默认值为零。
  • 消息字段: 该字段未设置。其确切值取决于语言。有关详细信息,请参阅 generated code guide
  • 枚举: 默认值为第一个定义的枚举值,该值必须为 0。请参阅 枚举默认值

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

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

覆盖默认标量值

在 protobuf 版本中,您可以为单一的非消息字段指定显式默认值。 例如,假设您想为 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” 返回发送方的值。

有关默认值在生成代码中的工作方式的更多详细信息,请参阅您选择的语言的 generated code guide

无法为 field_presence 特性设置为 IMPLICIT 的字段指定显式默认值。

枚举

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

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_UNSPECIFIEDENUM_TYPE_NAME_UNKNOWN。这是因为:

  • 零值必须是第一个元素,以与 proto2 语义兼容,其中第一个枚举值是默认值,除非显式指定了其他值。
  • 必须有零值才能与 proto3 语义兼容,其中零值用作使用此枚举类型的所有隐式存在字段的默认值。

还建议此第一个默认值没有其他语义含义,只是“此值未指定”。

SearchRequest.corpus 字段等枚举字段的默认值可以像这样显式覆盖:

Corpus corpus = 4 [default = CORPUS_UNIVERSAL];

如果枚举类型是从 proto2 迁移的,并且使用 option features.enum_type = CLOSED; ,则对枚举中的第一个值没有限制。 不建议更改此类枚举的第一个值,因为它将更改使用该枚举类型的任何字段的默认值,而没有显式字段默认值。

枚举值别名

您可以通过将相同的值分配给不同的枚举常量来定义别名。 为此,您需要将 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 位整数的范围内。 由于枚举值在传输格式上使用 varint 编码,负值效率低下,因此不推荐使用。 您可以在消息定义内定义枚举,如前面的示例所示,或者在外部定义 - 这些枚举可以在您的 .proto 文件中的任何消息定义中重复使用。 您还可以使用在一条消息中声明的枚举类型作为另一条消息中字段的类型,使用语法 MessageType.EnumType

特定于语言的枚举实现

当您在使用枚举的 .proto 文件上运行 protocol buffer 编译器时,将为 Java、Kotlin 或 C++ 具有相应的枚举代码,或者为 Python 生成一个特殊的 EnumDescriptor 类,用于在运行时生成的类中创建一组具有整数值的符号常量。

重要
生成的代码可能受到特定于语言的枚举器数量限制(一种语言可能只有几千个)。 查看您计划使用的语言的限制。

在反序列化期间,将保留未识别的枚举值,尽管当消息被反序列化时,其表示方式取决于语言。 在支持具有超出指定符号范围的值的开放式枚举类型的语言(例如 C++ 和 Go)中,未知的枚举值只是简单地存储为其底层的整数表示。 在具有封闭式枚举类型的语言(例如 Java)中,枚举中的一个情况用于表示未识别的值,并且可以使用特殊访问器来访问底层整数。 在任何一种情况下,如果序列化消息,未识别的值仍将与消息一起序列化。

重要
有关枚举应该如何工作以及它们在不同语言中的工作方式的信息,请参阅 generated code guide

保留值

如果您通过完全删除枚举项或将其注释掉来 更新 枚举类型,则未来的用户可以在对类型进行自己的更新时重用数值。 如果他们后来加载相同 .proto 的旧实例,这可能会导致严重问题,包括数据损坏、隐私漏洞等。 确保这种情况不会发生的一种方法是指定已删除项的数值(和/或名称,这也会导致 JSON 序列化问题)是保留的。 如果任何未来的用户尝试使用这些标识符,协议缓冲区编译器将报错。 您可以使用 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

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

使用 proto2 和 proto3 消息类型

可以导入 proto2proto3 消息类型然后在 editions 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 text format 来存储 protobuf buffer 消息,你能在 proto 定义中进行的修改是不同的。

查看 Proto 最佳实践[译] 并遵循以下规则: Check Proto Best Practices and the following rules:

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

未知字段

未知字段是格式良好的 protocol buffer 序列化数据,表示解析器无法识别的字段。 例如,当旧版二进制文件解析由新版二进制文件发送的数据(该数据包含新字段)时,这些新字段会在旧版二进制文件中成为未知字段。

Editions 消息会保留未知字段,并在解析过程中以及序列化输出中包含它们,这与 proto2 和 proto3 的行为一致。

保留未知字段

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

  • 将原型序列化为 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 中添加相应的声明,并移除验证。

// 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 将用于一个repeated 的扩展字段,具有完全限定的名称 .kittens.kitten_videos 和完全限定的类型 .kittens.Video。 有关扩展声明的更多信息,请参见 扩展声明

请注意,容器消息的文件(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 为大多数原型实例中填充的标准字段。 并不推荐将这些编号用于扩展。

如果你的编号约定可能涉及扩展使用非常大的字段编号,你可以使用 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
  ];
}

容器 的下一个扩展应该添加一个编号为 525000002 的新声明。

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

你可以在另一个消息的作用域中声明扩展:

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_countpuppies.Photo 的作用域内定义。

这是一个常见的误区:在消息类型内部声明一个 extend 块并不意味着外部类型与扩展类型之间存在任何关系。 特别是,前面的例子并不意味着 PhotoUserProfile 的某种子类。 它的意思只是符号 likes_countPhoto 的作用域内声明;它只是一个静态成员。

一种常见的模式是在扩展字段类型的作用域内定义扩展——例如,下面是一个扩展类型为 puppies.Photomedia.UserContent 扩展,其中扩展作为 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 包含一个任意序列化的消息(作为字节),以及一个 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 字段和 repeated 字段。 如果你需要将一个 repeated 字段添加到 oneof 中,可以使用一个包含该 repeated 字段的消息。

在生成的代码中,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 中的普通字段一样处理该字段的内容:
      • 原始类型(primitive)会覆盖已经设置的任何值。
      • 消息类型(message)会合并到已经设置的值中。
  • 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++ 中,如果你 交换 两个包含 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: 这与移动单一字段类似,存在类似的问题。

映射

如果你想在数据定义中创建一个关联映射,protocol buffers 提供了一个方便的快捷语法:

map<key_type, value_type> map_field = N;

…其中,key_type 可以是任何整数类型或字符串类型(即,除了浮点类型和字节类型之外的任何标量类型)。 请注意,enum 和 proto 消息类型都不能作为 key_typevalue_type 可以是任何类型,除了另一个 map 类型。

例如,如果你想创建一个项目映射,其中每个 Project 消息与一个字符串键相关联,你可以这样定义:

map<string, Project> projects = 3;

映射功能

  • 扩展(Extensions)不支持用于 map。
  • map 字段不能是重复的(repeated)。
  • map 值的底层传输格式的排序和遍历排序是未定义的,因此你不能依赖于 map 项的特定顺序。
  • 当为 .proto 生成文本格式时,map 会根据键进行排序。数值键会按数字顺序排序。
  • 当从底层传输格式解析或进行合并时,如果存在重复的 map 键,将使用最后一个看到的键。 当从文本格式解析 map 时,如果存在重复的键,解析可能会失败。
  • 如果你为 map 字段提供了键但没有提供值,序列化时的行为取决于语言。 在 C++、Java、Kotlin 和 Python 中,会序列化该类型的默认值,而在其他语言中,则什么也不会被序列化。
  • FooEntry 不能与 map foo 在同一作用域中存在,因为 FooEntry 已经被 map 的实现使用。

生成的 map API 当前在所有支持的语言中都可用。 你可以在相关的 API 文档 中找到关于所选语言的 map API 的更多信息。

向后兼容性问题

map 语法在的等价形式如下,因此不支持 map 的 protocol buffers 实现仍然可以处理你的数据:

message MapFieldEntry {
  key_type key = 1;
  value_type value = 2;
}

repeated MapFieldEntry map_field = N;

任何支持 map 的协议缓冲区实现,必须既能够生成,也能够接收可以被之前定义接受的数据。

Packages

你可以在 .proto 文件中添加一个可选的 package 说明符,以防止协议消息类型之间的命名冲突。

package foo.bar;
message Open { ... }

然后,在定义消息类型的字段时,你可以使用包说明符:

message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}

package 说明符对生成的代码的影响取决于你选择的语言:

  • C++ 中,生成的类被包装在一个 C++ 命名空间中。例如,Open 会位于命名空间 foo::bar 中。
  • JavaKotlin 中,包会作为 Java 包使用,除非你在 .proto 文件中显式提供了 option java_package
  • Python 中,package 指令会被忽略,因为 Python 模块是根据文件系统中的位置来组织的。
  • Go 中,包指令会被忽略,生成的 .pb.go 文件会位于与对应的 go_proto_library Bazel 规则命名相同的包中。 对于开源项目,你 必须 提供 option go_package 或设置 Bazel 的 -M 标志。
  • Ruby 中,生成的类会被包装在嵌套的 Ruby 命名空间中,并转换为所需的 Ruby 大小写风格(首字母大写;如果首字母不是字母,则会在前面加上 PB_)。 例如,Open 会位于命名空间 Foo::Bar 中。
  • PHP 中,package 会在转换为 PascalCase 后用作命名空间,除非你在 .proto 文件中显式提供了 option php_namespace。 例如,Open 会位于命名空间 Foo\Bar 中。
  • C# 中,包会在转换为 PascalCase 后用作命名空间,除非你在 .proto 文件中显式提供了 option csharp_namespace。 例如,Open 会位于命名空间 Foo.Bar 中。

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

Packages 和名称解析

protocol buffer 语言中的类型名称解析方式类似于 C++:首先搜索最内层的作用域,然后是下一个内层作用域,依此类推,每个包被视为其父包的“内部”包。 一个前导的点(例如,.foo.bar.Baz)表示从最外层作用域开始搜索。

protocol buffer 编译器通过解析导入的 .proto 文件来解析所有类型名称。 每种语言的代码生成器知道如何在该语言中引用每个类型,即使它有不同的作用域规则。

定义服务

如果你想将消息类型与 RPC(远程过程调用)系统一起使用,可以在 .proto 文件中定义一个 RPC 服务接口,protocol buffer 编译器将生成所选语言的服务接口代码和存根。 例如,如果你想定义一个 RPC 服务,其中的方法接受 SearchRequest 并返回 SearchResponse,你可以在 .proto 文件中按如下方式定义:

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

与 protocol buffer 一起使用的最直接的 RPC 系统是 gRPC:这是一个语言和平台中立的开源 RPC 系统,由 Google 开发。 gRPC 与 protocol buffer 特别兼容,并允许你使用特殊的 protocol buffer 编译器插件直接从 .proto 文件生成相关的 RPC 代码。

如果你不想使用 gRPC,也可以将 protocol buffer 与自定义的 RPC 实现一起使用。 你可以在 Proto2 语言指南 中了解更多信息。

此外,还有一些正在进行的第三方项目,用于为 protocol buffer 开发 RPC 实现。 有关我们已知的项目链接列表,请参阅 第三方插件维基页面

JSON 映射

标准的 protobuf 二进制网络传输格式是使用 protobuf 进行系统间通信时首选的序列化格式。 对于与使用 JSON 而非 protobuf 网络传输格式的系统进行通信,Protobuf 支持在 ProtoJSON 中的规范编码。

选项

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

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

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

  • java_package (文件选项):你想要为生成的 Java/Kotlin 类使用的包。 如果在 .proto 文件中没有显式给出 java_package 选项,则默认使用 proto 包(在 .proto 文件中使用 package 关键字指定的包)。 然而,proto 包通常不适合作为 Java 包,因为 proto 包通常不会以反向域名的方式开始。 如果没有生成 Java 或 Kotlin 代码,则此选项没有任何效果。
option java_package = "com.example.foo";
  • java_outer_classname (文件选项): 你想要生成的包装器 Java 类的类名(因此也是文件名)。 如果在 .proto 文件中没有显式指定 java_outer_classname,则类名将通过将 .proto 文件名转换为驼峰命名法来构造(例如,foo_bar.proto 会变成 FooBar.java)。 如果禁用了 java_multiple_files 选项,则为 .proto 文件生成的所有其他类/枚举等将作为嵌套类/枚举等生成在这个外部包装器 Java 类中。 如果没有生成 Java 代码,则此选项没有任何效果。
option java_outer_classname = "Ponycopter";
  • java_multiple_files (文件选项): 如果为 false,则只会为该 .proto 文件生成一个单独的 .java 文件,且为顶层消息、服务和枚举生成的所有 Java 类/枚举等将嵌套在一个外部类中(见 java_outer_classname)。 如果为 true,则为每个为顶层消息、服务和枚举生成的 Java 类/枚举等生成单独的 .java 文件,并且为该 .proto 文件生成的包装器 Java 类将不包含任何嵌套类/枚举等。 这是一个布尔选项,默认值为 false。 如果没有生成 Java 代码,则此选项没有任何效果。
option java_multiple_files = true;
  • optimize_for (文件选项): 可以设置为 SPEED, CODE_SIZE, 或者 LITE_RUNTIME。 这会以以下方式影响 C++ 和 Java 代码生成器(以及可能的第三方生成器):
    • SPEED(默认模式):protocol buffer 编译器将生成用于序列化、解析和执行其他常见操作的代码。这些代码经过高度优化。
    • CODE_SIZE:protocol buffer 编译器将生成最小化的类,并依赖共享的基于反射的代码来实现序列化、解析和各种其他操作。生成的代码因此会比 SPEED 模式下的小得多,但操作会变得较慢。类仍然会实现与 SPEED 模式下完全相同的公共 API。此模式最适用于包含大量 .proto 文件的应用程序,并且不需要所有操作都非常快速。
    • LITE_RUNTIME:协议缓冲区编译器将生成仅依赖于“lite”运行时库(libprotobuf-lite 而不是 libprotobuf)的类。lite 运行时比完整的库小得多(约小一个数量级),但省略了某些功能,如描述符和反射。这对于在受限平台(如手机)上运行的应用程序特别有用。编译器仍会像在 SPEED 模式下那样生成所有方法的快速实现。生成的类将仅实现每种语言中的 MessageLite 接口,该接口只提供完整 Message 接口的子集方法。
option optimize_for = CODE_SIZE;
  • cc_generic_services, java_generic_services, py_generic_services (文件选项): 通用服务已被弃用。 protocol buffer 编译器是否应分别在 C++、Java 和 Python 中基于 服务定义 生成抽象服务代码。 出于遗留原因,这些选项默认为 true。 然而,从 2.3.0 版本(2010 年 1 月)开始,建议 RPC 实现提供 代码生成器插件,以生成更适合各个系统的代码,而不是依赖于“抽象”服务。
// This file relies on plugins to generate service code.
option cc_generic_services = false;
option java_generic_services = false;
option py_generic_services = false;
  • cc_enable_arenas (文件选项): 为 C++ 生成 arena allocation 相关代码
  • objc_class_prefix (文件选项): 设置 Objective-C 类的前缀,该前缀将添加到从此 .proto 生成的所有 Objective-C 类和枚举的前面。 没有默认值。根据 Apple 的建议,你应使用 3 到 5 个大写字母的前缀。 请注意,所有 2 个字母的前缀均由 Apple 保留。
  • packed (字段选项): 在 protobuf editions, 此选项被锁定为 true。 要使用未打包的网络传输格式,你可以通过编辑功能覆盖此选项。 这提供了与 2.3.0 版本之前的解析器的兼容性(很少需要),如下例所示:
repeated int32 samples = 4 [features.repeated_field_encoding = EXPANDED];
  • deprecated (字段选项): 如果设置为 true,表示该字段已弃用,不应被新代码使用。 在大多数语言中,这没有实际效果。 在 Java 中,这会变成 @Deprecated 注解。 对于 C++,当使用已弃用的字段时,clang-tidy 会生成警告。 未来,其他语言特定的代码生成器可能会在字段的访问器上生成弃用注解,这将导致编译尝试使用该字段的代码时发出警告。 如果该字段未被任何人使用,并且你想防止新用户使用它,可以考虑用 reserved 语句替换字段声明。
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 指定了在解析导入指令时查找 .proto 文件的目录。 如果省略此选项,将使用当前目录。 可以通过多次传递 --proto_path 选项来指定多个导入目录;这些目录将按顺序进行搜索。 -I=_IMPORT_PATH_ 可以作为 --proto_path 的简写形式。
  • 你可以提供一个或多个输出指令:
    • --cpp_out 生成 C++ 代码到 DST_DIR。有关更多信息,请参阅 C++ 生成代码参考
    • --java_out 生成 Java 代码到 DST_DIR。有关更多信息,请参阅 Java 生成代码参考
    • --kotlin_out 生成额外的 Kotlin 代码到 DST_DIR。有关更多信息,请参阅 Kotlin 生成代码参考
    • --python_out 生成 Python 代码到 DST_DIR。有关更多信息,请参阅 Python 生成代码参考
    • --go_out 生成 Go 代码到 DST_DIR。有关更多信息,请参阅 Go 生成代码参考
    • --ruby_out 生成 Ruby 代码到 DST_DIR。有关更多信息,请参阅 Ruby 生成代码参考
    • --objc_out 生成 Objective-C 代码到 DST_DIR。有关更多信息,请参阅 Objective-C 生成代码参考
    • --csharp_out 生成 C# 代码到 DST_DIR。有关更多信息,请参阅 C# 生成代码参考
    • --php_out 生成 PHP 代码到 DST_DIR。有关更多信息,请参阅 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 环境中使用时,例如用于测试。

支持的平台

有关以下内容的信息: