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

语言指南 (proto3)

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

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

这是一个参考指南,如需有关使用本文档中介绍的许多功能的分步示例,请参阅所选语言的教程(目前仅限 proto2;更多 proto3 文档即将推出。

定义消息类型

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

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
  • 文件的第一行指定您使用 proto3 语法:如果您不执行此操作,协议缓冲区编译器会假定您使用的是 proto2。此文件必须是文件的第一行非空行。
  • SearchRequest 消息定义指定三个字段(名称/值对),每个字段对应于要包含在此类消息中的每段数据。每个字段都有一个名称和类型。

指定字段类型

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

分配字段编号

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

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

指定字段规则

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

  • singular:格式正确的消息可以有零个或一个字段(但不能超过一个)。使用 proto3 语法时,如果未为给定字段指定其他字段规则,则这是默认字段规则。您无法确定它是否已通过连接线解析。它将被序列化为电汇,除非它是默认值。如需详细了解此主题,请参阅字段存在
  • optional:与 singular 相同,不过您可以检查该值是否明确设置。optional 字段处于以下两种可能状态之一:
    • 该字段已设置,并包含从传输中明确设置或解析的值。它会序列化到线路。
    • 未设置字段,将返回默认值。它不会序列化到有线网络。
  • repeated:在格式正确的消息中,此字段类型可以重复零次或多次。系统会保留重复值的顺序。
  • map:这是一个成对的键值对字段。如需详细了解此字段类型,请参阅地图

在 proto3 中,标量数字类型的 repeated 字段默认使用 packed 编码。如需详细了解 packed 编码,请参阅协议缓冲区编码

添加更多消息类型

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

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

message SearchResponse {
 ...
}

添加评论

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

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

message SearchRequest {
  string query = 1;
  int32 page_number = 2;  // Which page number do we want?
  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";
}

请注意,您不能在同一 reserved 语句中混用字段名称和字段编号。

根据您的 .proto 生成的内容

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

  • 对于 C++,编译器会从每个 .proto 生成一个 .h.cc 文件,并针对文件中所述的每种消息类型提供一个类。
  • 对于 Java,编译器会生成 .java 文件,其中包含每种消息类型的类,以及用于创建消息类实例的特殊 Builder 类。
  • 对于 Kotlin,除了 Java 生成的代码之外,编译器还会为每种消息类型生成一个 .kt 文件,其中包含可用于简化消息实例的创建 DSL。
  • Python 稍有不同 - Python 编译器会在 .proto 中为每个模块类型生成一个静态描述符,然后该模块将与 metaclass 搭配使用,在运行时创建必要的 Python 数据访问类。
  • 对于 Go,编译器会生成 .pb.go 文件,其中包含文件中每种消息类型的类型。
  • 对于 Ruby,编译器会生成包含消息类型的 Ruby 模块的 .rb 文件。
  • 对于 Objective-C,编译器会从每个 .proto 生成一个 pbobjc.hpbobjc.m 文件,并针对文件中介绍的每种消息类型提供一个类。
  • 对于 C#,编译器会从每个 .proto 生成一个 .cs 文件,并针对文件中所述的每种消息类型提供一个类。
  • 对于 Dart,编译器会生成 .pb.dart 文件,并针对文件中的每种消息类型生成一个类。

如需详细了解如何为每种语言使用 API,请按照所选语言的教程(proto3 版本即将推出)进行操作。如需了解更详细的 API 信息,请参阅相关的 API 参考文档(proto3 版本也即将推出)。

标量值类型

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

.proto 类型 备注 C++ 类型 Java/Kotlin 类型[1] Python 类型[3] Go 类型 Ruby 类型 C# 类型 PHP 类型 飞镖类型
双精度 double 双精度 float 浮点数 64 浮点数 双精度 float 双精度
float float float float 浮点数 32 浮点数 float float 双精度
int32 使用可变长度的编码。对负数的编码效率低下 - 如果您的字段可能包含负值,请改用 sint32。 int32 int int int32 Fixnum 或 Bignum(根据需要) int integer int
int64 使用可变长度的编码。对负数的编码效率低下 - 如果字段可能有负值,请改用 sint64。 int64 long 整型/长整型[4] int64 大数值 long 整数/字符串[6] Int64
30 秒 使用可变长度的编码。 30 秒 整型[2] 整型/长整型[4] 30 秒 Fixnum 或 Bignum(根据需要) uint integer int
uint64 使用可变长度的编码。 uint64 [2] 整型/长整型[4] uint64 大数值 ulong 整数/字符串[6] Int64
Sint32 使用可变长度的编码。有符号整数值。与常规 int32 相比,这些函数可以更高效地对负数进行编码。 int32 int int int32 Fixnum 或 Bignum(根据需要) int integer int
Sint64 使用可变长度的编码。有符号整数值。与常规 int64 相比,这些函数可以更高效地对负数进行编码。 int64 long 整型/长整型[4] int64 大数值 long 整数/字符串[6] Int64
固定 32 始终为 4 个字节。如果值通常大于 228,则比 uint32 更高效。 30 秒 整型[2] 整型/长整型[4] 30 秒 Fixnum 或 Bignum(根据需要) uint integer int
固定 64 始终为 8 个字节。如果值通常大于 256,则比 uint64 更高效。 uint64 [2] 整型/长整型[4] uint64 大数值 ulong 整数/字符串[6] Int64
固定 32 始终为 4 个字节。 int32 int int int32 Fixnum 或 Bignum(根据需要) int integer int
固定 64 始终为 8 个字节。 int64 long 整型/长整型[4] int64 大数值 long 整数/字符串[6] Int64
bool bool 布尔值 bool bool TrueClass/FalseClass bool 布尔值 bool
字符串 字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,并且长度不得超过 232 字符串 字符串 str/unicode[5] 字符串 字符串 (UTF-8) 字符串 字符串 字符串
字节 可以包含任意长度的 232 字节。 字符串 ByteString str (Python 2)
个字节 (Python 3)
[]字节 字符串 (ASCII-8BIT) ByteString 字符串 List

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

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

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

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

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

[5] Python 字符串在解码时表示为 Unicode,但如果提供了 ASCII 字符串,则可以是 str(随时可能发生变化)。

[6] 整数用在 64 位机器上,字符串用在 32 位计算机上。

默认值

解析消息后,如果经过编码的消息不包含特定单数元素,则解析对象中的相应字段将设置为该字段的默认值。这些默认值因类型而异:

  • 对于字符串,默认值为空字符串。
  • 对于字节,默认值为空字节。
  • 对于布尔值,默认值为 false。
  • 对于数值类型,默认值为零。
  • 对于枚举,默认值为第一个定义的枚举值,必须为 0。
  • 对于消息字段,系统不会设置此字段。其确切值取决于语言。如需了解详情,请参阅生成的代码指南

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

请注意,对于标量消息字段,在解析消息后,就无从判断字段是明确设为默认值(例如布尔值是否设为 false)还是根本不设置:在定义消息类型时,您应该记住这一点。例如,如果您不想让某项行为默认发生,可设置一个布尔值,将其设为 false 时开启该行为。另请注意,如果标量消息字段已设置为默认值,则该值不会通过序列化进行序列化。

请参阅您所用语言的生成代码指南,详细了解生成的代码中默认值的工作原理。

枚举

在定义消息类型时,您可能希望它的某个字段仅具有一个预定义的值列表。例如,假设您想要为每个 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 {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  Corpus corpus = 4;
}

如您所见,Corpus 枚举的第一个常量映射到零:每个枚举定义必须包含一个映射到零的第一个常量作为其第一个元素。原因如下:

  • 必须有一个零值,以便我们可以使用 0 作为数字的默认值
  • 零值必须是第一个元素,以便与 proto2 语义(其中第一个枚举值始终是默认值)兼容。

您可以通过为不同的枚举常量分配相同的值来定义别名。为此,您需要将 allow_alias 选项设置为 true,否则协议编译器会在发现别名时生成错误消息。虽然所有别名值在反序列化期间都有效,但序列化时始终使用第一个值。

enum EnumAllowingAlias {
  option allow_alias = true;
  EAA_UNSPECIFIED = 0;
  EAA_STARTED = 1;
  EAA_RUNNING = 1;
  EAA_FINISHED = 2;
}
enum EnumNotAllowingAlias {
  ENAA_UNSPECIFIED = 0;
  ENAA_STARTED = 1;
  // ENAA_RUNNING = 1;  // Uncommenting this line will cause a compile error inside Google and a warning message outside.
  ENAA_FINISHED = 2;
}

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

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

在反序列化期间,无法识别的枚举值将保留在消息中,但当消息反序列化时如何表示该值取决于语言。在支持开放式枚举类型(例如 C++ 和 Go)范围之外的语言中,未知枚举值仅存储为其底层整数表示形式。在 Java 等封闭枚举类型语言中,枚举中的大小写用于表示无法识别的值,您可以通过特殊访问器访问底层整数。在任一情况下,如果消息已序列化,则无法识别的值仍会与消息进行序列化。

如需详细了解如何在应用中使用消息 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 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 文件放在旧位置中,并使用 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 标志设置为项目的根目录,并针对所有导入使用完全限定名称。

使用 proto2 消息类型

您可以导入 proto2 消息类型,并在 proto3 消息中使用它们,反之亦然。但是,proto2 枚举无法直接以 proto3 语法使用(如果导入的 proto2 消息使用它们,也没有关系)。

嵌套类型

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

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

如果您要在父消息类型之外重复使用此消息类型,请将其引用为 _Parent_._Type_

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

您可以根据需要嵌套深层消息:

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

更新消息类型

如果现有消息类型不再满足您的所有需求(例如,您希望消息格式有一个额外的字段),但您仍想使用以旧格式创建的代码,不用担心!更新消息类型非常简单,不会破坏任何现有代码。只需记住以下规则即可:

  • 请勿更改任何现有字段的字段编号。
  • 如果您添加新字段,则任何使用“旧”消息格式将代码序列化的消息仍可通过新生成的代码解析。您应该记住这些元素的默认值,以便新代码可以与旧代码生成的消息正确交互。同样,新代码创建的消息也可以用旧代码解析:旧二进制文件在解析时会直接忽略新字段。如需了解详情,请参阅未知字段部分。
  • 您可以移除字段,只要未在更新后的消息类型中再次使用该字段编号即可。建议您重命名该字段,添加前缀“OBSOLETE_”,或将该字段编号预留,以免 .proto 的未来用户不小心重复使用该编号。
  • int32uint32int64uint64bool 都兼容,这意味着您可以将字段从一种类型更改为另一种类型,而不会破坏向前或向后兼容性。如果从不适合相应类型的有线电缆解析数字,则其效果与在 C++ 中将该数字转换为类型时相同(例如,如果将 64 位数字读取为 int32 值,该数字将被截断为 32 位)。
  • sint32sint64 彼此兼容,但与其他整数类型不兼容。
  • 只要字节是有效的 UTF-8,stringbytes 就兼容。
  • 如果字节包含编码版本的消息,则嵌入式消息与 bytes 兼容。
  • fixed32 兼容 sfixed32fixed64sfixed64 兼容。
  • 对于 stringbytes 和消息字段,单数字段与 repeated 字段兼容。给定重复字段的序列化数据作为输入,如果该字段是基元类型字段,则希望此字段为单数的客户端将获取最后一个输入值;如果该字段是消息类型字段,则将合并所有输入元素。请注意,这对于数字类型(包括布尔值和枚举值)通常安全。数字类型的重复字段可以采用 packed 格式进行序列化,如果单数字段需要,则无法正确解析。
  • 在有线格式方面,enumint32uint32int64uint64 兼容(请注意,如果值不适合,它们会被截断)。但请注意,客户端在对消息进行反序列化时可能会以不同的方式处理它们:例如,无法识别的 proto3 enum 类型将保留在消息中,但当消息反序列化时,其表示方式取决于语言。Int 字段始终只保留它们的值。
  • 将单个 optional 字段或扩展更改为 oneof 的成员与二进制文件兼容,但对于某些语言(特别是 Go),生成的代码的 API 将以不兼容的方式更改。因此,如 AIP-180 中所述,Google 不会在其公共 API 中做出此类更改。同样,在确定源代码兼容性时,如果您确定一次一次不设置多个代码,将多个字段移至新的 oneof 可能是安全的。将字段移到现有的 oneof 是不安全的。同样,将单个字段 oneof 更改为 optional 字段或扩展也是安全的。

未知字段

未知字段是格式正确的协议缓冲区序列化数据,表示解析器无法识别的字段。例如,当旧二进制文件使用新字段解析新二进制文件发送的数据时,这些新字段将变为旧二进制文件中的未知字段。

最初,proto3 消息在解析期间始终舍弃未知字段,但在版本 3.5 中,我们重新引入了保留未知字段以匹配 proto2 行为。在版本 3.5 及更高版本中,未知字段在解析期间会保留并包含在序列化输出中。

不限

借助 Any 消息类型,您可以将消息作为嵌入式类型使用,而无需指定 .proto 定义。Any 包含任意序列化消息(如 bytes),以及作为全局唯一标识符并解析为消息类型的网址。如需使用 Any 类型,您需要导入 google/protobuf/any.proto

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}

给定消息类型的默认类型网址为 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 类型搭配使用的运行时库正在开发中

如果您已熟悉 proto2 语法,则 Any 可以保留任意 proto3 消息,类似于允许扩展程序的 proto2 消息。

单曲

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

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

请注意,如果设置了多个值,则由 proto 中的订单确定的最后一个设置值将覆盖之前的所有值

使用 Oneof

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

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

然后,将您的 oneof 字段添加到 oneof 定义中。您可以添加任何类型的字段,但 map 字段和 repeated 字段除外。

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

其中一项功能

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

    SampleMessage message;
    message.set_name("name");
    CHECK_EQ(message.name(), "");
    // Calling mutable_sub_message() will clear the name field and will set
    // sub_message to a new instance of SubMessage with none of its fields set
    message.mutable_sub_message();
    CHECK(message.name().empty());
    
  • 如果解析器在连接线上遇到同一成员的多个成员,则解析后的消息只会使用最后一位成员。

  • 其中一个不能为 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_EQ(msg2.name(), "");
    

向后兼容性问题

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

代码重用问题

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

地图

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

map<key_type, value_type> map_field = N;

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

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

map<string, Project> projects = 3;

  • 映射字段不能为 repeated
  • 映射值的线格式格式和映射迭代顺序尚未定义,因此您不能指望地图项按特定顺序排列。
  • .proto 生成文本格式时,映射按键排序。数字键按数字顺序排序。
  • 通过有线连接或合并时,如果存在重复的映射键,则使用最后一个看到的键。从文本格式解析映射时,如果存在重复的键,解析可能会失败。
  • 如果您为某个映射字段提供了键但没有值,序列化此字段时的行为将取决于语言。在 C++、Java、Kotlin 和 Python 中,该类型的默认值已序列化,但在其他语言中则未序列化。

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

向后兼容性

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

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

repeated MapFieldEntry map_field = N;

任何支持映射的协议缓冲区实现都必须生成并接受上述定义可以接受的数据。

软件包

您可以向 .proto 文件添加可选的 package 说明符,以防止协议消息类型之间出现名称冲突。

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

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

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

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

  • C++ 中,生成的类会封装在 C++ 命名空间内。例如,Open 将位于命名空间 foo::bar 中。
  • JavaKotlin 中,除非您在 .proto 文件中明确提供 option java_package,否则该软件包将用作 Java 软件包。
  • Python 中,软件包指令会被忽略,因为 Python 模块是根据它们在文件系统中的位置组织的。
  • Go 中,除非您在 .proto 文件中明确提供 option go_package,否则系统会将软件包用作 Go 软件包名称。
  • Ruby 中,生成的类会封装在嵌套的 Ruby 命名空间中,并转换为所需的 Ruby 大小写样式(第一个字母大写;如果第一个字符不是字母,则以 PB_ 开头)。例如,Open 将位于命名空间 Foo::Bar 中。
  • C# 中,除非在 .proto 文件中明确提供 option csharp_namespace,否则软件包在转换为 PascalCase 后会用作命名空间。例如,Open 将位于命名空间 Foo.Bar 中。

软件包和名称解析

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

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

定义服务

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

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

要与协议缓冲区搭配使用,最直接的 RPC 系统是 gRPC:Google 开发的一种语言和平台中立的开源 RPC 系统。gRPC 可与协议缓冲区配合使用,并可让您使用特殊的协议缓冲区编译器插件直接从 .proto 文件中生成相关的 RPC 代码。

如果不想使用 gRPC,您还可以将协议缓冲区与自己的 RPC 实现搭配使用。如需了解详情,请参阅 Proto2 语言指南

此外,还有许多进行中的第三方项目正在为协议缓冲区开发 RPC 实现。如需查看我们已知项目的链接列表,请参阅第三方插件 Wiki 页面

JSON 映射

Proto3 支持 JSON 格式的规范编码,可让您更轻松地在系统之间共享数据。下表中按类型介绍了编码。

将采用 JSON 编码的数据解析为协议缓冲区时,如果缺少值或其值为 null,系统会将其解读为相应的默认值

从协议缓冲区生成 JSON 编码的输出时,如果 protobuf 字段具有默认值,且该字段不支持字段存在,则默认将从输出中省略该字段。实现可能会提供在输出中包含默认值的字段的选项。

使用 optional 关键字定义的 proto3 字段支持字段存在。设置了值且支持字段存在的字段始终在 JSON 编码的输出中包含字段值,即使它是默认值也是如此。

proto3 JSON JSON 示例 备注
私信 对象 {"fooBar": v, "g": null, …} 生成 JSON 对象。消息字段名称会映射到 downCamelCase 并变为 JSON 对象键。如果指定了 json_name 字段选项,则指定的值将用作键。解析器既接受 bottomCamelCase 名称(或由 json_name 选项指定的名称)也接受原始 proto 字段名称。null 是所有字段类型接受的值,被视为相应字段类型的默认值。
枚举 字符串 "FOO_BAR" 系统将使用 proto 中指定的枚举值的名称。解析器接受枚举名称和整数值。
map<K,V> 对象 {"k": v, …} 所有键均转换为字符串。
重复版本 V array [v, …] 接受 null 作为空列表 []
bool true、false true, false
字符串 字符串 "Hello World!"
字节 base64 字符串 "YWJjMTIzIT8kKiYoKSctPUB+" JSON 值是采用带填充值的标准 base64 编码编码为字符串的数据。接受带内边距或不带内边距的标准或网址安全 base64 编码。
int32、Fixed32、uint32 number 1, -10, 0 JSON 值将是一个小数。接受数字或字符串。
int64、Fixed64、uint64 字符串 "1", "-10" JSON 值将是一个十进制字符串。接受数字或字符串。
浮点数、双精度 number 1.1, -10.0, 0, "NaN", "Infinity" JSON 值是数字或特殊字符串值“NaN”、“Infinity”和“-Infinity”。接受数字或字符串。也可使用指数表示法。-0 等同于 0。
不限 object {"@type": "url", "f": v, … } 如果 Any 包含具有特殊 JSON 映射的值,则将按如下方式转换该值:{"@type": xxx, "value": yyy}。否则,该值将转换为 JSON 对象,并插入 "@type" 字段以指示实际数据类型。
时间戳 字符串 "1972-01-01T10:00:20.021Z" 使用 RFC 3339,其中生成的输出始终是 Z 归一化,并使用 0、3、6 或 9 个小数位。也可接受除“Z”以外的偏移。
时长 字符串 "1.000340012s", "1s" 生成的输出始终包含 0 位、3 位、6 位或 9 位小数(具体取决于所需的精度),后跟后缀“s”。接受任意小数位(也可以为零),前提是它们精确到纳秒精度,且必须带有后缀“s”。
结构体 object { … } 任何 JSON 对象。请参阅struct.proto
封装容器类型 各种类型 2, "2", "foo", true, "true", null, 0, … 封装容器在 JSON 中使用的表示法与封装的基元类型相同,只不过在转换和传输数据期间允许并保留 null
FieldMask 字符串 "f.fooBar,h" 请参阅field_mask.proto
ListValue array [foo, bar, …]
任何 JSON 值。如需了解详情,请参阅 google.protobuf.Value
NullValue null JSON null
对象 {} 空 JSON 对象

JSON 选项

proto3 JSON 实现可能会提供以下选项:

  • 发出具有默认值的字段:默认情况下,proto3 JSON 输出中会忽略具有默认值的字段。实现可能会提供一个选项,用于将相应行为和输出字段替换为默认值。
  • 忽略未知字段:默认情况下,Proto3 JSON 解析器应拒绝未知字段,但可能会提供在解析时忽略未知字段的选项。
  • 使用 proto 字段名称,而不是 lowCamelCase 名称:默认情况下,proto3 JSON 打印机应将字段名称转换为 downCamelCase 格式,并将其用作 JSON 名称。实现可能会提供将 proto 字段名称用作 JSON 名称的选项。Proto3 JSON 解析器需要同时接受转换后的 LowerCamelCase 名称和 proto 字段名称。
  • 将枚举值作为整数(而不是字符串)发出:默认情况下,JSON 输出中使用枚举值的名称。可能会提供相应选项,以改用枚举值的数值。

选项

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

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

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

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

    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_enable_arenas(文件选项):为 C++ 生成的代码启用区域分配

  • objc_class_prefix(文件选项):设置 Objective-C 类前缀,附加到此 .proto 中所有通过 Objective-C 生成的类和枚举前面。没有默认值。您应该使用 Apple 建议的前缀(长度介于 3-5 个大写字母之间)。请注意,所有 2 个字母的前缀都是 Apple 保留的。

  • deprecated(字段选项):如果设置为 true,则表示该字段已弃用,不应被新代码使用。在大多数语言中,这没有实际影响。在 Java 中,这将成为 @Deprecated 注解。对于 C++,每次使用已弃用的字段时,clang-tidy 都会生成警告。日后,特定于语言的代码生成器可能会在字段访问器上生成弃用注释,这反过来会导致在编译尝试使用该字段的代码时发出警告。如果该字段未被任何人使用,而您想阻止新用户使用该字段,请考虑将该字段声明替换为预留语句。

    int32 old_field = 6 [deprecated = true];
    

自定义选项

协议缓冲区还允许您定义和使用自己的选项。这是一项高级功能,大多数用户都不需要。如果您确实需要创建自己的选项,请参阅 Proto2 语言指南了解详情。请注意,创建自定义选项使用扩展,只有 proto3 中的自定义选项才允许使用这些扩展。

生成类

如需生成使用 .proto 文件中定义的消息类型所需的 Java、Kotlin、Python、C++、Go、Ruby、Objective-C 或 C# 代码,您需要在 .proto 上运行协议缓冲区编译器 protoc。如果您尚未安装编译器,请下载软件包并按照自述文件中的说明操作。对于 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 格式的归档文件。根据 Java JAR 规范的要求,系统将为 .jar 输出提供一个清单文件。请注意,如果输出归档已存在,将被覆盖;编译器不够智能,无法将文件添加到现有归档。

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

文件位置

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

地理位置应该与语言无关

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

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

支持的平台

相关信息如下: