protocol buffers 文档(一)-语法指导

前言

翻译查阅外网资料过程中遇到的比较优秀的文章和资料,一是作为技术参考以便日后查阅,二是训练英文能力。

此文翻译自 Protocol Buffers 官方文档 Developer Guide 部分

protocol buffers 介绍

protocol buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于通信协议、数据存储等。protocol buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小、更快、更为简单。你可以定义数据的结构,然后使用特殊生成的源代码轻松的在各种数据流中使用各种语言进行编写和读取结构数据。你甚至可以更新数据结构,而不破坏根据旧数据结构编译而成并且已部署的程序。

protocol buffers 如何工作的

你可以通过在 .proto 文件中定义 protocol buffer message 类型,来指定你想如何对序列化信息进行结构化。每一个 protocol buffer message 是一个信息的小逻辑记录,包含了一系列的 name-value 对。这里有一个非常基础的 .proto 文件样例,它定义了一个包含 "person" 相关信息的 message:

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

正如你所见,message 格式很简单 - 每种 message 类型都有一个或多个具有唯一编号的字段,每个字段都有一个名称和一个值类型,其中值类型可以是数字(整数或浮点数),布尔值,字符串,原始字节,甚至其它 protocol buffer message 类型,这意味着允许你分层次地构建数据。你可以指定 optional 字段,required 字段和 repeated 字段。 你可以在 Protocol Buffer 语言指南 中找到有关编写 .proto 文件的更多信息。

一旦定义了 messages,就可以在 .proto 文件上运行 protocol buffer 编译器来生成指定语言的数据访问类。这些类为每个字段提供了简单的访问器(如 name()和 set_name()),以及将整个结构序列化为原始字节和解析原始字节的方法 - 例如,如果你选择的语言是 C++,则运行编译器上面的例子将生成一个名为 Person 的类。然后,你可以在应用程序中使用此类来填充,序列化和检索 Person 的 messages。于是你可以写一些这样的代码:

Person person;
person.set_name("John Doe");
person.set_id(1234);
person.set_email("jdoe@example.com");
fstream output("myfile", ios::out | ios::binary);
person.SerializeToOstream(&output);

之后,你可以重新读取解析你的 message

fstream input("myfile", ios::in | ios::binary);
Person person;
person.ParseFromIstream(&input);
cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;

你可以在 message 格式中添加新字段,而不会破坏向后兼容性;旧的二进制文件在解析时只是忽略新字段。因此,如果你的通信协议使用 protocol buffers 作为其数据格式,则可以扩展协议而无需担心破坏现有代码。
你可以在 API 参考部分 中找到使用生成的 protocol buffer 代码的完整参考,你可以在 协议缓冲区编码 中找到更多关于如何对 protocol buffer messages 进行编码的信息。

为什么不使用 XML?

对于序列化结构数据,protocol buffers 比 XML 更具优势。Protocol buffers:

  • 更简单
  • 小 3 ~ 10 倍
  • 快 20 ~ 100 倍
  • 更加清晰明确
  • 自动生成更易于以编程方式使用的数据访问类

例如,假设你想要为具有姓名和电子邮件的人建模。在XML中,你需要:

<person>
    <name>John Doe</name>
    <email>jdoe@example.com</email>
</person>

而相对应的 protocol buffer message(参见 protocol buffer 文本格式)是:

# Textual representation of a protocol buffer.
# This is *not* the binary format used on the wire.
person {
  name: "John Doe"
  email: "jdoe@example.com"
}

当此消息被编码为 protocol buffer 二进制格式 时(上面的文本格式只是为了调试和编辑的方便而用人类可读的形式表示),它可能是 28 个字节长,需要大约 100-200 纳秒来解析。如果删除空格,XML版本至少为 69 个字节,并且需要大约 5,000-10,000 纳秒才能解析。
此外,比起 XML,操作 protocol buffer 更为容易:

cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;

而使用 XML,你必须执行以下操作:

cout << "Name: "
     << person.getElementsByTagName("name")->item(0)->innerText()
     << endl;
cout << "E-mail: "
     << person.getElementsByTagName("email")->item(0)->innerText()
     << endl;

但是,protocol buffers 并不总是比 XML 更好的解决方案 - 例如,protocol buffers 不是使用标记(例如 HTML)对基于文本的文档建模的好方法,因为你无法轻松地将结构与文本交错。此外,XML 是人类可读的和人类可编辑的;protocol buffers,至少它们的原生格式,并不具有这样的特点。XML 在某种程度上也是自我描述的。只有拥有 message 定义(.proto文件)时,protocol buffer 才有意义。

介绍 proto3

我们最新的版本3 release ,它引入了新的语言版本 - Protocol Buffers 语言版本3(又称 proto3),并且添加了现有语言版本(又称 proto2)的一些新功能。Proto3 简化了 Protocol Buffers 语言,既易于使用,又可以在更广泛的编程语言中使用:这个版本允许你使用 Java,C ++,Python,Java Lite,Ruby,JavaScript,Objective-C 和 C# 生成 protocol buffer 代码。此外,你可以使用最新的 Go protoc 插件为 Go 生成 proto3 代码,该插件可从 github 库 golang/protobuf 获得。更多语言正在筹备中。

请注意,两种语言版本的 API 不完全兼容。为避免给现有用户带来不便,我们将继续在新版本的 protocol buffers 中支持以前的语言版本。

你可以在 发行说明 中看到与当前默认版本的主要差异,并在 Proto3 语法指引 中了解proto3 语法)。proto3 的完整文档即将推出!

(如果名称 proto2 和 proto3 看起来有点令人困惑,那是因为当我们最初开源 protocol buffers 时,它实际上是 Google 的第二个语言版本 - 也称为 proto2。这也是为什么我们的开源版本从 v2.0.0 开始)。

一点点历史

Protocol buffers 最初是在 Google 开发的,用于处理索引服务器请求/响应协议。在 protocol buffer 之前,有一种请求和响应的格式,它手动进行编组/解组,并支持许多版本的协议。这导致了一些非常丑陋的代码,例如:

 if (version == 3) {
   ...
 } else if (version > 4) {
   if (version == 5) {
     ...
   }
   ...
 }

明确格式化的协议也使新协议版本的推出变得复杂,因为开发人员必须确保请求的发起者和处理请求的实际服务器之间的所有服务器都能理解新协议,然后才能切换开关以开始使用新协议。

协议缓冲区旨在解决这些问题:

  • 可以轻松引入新字段,中间服务器不需要检查数据,可以简单地解析它并传递数据而无需了解所有字段。
  • 格式更具自我描述性,可以用各种语言处理(C ++,Java 等)

但是,用户仍然需要手写自己的解析代码。

随着系统的发展,它获得了许多其他功能和用途:

  • 自动生成的序列化和反序列化代码避免了手动解析的需要。
  • 除了用于短期 RPC(远程过程调用)请求之外,人们还开始使用 protocol buffers 作为一种方便的自描述格式,用于持久存储数据(例如在 Bigtable 中)。
  • 服务器 RPC 接口开始被声明为协议文件的一部分,protocol 编译器生成存根类,用户可以使用服务器接口的实际实现来覆盖这些类。

Protocol buffers 现在是 Google 的数据通用语言 - 在撰写本文时,Google 代码树中有 12183 个 .proto 文件,其中一共定义了 48162 种不同的 message 类型。它们既可用于 RPC 系统,也可用于各种存储系统中的数据持久存储。

语法指引(proto2)

本指南介绍如何使用 protocol buffer 语言来构造 protocol buffer 数据,包括 .proto 文件语法以及如何从 .proto 文件生成数据访问类。它涵盖了 protocol buffer 语言的 proto2 版本:有关较新的 proto3 语法的信息,请参阅 Proto3 语法指引

这是一个参考指南,有关使用本文档中描述的许多功能的分步示例,请参阅各种语言对应的具体 教程

定义一个 Message 类型

首先让我们看一个非常简单的例子。假设你要定义一个搜索请求的 message 格式,其中每个搜索请求都有一个查询字符串,你感兴趣的特定结果页数(第几页)以及每页的结果数。下面就是定义这个请求的 .proto 文件:

message SearchRequest {
  required string query = 1;  // 查询字符串
  optional int32 page_number = 2;  // 第几页
  optional int32 result_per_page = 3;  // 每页的结果数
}

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

指定字段类型

在上面的示例中,所有字段都是 标量类型:两个整数(page_numberresult_per_page)和一个字符串(query)。但是,你还可以为字段指定复合类型,包括 枚举 和其它的 message 类型。

分配字段编号

如你所见,message 定义中的每个字段都有唯一编号。这些数字以 message 二进制格式 标识你的字段,并且一旦你的 message 被使用,这些编号就无法再更改。请注意,1 到 15 范围内的字段编号需要一个字节进行编码,编码结果将同时包含编号和类型(你可以在 Protocol Buffer 编码 中找到更多相关信息)。16 到 2047 范围内的字段编号占用两个字节。因此,你应该为非常频繁出现的 message 元素保留字段编号 1 到 15。请记住为将来可能添加的常用元素预留出一些空间。

你可以指定的最小字段数为 1,最大字段数为 2的29次方 - 1 或 536,870,911。你也不能使用 19000 到 19999 范围内的数字(FieldDescriptor::kFirstReservedNumberFieldDescriptor::kLastReservedNumber),因为它们是为 Protocol Buffers 的实现保留的 - 如果你使用这些保留数字之一,protocol buffer 编译器会抱怨你的 .proto。同样,你也不能使用任何以前定义的 保留 字段编号。

住:不能使用任何以前定义的保留字段编号” 指的是使用 reserved 关键字声明的保留字段。

指定字段规则

你指定的 message 字段可以是下面几种情况之一:

  • required: 格式良好的 message 必须包含该字段一次。
  • optional: 格式良好的 message 可以包含该字段零次或一次(不超过一次)。
  • repeated: 该字段可以在格式良好的消息中重复任意多次(包括零)。其中重复值的顺序会被保留。

由于一些历史原因,标量数字类型的 repeated 字段不能尽可能高效地编码。新代码应使用特殊选项 [packed = true] 来获得更高效的编码。例如:

repeated int32 samples = 4 [packed=true];

你可以在 Protocol Buffer 编码 中找到更多有关 packed 编码的信息。

对 required 的使用永远都应该非常小心。如果你希望在某个时刻停止写入或发送 required 字段,则将字段更改为可选字段将会有问题 - 旧读者会认为没有此字段的邮件不完整,可能会无意中拒绝或删除它们。你应该考虑为 buffers 编写特定于应用程序的自定义验证的例程。谷歌的一些工程师得出的结论是,使用 required 弊大于利;他们更喜欢只使用 optional 和 repeated。但是,这种观点并未普及。

注:在 proto3 中已经为兼容性彻底抛弃 required。

添加更多 message 类型

可以在单个 .proto 文件中定义多种 message 类型。这在你需要定义多个相关 message 的时候会很有用 - 例如,如果要定义与搜索请求相应的搜索回复 message - SearchResponse message,则可以将其添加到相同的 .proto:

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

message SearchResponse {
 ...
}

组合 messages 会导致膨胀虽然可以在单个 .proto 文件中定义多种 messages 类型(例如 message,enum 和 service),但是当在单个文件中定义了大量具有不同依赖关系的 messages 时,它也会导致依赖性膨胀。建议每个 .proto 文件包含尽可能少的 message 类型。

添加注释

为你的 .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.
}

Reserved 保留字段

如果你通过完全删除字段或将其注释掉来更新 message 类型,则未来一些用户在做他们的修改或更新时就可能会再次使用这些字段编号。如果以后加载相同 .proto 的旧版本,这可能会导致一些严重问题,包括数据损坏,隐私错误等。确保不会发生这种情况的一种方法是指定已删除字段的字段编号为 “保留” 状态。如果将来的任何用户尝试使用这些字段标识符,protocol buffer 编译器将会抱怨。

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

注:不能在同一 "reserved" 语句中将字段名称和字段编号混合在一起指定。

你的 .proto 文件将生成什么?

当你在 .proto 上运行 protocol buffer 编译器时,编译器将会生成所需语言的代码,这些代码可以操作文件中描述的 message 类型,包括获取和设置字段值、将 message 序列化为输出流、以及从输入流中解析出 message。

  • 对于 C++,编译器从每个 .proto 生成一个 .h 和 .cc 文件,其中包含文件中描述的每种 message 类型对应的类。
  • 对于 Java,编译器为每个 message 类型生成一个 .java 文件(类),以及用于创建 message 类实例的特殊 Builder 类。
  • Python 有点不同 - Python 编译器生成一个模块,其中包含 .proto 中每种 message 类型的静态描述符,然后与元类一起使用以创建必要的 Python 数据访问类。
  • 对于 Go,编译器会生成一个 .pb.go 文件,其中包含对应每种 message 类型的类型。
    你可以按照所选语言的教程了解更多有关各种语言使用 API ​​的信息。有关更多 API 详细信息,请参阅相关的 API 参考

标量值类型

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

.proto Notes C++ Java Python Go
double double double float *float64
float float float float *float32
int32 使用可变长度编码。编码负数的效率低 - 如果你的字段可能有负值,请改用 sint32 int32 int int *int32
int64 使用可变长度编码。编码负数的效率低 - 如果你的字段可能有负值,请改用 sint64 int64 long int/long[3] *int64
uint32 使用可变长度编码 uint32 int[1] int/long[3] *uint32
uint64 使用可变长度编码 uint64 long[1] int/long[3] *uint64
sint32 使用可变长度编码。有符号的 int 值。这些比常规 int32 对负数能更有效地编码 int32 int int *int32
sint64 使用可变长度编码。有符号的 int 值。这些比常规 int64 对负数能更有效地编码 int64 long int/long[3] *int64
fixed32 总是四个字节。如果值通常大于 228,则比 uint32 更有效。 uint32 int[1] int/long[3] *uint32
fixed64 总是八个字节。如果值通常大于 256,则比 uint64 更有效。 uint64 long[1] int/long[3] *uint64
sfixed32 总是四个字节 int32 int int *int32
sfixed64 总是八个字节 int64 long int/long[3] *int64
bool bool boolean bool *bool
string 字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本 string String str/unicode[4] *string
bytes 可以包含任意字节序列 string ByteString str []byte

Protocol Buffer 编码 中你可以找到有关序列化 message 时这些类型如何被编码的详细信息。

[1] 在 Java 中,无符号的 32 位和 64 位整数使用它们对应的带符号表示,第一个 bit 位只是简单的存储在符号位中。
[2] 在所有情况下,设置字段的值将执行类型检查以确保其有效。
[3] 64 位或无符号 32 位整数在解码时始终表示为 long,但如果在设置字段时给出 int,则可以为int。在所有情况下,该值必须适合设置时的类型。见 [2]。
[4] Python 字符串在解码时表示为 unicode,但如果给出了 ASCII 字符串,则可以是 str(这条可能会发生变化)。

Optional 可选字段和默认值

如上所述,message 描述中的元素可以标记为可选 optional。格式良好的 message 可能包含也可能不包含被声明为可选的元素。解析 message 时,如果 message 不包含 optional 元素,则解析对象中的相应字段将设置为该字段的默认值。可以将默认值指定为 message 描述的一部分。例如,假设你要为 SearchRequest 的 result_per_page 字段提供默认值10。

optional int32 result_per_page = 3 [default = 10];

如果未为 optional 元素指定默认值,则使用特定于类型的默认值:对于字符串,默认值为空字符串。对于 bool,默认值为 false。对于数字类型,默认值为零。对于枚举,默认值是枚举类型定义中列出的第一个值。这意味着在将值添加到枚举值列表的开头时必须小心。有关如何安全的更改定义的指导,请参阅 更新 Message 类型 部分(见下面的 更新 message 类型)。

枚举 Enumerations

在定义 message 类型时,你可能希望其中一个字段只有一个预定义的值列表。例如,假设你要为每个 SearchRequest 添加语料库字段,其中语料库可以是 UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS 或 VIDEO。你可以通过向 message 定义添加枚举来简单地执行此操作 - 具有枚举类型的字段只能将一组指定的常量作为其值(如果你尝试提供不同的值,则解析器会将其视为一个未知的领域)。在下面的例子中,我们添加了一个名为 Corpus 的枚举,其中包含所有可能的值,之后定义了一个类型为 Corpus 枚举的字段:

message SearchRequest {
  required string query = 1;
  optional int32 page_number = 2;
  optional int32 result_per_page = 3 [default = 10];
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  optional Corpus corpus = 4 [default = UNIVERSAL];
}

你可以通过为不同的枚举常量指定相同的值来定义别名。为此,你需要将 allow_alias 选项设置为true,否则 protocol 编译器将在找到别名时生成错误消息。

enum EnumAllowingAlias {
  option allow_alias = true;
  UNKNOWN = 0;
  STARTED = 1;
  RUNNING = 1;
}
enum EnumNotAllowingAlias {
  UNKNOWN = 0;
  STARTED = 1;
  // RUNNING = 1;  // 取消此行注释将导致 Google 内部的编译错误和外部的警告消息
}

枚举器常量必须在 32 位整数范围内。由于 enum 值在线上使用 varint encoding ,负值效率低,因此不推荐使用。你可以在 message 中定义 enums,如上例所示的那样。或者将其定义在 message 外部 - 这样这些 enum 就可以在 .proto 文件中的任何 message 定义中重用。你还可以使用一个 message 中声明的 enum 类型作为不同 message 中字段的类型,使用语法 MessageType.EnumType来实现。

当你在使用 enum.proto 上运行 protocol buffer 编译器时,生成的代码将具有相应的用于 Java 或 C++ 的 enum,或者用于创建集合的 Python 的特殊 EnumDescriptor 类。运行时生成的类中具有整数值的符号常量。

有关如何在应用程序中使用 enums 的更多信息,请参阅相关语言的 代码生成指南

保留值

如果你通过完全删除枚举条目或将其注释掉来更新枚举类型,则未来用户可能在对 message 做出自己的修改或更新时重复使用这些数值。如果以后加载相同 .proto 的旧版本,这可能会导致严重问题,包括数据损坏,隐私错误等。确保不会发生这种情况的一种方法是指定已删除字段的字段编号为 “保留” 状态。如果将来的任何用户尝试使用这些字段标识符,protocol buffer 编译器将会抱怨。你可以使用 max 关键字指定保留的数值范围一直到最大值。

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}

注:你不能在同一 "reserved" 语句中将字段名称和字段编号混合在一起指定。

使用其他 Message 类型

你可以使用其他 message 类型作为字段类型。例如,假设你希望在每个 SearchResponse 消息中包含 Result message - 为此,你可以在同一 .proto 中定义 Result message 类型,然后在SearchResponse 中指定 Result 类型的字段:

message SearchResponse {
  repeated Result result = 1;
}

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

导入定义 Importing Definitions

在上面的示例中,Result message 类型在与 SearchResponse 相同的文件中定义 - 如果要用作字段类型的 message 类型已在另一个 .proto 文件中定义,该怎么办?

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

import "myproject/other_protos.proto";

默认情况下,你只能使用直接导入的 .proto 文件中的定义。但是,有时你可能需要将 .proto 文件移动到新位置。现在,你可以在旧位置放置一个虚拟 .proto 文件,以使用 import public 概念将所有导入转发到新位置,而不是直接移动 .proto 文件并在一次更改中更新所有调用点。导入包含 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";
// 你可以使用 old.proto 和 new.proto 中的定义,但无法使用 other.proto

使用命令 -I/--proto_path 让 protocol 编译器在指定的一组目录中搜索要导入的文件。如果没有给出这个命令选项,它将查找调用编译器所在的目录。通常,你应将 --proto_path 设置为项目的根目录,并对所有导入使用完全限定名称。

使用 proto3 Message 类型

可以导入 proto3 message 类型并在 proto2 message 中使用它们,反之亦然。但是,proto2 枚举不能用于 proto3 语法。

嵌套类型 Nested Types

你可以在其他 message 类型中定义和使用 message 类型,如下例所示 - 此处结果消息在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;
}

你可以根据需要深入的嵌套消息:

message Outer {                  // Level 0
  message MiddleAA {  // Level 1
    message Inner {   // Level 2
      required int64 ival = 1;
      optional bool  booly = 2;
    }
  }
  message MiddleBB {  // Level 1
    message Inner {   // Level 2
      required int32 ival = 1;
      optional bool  booly = 2;
    }
  }
}

Groups

请注意,此功能已弃用,在创建新消息类型时不应使用 - 请改用嵌套消息类型。
Groups 是在 message 定义中嵌套信息的另一种方法。例如,指定包含许多结果的SearchResponse 的另一种方法如下:

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

group 只是将嵌套 message 类型和字段组合到单个声明中。在你的代码中,你可以将此消息视为具有名为 resultResult 类型字段(前一名称转换为小写,以便它不与前者冲突)。因此,此示例完全等同于上面的 SearchResponse,但 message 具有不同的编码结果。

注:此功能已弃用,这里只为尽可能保留原文内容。

更新 message 类型

如果现有的 message 类型不再满足你的所有需求 - 例如,你希望 message 格式具有额外的字段 - 但你仍然希望使用旧格式创建代码,请不要担心!在不破坏任何现有代码的情况下更新 message 类型非常简单。请记住以下规则:

  • 请勿更改任何现有字段的字段编号。
  • 你添加的任何新字段都应该是 optionalrepeated。这意味着使用“旧”消息格式的代码序列化的任何消息都可以由新生成的代码进行解析,因为它们不会缺少任何 required 元素。你应该为这些元素设置合理的 默认值,以便新代码可以正确地与旧代码生成的 message 进行交互。同样,你的新代码创建的 message 可以由旧代码解析:旧的二进制文件在解析时只是忽略新字段。但是未丢弃这个新字段(未知字段),如果稍后序列化消息,则将新字段(未知字段)与其一起序列化 - 因此,如果将消息传递给新代码,则新字段仍然可用。
  • 只要在更新的 message 类型中不再使用字段编号,就可以删除非必填字段。你可能希望重命名该字段,可能添加前缀 "OBSOLETE_",或者将字段编号保留(Reserved),以便将来你的 .proto 的用户不会不小心重用这个编号。
  • 只要类型和编号保持不变,非必填字段就可以转换为扩展 extensions,反之亦然。
  • int32uint32int64uint64bool 都是兼容的 - 这意味着你可以将字段从这些类型更改为另一种类型,而不会破坏向前或向后兼容性。如果从中解析出一个不符合相应类型的数字,你将获得与在 C++ 中将该数字转换为该类型时相同的效果(例如,如果将 64 位数字作为 int32 读取,它将被截断为 32 位)。
  • sint32sint64 彼此兼容,但与其他整数类型不兼容。
  • 只要字节是有效的 UTF-8,stringbytes 就是兼容的。
  • 如果字节包含 message 的编码版本,则嵌入 message 与 bytes 兼容。
  • fixed32sfixed32 兼容,fixed64sfixed64 兼容。
  • optionalrepeated 兼容。给定重复字段的序列化数据作为输入,期望该字段为 optional 的客户端将采用最后一个输入值(如果它是基本类型字段)或合并所有输入元素(如果它是 message 类型字段)。
  • 更改默认值通常是正常的,只要你记住永远不会通过网络发送默认值。因此,如果程序接收到未设置特定字段的消息,则程序将看到该程序的协议版本中定义的默认值。它不会看到发件人代码中定义的默认值。
  • enumint32uint32int64uint64兼容(注意,如果它们不适合,值将被截断),但要注意 message 反序列化时客户端代码对待它们将有所不同。值得注意的是,当 message 被反序列化时,将丢弃无法识别的 enum 值,这使得字段的 has.. 访问器返回 false 并且其 getter 返回 enum 定义中列出的第一个值,或者如果指定了一个默认值则返回默认值。在 repeated 枚举字段的情况下,任何无法识别的值都将从列表中删除。但是,整数字段将始终保留其值。因此,在有可能接收超出范围的枚举值时,对整数升级为 enum 这一操作需要非常小心。
  • 在当前的 Java 和 C++ 实现中,当删除无法识别的 enum 值时,它们与其他未知字段一起存储。请注意,如果此数据被序列化,然后由识别这些值的客户端重新解析,则会导致奇怪的行为。在 optional 可选字段的情况下,即使在反序列化原始 message 之后写入新值,旧值仍然可以被客户端识别。在 repeated 字段的情况下,旧值将出现在任何已识别和新添加的值之后,这意味着顺序将不被保留。
  • 将单个 optional 值更改为 newoneof 的成员是安全且二进制兼容的。如果你确定没有代码一次设置多个,则将多个 optional 字段移动到新的 oneof 中可能是安全的。但是将任何字段移动到现有的 oneof 是不安全的。

扩展 Extensions

通过扩展,你可以声明 message 中的一系列字段编号用于第三方扩展。扩展名是那些未由原始 .proto 文件定义的字段的占位符。这允许通过使用这些字段编号来定义部分或全部字段从而将其它 .proto 文件定义的字段添加到当前 message 定义中。我们来看一个例子:

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

这表示 Foo 中的字段数 [100,199] 的范围是为扩展保留的。其他用户现在可以使用指定范围内的字段编号在他们自己的 .proto 文件中为 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 类型,但不能是 oneofs 或 maps。

嵌套扩展

你可以在另一种 message 类型内部声明扩展:

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

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

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

换句话说,唯一的影响是 bar 是在 Baz 的范围内定义。

注意: 这是一个常见的混淆源:在一个 message 类型中声明嵌套的扩展块并不意味着外部类型和扩展类型之间存在任何关系。特别是,上面的例子并不意味着 Baz 是 Foo 的任何子类。这意味着符号栏是在 Baz 范围内声明的;它仅仅只是一个静态成员而已。

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

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

注:实际上就是要对某个 message A 扩展一个字段 B(B 类型),那么可以将这条扩展语句写在 message B 的定义里。

但是,并不是必须要在类型内才能定义该类型的扩展字段。你也可以这样做:

message Baz {
  ...
}

// 该定义甚至可以移到另一个文件中
extend Foo {
  optional Baz foo_baz_ext = 127;
}

实际上,这种语法可能是首选的,以避免混淆。如上所述,嵌套语法经常被不熟悉扩展的用户误认为是子类。

选择扩展字段编号

确保两个用户不使用相同的字段编号向同一 message 类型添加扩展名非常重要 - 如果扩展名被意外解释为错误类型,则可能导致数据损坏。你可能需要考虑为项目定义扩展编号的约定以防止这种情况发生。

如果你的编号约定可能涉及那些具有非常大字段编号的扩展,则可以使用 max 关键字指定扩展范围至编号最大值:

message Foo {
  extensions 1000 to max;
}

最大值为 2的29次方 - 1,或者 536,870,911。

与一般选择字段编号时一样,你的编号约定还需要避免 19000 到 19999 的字段编号(FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber),因为它们是为 Protocol Buffers 实现保留的。你可以定义包含此范围的扩展名范围,但 protocol 编译器不允许你使用这些编号定义实际扩展名。

Oneof

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

Oneof 字段类似于可选字段,除了 oneof 共享内存中的所有字段,并且最多只能同时设置一个字段。设置 oneof 的任何成员会自动清除所有其他成员。你可以使用特殊的 case() 或 WhichOneof() 方法检查 oneof 字段中当前是哪个值(如果有)被设置,具体方法取决于你选择的语言。

使用 Oneof

要在 .proto 中定义 oneof,请使用 oneof 关键字,后跟你的 oneof 名称,在本例中为 test_oneof:

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

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

在生成的代码中,oneof 字段与常规 optional 方法具有相同的 getter 和 setter。你还可以使用特殊方法检查 oneof 中的值(如果有)。你可以在相关的 API 参考中找到有关所选语言的 oneof API的更多信息。

Oneof 特性

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

SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // Will clear name field.
CHECK(!message.has_name());


- 如果解析器遇到同一个 oneof 的多个成员,则在解析的消息中仅使用看到的最后一个成员。

- oneof 不支持扩展

- oneof 不能使用 repeated

- 反射 API 适用于 oneof 字段

- 如果你使用的是 C++,请确保你的代码不会导致内存崩溃。以下示例代码将崩溃,因为已通过调用 set_name() 方法删除了 sub_message。

```c
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name");      // Will delete sub_message
sub_message->set_...            // Crashes here
  • 同样在 C++中,如果你使用 Swap() 交换了两条 oneofs 消息,则每条消息将以另一条消息的 oneof 实例结束:在下面的示例中,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 的值返回 None/NOT_SET,则可能意味着 oneof 尚未设置或已设置为 oneof 的另一个字段。这种情况是无法区分的,因为无法知道未知字段是否是 oneof 成员。

#### 标签重用问题

- **将 optional 可选字段移入或移出 oneof**:在序列化和解析 message 后,你可能会丢失一些信息(某些字段将被清除)。但是,你可以安全地将单个字段移动到新的 oneof 中,并且如果已知只有一个字段被设置,则可以移动多个字段。
- **删除 oneof 字段并将其重新添加回去**:在序列化和解析 message 后,这可能会清除当前设置的 oneof 字段。
- **拆分或合并 oneof**:这与移动常规的 optional 字段有类似的问题。

## Maps

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

```cpp
map<key_type, value_type> map_field = N;

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

因此,举个例子,如果要创建项目映射,其中每个 "Project" message 都与字符串键相关联,则可以像下面这样定义它:

map<string, Project> projects = 3;

生成的 map API 目前可用于所有 proto2 支持的语言。你可以在相关的 API 参考 中找到有关所选语言的 map API 的更多信息。

Maps 特性

  • maps 不支持扩展
  • maps 不能是 repeated、optional、required
  • map 值的格式排序和 map 迭代排序未定义,因此你不能依赖于特定顺序的 map 项
  • 生成 .proto 的文本格式时,maps 按键排序。数字键按数字排序
  • 当解析或合并时,如果有重复的 map 键,则使用最后看到的键。从文本格式解析 map 时,如果存在重复键,则解析可能会失败

向后兼容性

map 语法等效于以下内容,因此不支持 map 的 protocol buffers 实现仍可处理你的数据:

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

repeated MapFieldEntry map_field = N;

任何支持 maps 的 protocol buffers 实现都必须生成和接受上述定义所能接受的数据。

Packages

你可以将 optional 可选的包说明符添加到 .proto 文件,以防止 protocol message 类型之间的名称冲突。

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

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

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

package 影响生成的代码的方式取决于你所选择的语言:

  • C++ 中,生成的类包含在 C++ 命名空间中。例如,Open 将位于命名空间 foo::bar 中。
  • Java 中,除非在 .proto 文件中明确提供选项 java_package,否则该包将用作 Java 包
  • Python 中,package 指令被忽略,因为 Python 模块是根据它们在文件系统中的位置进行组织的

请注意,即使 package 指令不直接影响生成的代码,但是例如在 Python 中,仍然强烈建议指定 .proto 文件的包,否则可能导致描述符中的命名冲突并使 proto 对于其他语言不方便。

Packages 和名称解析

protocol buffer 语言中的类型名称解析与 C++ 类似:首先搜索最里面的范围,然后搜索下一个范围,依此类推,每个包被认为是其父包的 “内部”。一个领先的 '.'(例如 .foo.bar.Baz)意味着从最外层的范围开始。

protocol buffer 编译器通过解析导入的 .proto 文件来解析所有类型名称。每种语言的代码生成器都知道如何使用相应的语言类型,即使它具有不同的范围和规则。

定义服务

如果要将 message 类型与 RPC(远程过程调用)系统一起使用,则可以在 .proto 文件中定义 RPC 服务接口,protocol buffer 编译器将使用你选择的语言生成服务接口代码和存根。因此,例如,如果要定义一个 RPC 服务,其中具有一个获取 SearchRequest 并返回 SearchResponse 的方法,可以在 .proto 文件中定义它,如下所示:

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

默认情况下,protocol 编译器将生成一个名为 SearchService 的抽象接口和相应的 “存根” 实现。存根转发所有对 RpcChannel 的调用,而 RpcChannel 又是一个抽象接口,你必须根据自己的 RPC 系统自行定义。例如,你可以实现一个 RpcChannel,它将 message 序列化并通过 HTTP 将其发送到服务器。换句话说,生成的存根提供了一个类型安全的接口,用于进行基于 protocol-buffer 的 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: 一个由谷歌开发的与语言和平台无关的开源 RPC 系统。gRPC 特别适用于 protocol buffers,并允许你使用特殊的 protocol buffers 编译器插件直接从 .proto 文件生成相关的 RPC 代码。但是,由于使用 proto2 和 proto3 生成的客户端和服务器之间存在潜在的兼容性问题,我们建议你使用 proto3 来定义 gRPC 服务。你可以在 Proto3 语言指南 中找到有关 proto3 语法的更多信息。如果你确实希望将 proto2 与 gRPC 一起使用,则需要使用 3.0.0 或更高版本的 protocol buffers 编译器和库。

除了 gRPC 之外,还有许多正在进行的第三方项目,用于开发 Protocol Buffers 的 RPC 实现。有关我们了解的项目的链接列表,请参阅 第三方附加组件维基页面

选项 Options

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

一些选项是文件级选项,这意味着它们应该在顶级范围内编写,而不是在任何消息,枚举或服务定义中。一些选项是 message 消息级选项,这意味着它们应该写在 message 消息定义中。一些选项是字段级选项,这意味着它们应该写在字段定义中。选项也可以写在枚举类型、枚举值、服务类型和服务方法上,但是,目前在这几个项目上并没有任何有用的选项。

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

  • 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 文件名转换为 camel-case 来构造类名(因此 foo_bar.proto 变为 FooBar.java)。如果不生成 Java 代码,则此选项无效。

```protobuf
option java_outer_classname = "Ponycopter";
  • optimize_for(文件选项):可以设置为

    SPEED,CODE_SIZE 或 LITE_RUNTIME。这会以下列方式影响 C++和 Java 的代码生成器(可能还有第三方生成器):

    • SPEED(默认值):protocol buffer 编译器将生成用于对 message 类型进行序列化,解析和执行其他常见操作的代码。此代码经过高度优化。
  • CODE_SIZE:protocol buffer 编译器将生成最少的类,并依赖于基于反射的共享代码来实现序列化,解析和各种其他操作。因此,生成的代码将比使用 SPEED 小得多,但操作会更慢。类仍将实现与 SPEED 模式完全相同的公共 API。此模式在包含大量 .proto 文件的应用程序中最有用,并且不需要所有这些文件都非常快。

    • LITE_RUNTIME:protocol buffer 编译器将生成仅依赖于 “lite” 运行时库(libprotobuf-lite 而不是libprotobuf)的类。精简版运行时比整个库小得多(大约小一个数量级),但省略了描述符和反射等特定功能。这对于在移动电话等受限平台上运行的应用程序尤其有用。编译器仍将生成所有方法的快速实现,就像在 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 实现最好提供 [代码生成器插件](https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.compiler.plugin.pb) 生成更具体到每个系统的代码,而不是依赖于 “抽象” 服务。

```protobuf
// 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
  • 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(field option):如果设置为true,表示该字段已弃用,新代码不应使用该字段。在大多数语言中,这没有实际效果。在 Java 中,这变成了@Deprecated注释。将来,其他特定于语言的代码生成器可能会在字段的访问器上生成弃用注释,这将导致在编译尝试使用该字段的代码时发出警告。如果任何人都未使用该字段,并且你希望阻止新用户使用该字段,请考虑使用reserved替换字段声明。

optional int32 old_field = 6 [deprecated=true];


### 自定义选项

Protocol Buffers 甚至允许你定义和使用自己的选项。请注意,这是 **高级功能**,大多数人不需要。由于选项是由 `google/protobuf/descriptor.proto`(如 `FileOptions` 或 `FieldOptions`)中定义的消息定义的,因此定义你自己的选项只需要扩展这些消息。例如:

```protobuf
import "google/protobuf/descriptor.proto";

extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}

message MyMessage {
option (my_option) = "Hello world!";
}

这里我们通过扩展 MessageOptions 定义了一个新的 message 级选项。然后,当我们使用该选项时,必须将选项名称括在括号中以指示它是扩展名。我们现在可以在 C++ 中读取 my_option 的值,如下所示:

string value = MyMessage::descriptor()->options().GetExtension(my_option);

这里,MyMessage::descriptor()->options() 返回 MyMessageMessageOptions protocol message。从中读取自定义选项就像阅读任何其他扩展。

同样,在 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.EnumOptions {
  optional bool my_enum_option = 50003;
}
extend google.protobuf.EnumValueOptions {
  optional uint32 my_enum_value_option = 50004;
}
extend google.protobuf.ServiceOptions {
  optional MyEnum my_service_option = 50005;
}
extend google.protobuf.MethodOptions {
  optional MyMessage my_method_option = 50006;
}

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

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

生成你的类

要生成 Java,Python 或 C++代码,你需要使用 .proto 文件中定义的 message 类型,你需要在 .proto 上运行 protocol buffer 编译器 protoc。如果尚未安装编译器,请 下载软件包 并按照 README 文件中的说明进行操作。

Protocol 编译器的调用如下:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto
  • IMPORT_PATH 指定在解析导入指令时查找 .proto 文件的目录。如果省略,则使用当前目录。可以通过多次传递 --proto_path 选项来指定多个导入目录;他们将按顺序搜索。-I = IMPORT_PATH 可以用作 --proto_path 的缩写形式。
  • 你可以提供一个或多个输出指令:
    • --cpp_outDST_DIR 中生成 C++ 代码。有关详细信息,请参阅 C++ 生成的代码参考
    • --java_outDST_DIR中生成 Java 代码。有关更多信息,请参阅 Java 生成的代码参考
    • --python_outDST_DIR 中生成 Python 代码。有关更多信息,请参阅 Python 生成的代码
      为了方便起见,如果 DST_DIR 以 .zip 或 .jar 结尾,编译器会将输出写入到具有给定名称的单个 ZIP 格式的存档文件。.jar 输出还将根据 Java JAR 规范的要求提供清单文件。请注意,如果输出存档已存在,则会被覆盖;编译器不够智能,无法将文件添加到现有存档中。
  • 你必须提供一个或多个 .proto 文件作为输入。可以一次指定多个 .proto 文件。虽然文件是相对于当前目录命名的,但每个文件必须驻留在其中一个 IMPORT_PATH 中,以便编译器可以确定其规范名称。

语言指导(proto3)

翻译自:https://developers.google.com/protocol-buffers/docs/proto3

本指导描述了如何使用 protocol buffer 语言来构建 protocol buffer 数据,包括 .proto 文件语法和如何基于该 .proto 文件生成数据访问类。本文是涵盖 protocol buffer 语言 proto3 版本的内容,若需要 proto2 版本的信息,请参考 Proto2 Language Guide

本文是语言指导——关于文中描述内容的分步示例,请参考所选编程语言的对应 tutorial

定义一个消息类型

我们先看一个简单示例。比如说我们想定义个关于搜索请求的消息,每个搜索请求包含一个查询字符串,一个特定的页码,和每页的结果数量。下面是用于定义消息类型的 .proto 文件:

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
  • 文件的第一行指明了我们使用的是 proto3 语法:若不指定该行 protocol buffer 编译器会认为是 proto2 。该行必须是文件的第一个非空或非注释行。
  • SearchRequest 消息定义了三个字段(名称/值对),字段就是每个要包含在该类型消息中的部分数据。每个字段都具有名称和类型 。

指定字段类型

上面的例子中,全部字段都是标量类型:两个整型(page_numberresult_per_page)和一个字符串型(query)。同样,也可以指定复合类型的字段,包括枚举型和其他消息类型。

分配字段编号

正如你所见,消息中定义的每个字段都有一个唯一编号。字段编号用于在消息二进制格式中标识字段,同时要求消息一旦使用字段编号就不应该改变。注意一点 1 到 15 的字段编号需要用 1 个字节来编码,编码同时包括字段编号和字段类型( 获取更多信息请参考 Protocol Buffer Encoding )。16 到 2047 的字段变化使用 2 个字节。因此应将 1 到 15 的编号用在消息的常用字段上。注意应该为将来可能添加的常用字段预留字段编号。

最小的字段编号为 1,最大的为 2^29 - 1,或 536,870,911。注意不能使用 19000 到 19999 (FieldDescriptor::kFirstReservedNumberFieldDescriptor::kLastReservedNumber)的字段编号,因为是 protocol buffer 内部保留的——若在 .proto 文件中使用了这些预留的编号 protocol buffer 编译器会发出警告。同样也不能使用之前预留的字段编号。

指定字段规则

消息的字段可以是一下规则之一:

  • singular , 格式良好的消息可以有 0 个或 1 个该字段(但不能多于 1 个)。这是 proto3 语法的默认字段规则。
  • repeated ,格式良好的消息中该字段可以重复任意次数(包括 0 次)。重复值的顺序将被保留。

在 proto3 中,标量数值类型的重复字段默认会使用 packed 压缩编码。

更多关于 packed 压缩编码的信息请参考 Protocol Buffer Encoding

增加更多消息类型

单个 .proto 文件中可以定义多个消息类型。这在定义相关联的多个消息中很有用——例如要定义与搜索消息SearchRequest 相对应的回复消息 SearchResponse,则可以在同一个 .proto 文件中增加它的定义:

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

message SearchResponse {
 ...
}

增加注释

使用 C/C++ 风格的 ///* ... */ 语法在 .proto 文件添加注释。

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

保留字段

在采取彻底删除或注释掉某个字段的方式来更新消息类型时,将来其他用户再更新该消息类型时可能会重用这个字段编号。后面再加载该 .ptoto 的旧版本时会引发好多问题,例如数据损坏,隐私漏洞等。一个防止该问题发生的办法是将删除字段的编号(或字段名称,字段名称会导致在 JSON 序列化时产生问题)设置为保留项 reserved。protocol buffer 编译器在用户使用这些保留字段时会发出警告。

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

注意,不能在同一条 reserved 语句中同时使用字段编号和名称。

.proto 文件会生成什么?

当 protocol buffer 编译器作用于一个 .proto 文件时,编辑器会生成基于所选编程语言的关于 .proto 文件中描述消息类型的相关代码 ,包括对字段值的获取和设置,序列化消息用于输出流,和从输入流解析消息。

  • 对于 C++, 编辑器会针对于每个 .proto 文件生成.h.cc 文件,对于每个消息类型会生成一个类。
  • 对于 Java, 编译器会生成一个 .java 文件和每个消息类型对应的类,同时包含一个特定的 Builder类用于构建消息实例。
  • Python 有些不同 – Python 编译器会对于 .proto 文件中每个消息类型生成一个带有静态描述符的模块,以便于在运行时使用 metaclass 来创建必要的 Python 数据访问类。
  • 对于 Go, 编译器会生成带有每种消息类型的特定数据类型的定义在.pb.go 文件中。
  • 对于 Ruby,编译器会生成带有消息类型的 Ruby 模块的 .rb 文件。
  • 对于Objective-C,编辑器会针对于每个 .proto 文件生成pbobjc.hpbobjc.m. 文件,对于每个消息类型会生成一个类。
  • 对于 C#,编辑器会针对于每个 .proto 文件生成.cs 文件,对于每个消息类型会生成一个类。
  • 对于 Dart,编辑器会针对于每个 .proto 文件生成.pb.dart 文件,对于每个消息类型会生成一个类。

可以参考所选编程语言的教程了解更多 API 的信息。更多 API 详细信息,请参阅相关的 API reference

标量数据类型

消息标量字段可以是以下类型之一——下表列出了可以用在 .proto 文件中使用的类型,以及在生成代码中的相关类型:

.proto Type Notes C++ Type Java Type Python Type[2] Go Type Ruby Type C# Type PHP Type Dart Type
double double double float float64 Float double float double
float float float float float32 Float float float double
int32 使用变长编码。负数的编码效率较低——若字段可能为负值,应使用 sint32 代替。 int32 int int int32 Fixnum or Bignum (as required) int integer int
int64 使用变长编码。负数的编码效率较低——若字段可能为负值,应使用 sint64 代替。 int64 long int/long[3] int64 Bignum long integer/string[5] Int64
uint32 使用变长编码。 uint32 int[1] int/long[3] uint32 Fixnum or Bignum (as required) uint integer int
uint64 使用变长编码。 uint64 long[1] int/long[3] uint64 Bignum ulong integer/string[5] Int64
sint32 使用变长编码。符号整型。负值的编码效率高于常规的 int32 类型。 int32 int int int32 Fixnum or Bignum (as required) int integer int
sint64 使用变长编码。符号整型。负值的编码效率高于常规的 int64 类型。 int64 long int/long[3] int64 Bignum long integer/string[5] Int64
fixed32 定长 4 字节。若值常大于2^28 则会比 uint32 更高效。 uint32 int[1] int/long[3] uint32 Fixnum or Bignum (as required) uint integer int
fixed64 定长 8 字节。若值常大于2^56 则会比 uint64 更高效。 uint64 long[1] int/long[3] uint64 Bignum ulong integer/string[5] Int64
sfixed32 定长 4 字节。 int32 int int int32 Fixnum or Bignum (as required) int integer int
sfixed64 定长 8 字节。 int64 long int/long[3] int64 Bignum long integer/string[5] Int64
bool bool boolean bool bool TrueClass/FalseClass bool boolean bool
string 包含 UTF-8 和 ASCII 编码的字符串,长度不能超过 2^32 。 string String str/unicode[4] string String (UTF-8) string string String
bytes 可包含任意的字节序列但长度不能超过 2^32 。 string ByteString str []byte String (ASCII-8BIT) ByteString string List

可以在 Protocol Buffer Encoding 中获取更多关于消息序列化时类型编码的相关信息。

[1] Java 中,无符号 32 位和 64 位整数使用它们对应的符号整数表示,第一个 bit 位仅是简单地存储在符号位中。

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

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

[4] Python 字符串在解码时表示为 unicode,但如果给出了 ASCII 字符串,则可以是 str(这条可能会发生变化)。

[5] Integer 用于 64 位机器,string 用于 32 位机器。

默认值

当解析消息时,若消息编码中没有包含某个元素,则相应的会使用该字段的默认值。默认值依据类型而不同:

  • 字符串类型,空字符串
  • 字节类型,空字节
  • 布尔类型,false
  • 数值类型,0
  • 枚举类型,第一个枚举元素
  • 内嵌消息类型,依赖于所使用的编程语言。参考 generated code guide 获取详细信息。

对于可重复类型字段的默认值是空的( 通常是相应语言的一个空列表 )。

注意一下标量字段,在消息被解析后是不能区分字段是使用默认值(例如一个布尔型字段是否被设置为 false )赋值还是被设置为某个值的。例如你不能通过对布尔值等于 false 的判断来执行一个不希望在默认情况下执行的行为。同时还要注意若一个标量字段设置为默认的值,那么是不会被序列化以用于传输的。

查看 generated code guide 来获得更多关于编程语言生成代码的内容。

枚举

定义消息类型时,可能需要某字段值是一些预设值之一。例如当需要在 SearchRequest 消息类型中增加一个 corpus 字段, corpus 字段的值可以是 UNIVERSALWEBIMAGESLOCALNEWSPRODUCTSVIDEO。仅仅需要在消息类型中定义带有预设值常量的 enum 类型即可完成上面的定义。

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 4;
}

如你所见,Corpus 枚举类型的第一个常量映射到 0 :每个枚举的定义必须包含一个映射到 0 的常量作为第一个元素。原因是:

  • 必须有一个 0 值,才可以作为数值类型的默认值。
  • 0 值常量必须作为第一个元素,是为了与 proto2 的语义兼容就是第一个元素作为默认值。

将相同的枚举值分配给不同的枚举选项常量可以定义别名。要定义别名需要将 allow_alisa 选项设置为 true,否则 protocol 编译器当发现别名定义时会报错。

enum EnumAllowingAlias {
  option allow_alias = true;
  UNKNOWN = 0;
  STARTED = 1;
  RUNNING = 1;
}
enum EnumNotAllowingAlias {
  UNKNOWN = 0;
  STARTED = 1;
  // RUNNING = 1;  // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}

枚举的常量值必须在 32 位整数的范围内。因为枚举值在传输时采用的是 varint 编码,同时负值无效因而不建议使用。可以如上面例子所示,将枚举定义在消息类型内,也可以将其定义外边——这样该枚举可以用在 .proto 文件中定义的任意的消息类型中以便重用。还可以使用 MessageType.EnumType 语法将枚举定义为消息字段的某一数据类型。

使用 protocol buffer 编译器编译 .proto 中的枚举时,对于 Java 或 C 会生成相应的枚举类型,对于 Python 会生成特定的 EnumDescriptor 类用于在运行时创建一组整型值符号常量即可。

反序列化时,未识别的枚举值会被保留在消息内,但如何表示取决于编程语言。若语言支持开放枚举类型允许范围外的值时,这些未识别的枚举值简单的以底层整型进行存储,就像 C++ 和 Go。若语言支持封闭枚举类型例如 Java,一种情况是使用特殊的访问器(译注:accessors)来访问底层的整型。无论哪种语言,序列化时的未识别枚举值都会被保留在序列化结果中。

更多所选语言中关于枚举的处理,请参考 generated code guide

保留值

在采取彻底删除或注释掉某个枚举值的方式来更新枚举类型时,将来其他用户再更新该枚举类型时可能会重用这个枚举数值。后面再加载该 .ptoto 的旧版本时会引发好多问题,例如数据损坏,隐私漏洞等。一个防止该问题发生的办法是将删除的枚举数值(或名称,名称会导致在 JSON 序列化时产生问题)设置为保留项 reserved。protocol buffer 编译器在用户使用这些特定数值时会发出警告。可以使用 max 关键字来指定保留值的范围到最大可能值。

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}

注意不能在 reserved 语句中混用字段名称和数值。

使用其他消息类型

消息类型也可作为字段类型。例如,我们需要在 SearchResponse 消息中包含 Result 消息——想要做到这一点,可以将 Result 消息类型的定义放在同一个 .proto 文件中同时在 SearchResponse 消息中指定一个 Result 类型的字段:

message SearchResponse {
  repeated Result results = 1;
}

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

导入定义

前面的例子中,我们将 Result 消息定义在了与 SearchResponse 相同的文件中——但若我们需要作为字段类型使用的消息类型已经定义在其他的 .proto 文件中了呢?

可以通过导入操作来使用定义在其他 .proto 文件中的消息定义。在文件的顶部使用 import 语句完成导入其他 .proto 文件中的定义:

import "myproject/other_protos.proto";

默认情况下仅可以通过直接导入 .proto 文件来使用这些定义。然而有时会需要将 .proto 文件移动位置。可以通过在原始位置放置一个伪 .proto 文件使用 import public 概念来转发对新位置的导入,而不是在发生一点更改时就去更新全部对旧文件的导入位置。任何导入包含 import public 语句的 proto 文件就会对其中的 import public 依赖产生传递依赖。例如:

// new.proto
// 全部定义移动到该文件

// old.proto
// 这是在客户端中导入的伪文件
import public "new.proto";
import "other.proto";

// client.proto
import "old.proto";
// 可使用 old.proto 和 new.proto 中的定义,但不能使用 other.proto 中的定义

protocol 编译器会使用命令行参数 -I/--proto_path 所指定的目录集合中检索需要导入的文件。若没有指定,会在调用编译器的目录中检索。通常应该将 --proto_path 设置为项目的根目录同时在 import 语句中使用全限定名。

使用 proto2 类型

可以在 proto3 中导入 proto2 定义的消息类型,反之亦然。然而,proto2 中的枚举不能直接用在 proto3 语法中(但导入到 proto2 中 proto3 定义的枚举是可用的)。

嵌套类型

可以在一个消息类型中定义和使用另一个消息类型,如下例所示—— 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_ ,或保留字段编号以保证后续不能重用该编号。
  • int32uint32int64uint64, 和 bool 是完全兼容的——意味着可以从这些字段其中的一个更改为另一个而不破坏前后兼容性。若解析出来的数值与相应的类型不匹配,会采用与 C++ 一致的处理方案(例如,若将 64 位整数当做 32 位进行读取,则会被转换为 32 位)。
  • sint32sint64 相互兼容但不与其他的整型兼容。
  • string and bytes 在合法 UTF-8 字节前提下也是兼容的。
  • 嵌套消息与 bytes 在 bytes 包含消息编码版本的情况下也是兼容的。
  • fixed32sfixed32 兼容, fixed64sfixed64兼容。
  • enumint32uint32int64,和 uint64 兼容(注意若值不匹配会被截断)。但要注意当客户端反序列化消息时会采用不同的处理方案:例如,未识别的 proto3 枚举类型会被保存在消息中,但是当消息反序列化时如何表示是依赖于编程语言的。整型字段总是会保持其的值。
  • 将一个单独值更改为新 oneof 类型成员之一是安全和二进制兼容的。 若确定没有代码一次性设置多个值那么将多个字段移入一个新 oneof 类型也是可行的。将任何字段移入已存在的 oneof 类型是不安全的。

未知字段

未知字段是解析结构良好的 protocol buffer 已序列化数据中的未识别字段的表示方式。例如,当旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段。

本来,proto3 在解析消息时总是会丢弃未知字段,但在 3.5 版本中重新引入了对未知字段的保留机制以用来兼容 proto2 的行为。在 3.5 或更高版本中,未知字段在解析时会被保留同时也会包含在序列化结果中。

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 Any& detail : status.details()) {
  if (detail.Is<NetworkErrorDetails>()) {
    NetworkErrorDetails network_error;
    detail.UnpackTo(&network_error);
    ... processing network_error ...
  }
}

当前处理 Any 类型的运行库正在开发中

若你已经熟悉了 proto2 语法,Any 类型的位于 extensions 部分。

Oneof

若一个含有多个字段的消息同时大多数情况下一次仅会设置一个字段,就可以使用 oneof 特性来强制该行为同时节约内存。

Oneof 字段除了全部字段位于 oneof 共享内存以及大多数情况下一次仅会设置一个字段外与常规字段类似。对任何oneof 成员的设置会自动清除其他成员。可以通过 case()WhichOneof() 方法来检测 oneof 中的哪个值被设置了,这个需要基于所选的编程语言。

使用 oneof

使用 oneof 关键字在 .proto 文件中定义 oneof,同时需要跟随一个 oneof 的名字,就像本例中的 test_oneof

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

然后将字段添加到 oneof 的定义中。可以增加任意类型的字段,但不能使用 repeated 字段。

在生成的代码中,oneof 字段和常规字段一致具有 getters 和 setters 。同时也会获得一个方法以用于检测哪个值被设置了。更多所选编程语言中关于 oneof 的 API 可以参考 API reference

Oneof 特性

  • 设置 oneof 的一个字段会清除其他字段。因此入设置了多次 oneof 字段,仅最后设置的字段生效。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message();   // 会清理 name 字段
CHECK(!message.has_name());
  • 若解析器在解析得到的数据时碰到了多个 oneof 的成员,最后一个碰到的是最终结果。
  • oneof 不能是 repeated
  • 反射 API 可作用于 oneof 字段。
  • 若将一个 oneof 字段设为了默认值(就像为 int32 类型设置了 0 ),那么 oneof 字段会被设置为 "case",同时在序列化编码时使用。
  • 若使用 C++ ,确认代码不会造成内存崩溃。以下的示例代码就会导致崩溃,因为 sub_message 在调用 set_name() 时已经被删除了。
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name");      // 会删除 sub_message
sub_message->set_...            // 此处会崩溃
  • 同样在 C++ 中,若 Swap() 两个 oneof 消息,那么消息会以另一个消息的 oneof 的情况:下例中,msg1会是 sub_message1msg2 中会是 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:这与移动常规字段有类似的问题。

Map 映射表

若需要创建关联映射表作为定义的数据的一部分,protocol buffers 提供了方便的快捷语法:

map<key_type, value_type> map_field = N;

key_type 处可以是整型或字符串类型(其实是除了 float 和 bytes 类型外任意的标量类型)。注意枚举不是合法的 key_typevalue_type 是除了 map 外的任意类型。

例如,若需要创建每个项目与一个字符串 key 相关联的映射表,可以采用下面的定义:

map<string, Project> projects = 3;
  • 映射表字段不能为 repeated
  • 映射表的编码和迭代顺序是未定义的,因此不能依赖映射表元素的顺序来操作。
  • 当基于 .proto 生成文本格式时,映射表的元素基于 key 来排序。数值型的 key 基于数值排序。
  • 当解析或合并时,若出现冲突的 key 以最后一个 key 为准。当从文本格式解析时,若 key 冲突则会解析失败。
  • 若仅仅指定了映射表中某个元素的 key 而没有指定 value,当序列化时的行为是依赖于编程语言。在 C++,Java,和 Python 中使用类型的默认值来序列化,但在有些其他语言中可能不会序列化任何东西。

生成的映射表 API 当前可用于全部支持 proto3 的编程语言。在 API reference 中可以获取更多关于映射表 API 的内容。

向后兼容问题

映射表语法与以下代码是对等的,因此 protocol buffers 的实现即使不支持映射表也可以正常处理数据:

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

repeated MapFieldEntry map_field = N;

任何支持映射表的 protocol buffers 实现都必须同时处理和接收上面代码的数据定义。

可以在 .proto 文件中使用 package 指示符来避免 protocol 消息类型间的命名冲突。

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

这样在定义消息的字段类型时就可以使用包指示符来完成:

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

包指示符的处理方式是基于编程语言的:

  • C++ 中生成的类位于命名空间中。例如,Open 会位于命名空间 foo::bar 中。
  • Java 中,使用 Java 的包,除非在 .proto 文件中使用 option java_pacakge 做成明确的指定。
  • Python 中,package 指示符被忽略,这是因为 Python 的模块是基于文件系统的位置来组织的。
  • Go 中,作为 Go 的包名来使用,除非在 .proto 文件中使用 option java_pacakge 做成明确的指定。
  • Ruby 中,生成的类包裹于 Ruby 的命名空间中,还要转换为 Ruby 所需的大小写风格(首字母大写;若首字符不是字母,则使用 PB_ 前缀)。例如,Open 会位于命名空间 Foo::Bar 中。
  • C# 中作为命名空间来使用,同时需要转换为 PascalCase 风格,除非在 .proto 使用 option csharp_namespace 中明确的指定。例如,Open 会位于命名空间 Foo.Bar 中。

包和名称解析

protocol buffer 中类型名称解析的工作机制类似于 C++ :先搜索最内层作用域,然后是次内层,以此类推,每个包被认为是其外部包的内层。前导点(例如,.foo.bar.Baz)表示从最外层作用域开始。

protocol buffer 编译器会解析导入的 .proto 文件中的全部类型名称。基于编程语言生成的代码也知道如何去引用每种类型,即使编程语言有不同的作用域规则。

定义服务

若要在 RPC (Remote Procedure Call,远程过程调用)系统中使用我们定义的消息类型,则可在 .proto 文件中定义这个 RPC 服务接口,同时 protocol buffer 编译器会基于所选编程语言生成该服务接口代码。例如,若需要定义一个含有可以接收 SearchRequest 消息并返回 SearchResponse 消息方法的 RPC 服务,可以在 .proto 文件中使用如下代码定义:

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

最直接使用 protocal buffer 的 RPC 系统是 gRPC :一款 Google 开源,语言和平台无关的 RPC 系统。gRPC 对 protocol buffer 的支持非常好同时允许使用特定的 protocol buffer 编译器插件来基于 .proto 文件生成相关的代码。

若不想使用 gRPC,同样可以在自己的 RPC 实现上使用 protocol buffer。可以在 Proto2 Language Guide 处获得更多关于这方面的信息。

同样也有大量可用的第三方使用 protocol buffer 的项目。对于我们了解的相关项目列表,请参考 third-party add-ons wiki page

JSON 映射

Proto3 支持 JSON 的规范编码,这使得系统间共享数据变得更加容易。下表中,将逐类型地描述这些编码。

若 JSON 编码中不存在某个值或者值为 null,当将其解析为 protocol buffer 时会解析为合适的默认值。若 procol buffer 中使用的是字段的默认值,则默认情况下 JSON 编码会忽略该字段以便于节省空间。实现上应该提供一个选项以用来将具有默认值的字段生成在 JSON 编码中。

proto3 JSON JSON 示例 说明
message object {"fooBar": v, "g": null,…} 生成 JSON 对象。消息字段名映射为对象的 lowerCamelCase(译著:小驼峰) 的 key。若指定了 json_name 选项,则使用该选项值作为 key。解析器同时支持 lowerCamelCase 名称(或 json_name 指定名称)和原始 proto 字段名称。全部类型都支持 null 值,是当做对应类型的默认值来对待的。
enum string "FOO_BAR" 使用 proto 中指定的枚举值的名称。解析器同时接受枚举名称和整数值。
map<K,V> object ` 所有的 key 被转换为字符串类型。
repeated V array [v, …] null 被解释为空列表 []。
bool true, false true, false
string string "Hello World!"
bytes base64 string "YWJjMTIzIT8kKiYoKSctPUB+" JSON 值是使用标准边界 base64 编码的字符串。不论标准或 URL 安全还是携带边界与否的 base64 编码都支持。
int32, fixed32, uint32 number 1, -10, 0 JSON 值是 10 进制数值。数值或字符串都可以支持。
int64, fixed64, uint64 string "1", "-10" JSON 值是 10 进制字符串。数值或字符串都支持。
float, double number 1.1, -10.0, 0, "NaN","Infinity" JSON 值是数值或特定的字符串之一:"NaN","Infinity" 和 "-Infinity" 。数值和字符串都支持。指数表示法同样支持。
Any object {"@type": "url", "f": v, … } 若 Any 类型包含特定的 JSON 映射值,则会被转换为下面的形式: {"@type": xxx, "value": yyy}。否则,会被转换到一个对象中,同时会插入一个 "@type" 元素用以指明实际的类型。
Timestamp string "1972-01-01T10:00:20.021Z" 采用 RFC 3339 格式,其中生成的输出总是 Z规范的,并使用 0、3、6 或 9 位小数。除 “Z” 以外的偏移量也可以。
Duration string "1.000340012s", "1s" 根据所需的精度,生成的输出可能会包含 0、3、6 或 9 位小数,以 “s” 为后缀。只要满足纳秒精度和后缀 “s” 的要求,任何小数(包括没有)都可以接受。
Struct object { … } 任意 JSON 对象。参见 struct.proto.
Wrapper types various types 2, "2", "foo", true,"true", null, 0, … 包装器使用与包装的原始类型相同的 JSON 表示,但在数据转换和传输期间允许并保留 null。
FieldMask string "f.fooBar,h" 参见field_mask.proto
ListValue array [foo, bar, …]
Value value Any JSON value
NullValue null JSON null
Empty object {} 空 JSON 对象

JSON 选项

proto3 的 JSON 实现可以包含如下的选项:

  • 省略使用默认值的字段:默认情况下,在 proto3 的 JSON 输出中省略具有默认值的字段。该实现可以使用选项来覆盖此行为,来在输出中保留默认值字段。
  • 忽略未知字段:默认情况下,proto3 的 JSON 解析器会拒绝未知字段,同时提供选项来指示在解析时忽略未知字段。
  • 使用 proto 字段名称代替 lowerCamelCase 名称: 默认情况下,proto3 的 JSON 编码会将字段名称转换为 lowerCamelCase(译著:小驼峰)形式。该实现提供选项可以使用 proto 字段名代替。Proto3 的 JSON 解析器可同时接受 lowerCamelCase 形式 和 proto 字段名称。
  • 枚举值使用整数而不是字符串表示: 在 JSON 编码中枚举值是使用枚举值名称的。提供了可以使用枚举值数值形式来代替的选项。

选项

.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_multiple_files (文件选项):导致将顶级消息、枚举、和服务定义在包级,而不是在以 .proto 文件命名的外部类中。
option java_multiple_files = true;
  • java_outer_classname(文件选项):想生成的最外层 Java 类(也就是文件名)。若没有在 .proto 文件中明确指定 java_outer_classname 选项,类名将由 .proto 文件名转为 camel-case 来构造(因此 foo_bar.proto 会变为 FooBar.java)。若不生成 Java 代码,则此选项无效。
option java_outer_classname = "Ponycopter";
  • optimize_for (文件选项): 可被设为 SPEEDCODE_SIZE,或 LITE_RUNTIME。这会影响 C++ 和 Java 代码生成器(可能包含第三方生成器) 的以下几个方面:
  • SPEED (默认): protocol buffer 编译器将生成用于序列化、解析和消息类型常用操作的代码。生成的代码是高度优化的。
  • CODE_SIZE :protocol buffer 编译器将生成最小化的类,并依赖于共享的、基于反射的代码来实现序列化、解析和各种其他操作。因此,生成的代码将比 SPEED 模式小的多,但操作将变慢。类仍将实现与 SPEED 模式相同的公共 API。这种模式在处理包含大量 .proto 文件同时不需要所有操作都要求速度的应用程序中最有用。
  • LITE_RUNTIME :protocol buffer 编译器将生成仅依赖于 “lite” 运行库的类(libprotobuf-lite 而不是libprotobuf)。lite 运行时比完整的库小得多(大约小一个数量级),但会忽略某些特性,比如描述符和反射。这对于在受限平台(如移动电话)上运行的应用程序尤其有用。编译器仍然会像在 SPEED 模式下那样生成所有方法的快速实现。生成的类将仅用每种语言实现 MessageLite 接口,该接口只提供 Message 接口的一个子集。
option optimize_for = CODE_SIZE;    
  • cc_enable_arenas(文件选项):为生成的 C++ 代码启用 arena allocation
  • objc_class_prefix (文件选项): 设置当前 .proto 文件生成的 Objective-C 类和枚举的前缀。没有默认值。你应该使用 recommended by Apple 的 3-5 个大写字母作为前缀。注意所有 2 个字母前缀都由 Apple 保留。
  • deprecated (字段选项):若设置为 true, 指示该字段已被废弃,新代码不应使用该字段。在大多数语言中,这没有实际效果。在 Java 中,这变成了一个 @Deprecated 注释。将来,其他语言的代码生成器可能会在字段的访问器上生成弃用注释,这将导致在编译试图使用该字段的代码时发出警告。如果任何人都不使用该字段,并且您希望阻止新用户使用它,那么可以考虑使用保留语句替换字段声明。
int32 old_field = 6 [deprecated=true];

自定义选项

protocol buffer 还允许使用自定义选项。大多数人都不需要此高级功能。若确认要使用自定义选项,请参阅 Proto2 Language Guide 了解详细信息。注意使用 extensions 来创建自定义选项,只允许用于 proto3 中。

生成自定义类

若要生成操作 .proto 文件中定义的消息类型的 Java、Python、C++、Go、Ruby、Objective-C 或 C# 代码,需要对 .proto 文件运行 protocol buffer 编译器 protoc。若还没有安装编译器,请 download the package 并依据 README 完成安装。对于 Go ,还需要为编译器安装特定的代码生成器插件:可使用 GitHub 上的 golang/protobuf 库。

Protocol buffer 编译器的调用方式如下:

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_PATHimport 指令检索 .proto 文件的目录。若未指定,使用当前目录。多个导入目录可以通过多次传递 --proto_path 选项实现;这些目录会依顺序检索。 -I=*IMPORT_PATH* 可作为 --proto_path 的简易格式使用。
  • 可以提供一个或多个输出指令:
  • --cpp_outDST_DIR目录 生成 C++ 代码。参阅 C++ generated code reference 获取更多信息。
  • --java_outDST_DIR目录 生成 Java 代码。参阅 Java generated code reference 获取更多信息。
  • --python_outDST_DIR目录 生成 Python代码。参阅 Python generated code reference 获取更多信息。
  • --go_outDST_DIR目录 生成 Go 代码。参阅 Go generated code reference 获取更多信息。
  • --ruby_outDST_DIR目录 生成 Ruby 代码。 coming soon!
  • --objc_outDST_DIR目录 生成 Objective-C 代码。参阅 Objective-C generated code reference 获取更多信息。
  • --csharp_outDST_DIR目录 生成 C# 代码。参阅 C# generated code reference 获取更多信息。
  • --php_outDST_DIR目录 生成 PHP代码。参阅 PHP generated code reference 获取更多信息。

作为额外的便利,若 DST_DIR 以 .zip.jar 结尾,编译器将会写入给定名称的 ZIP 格式压缩文件,.jar 还将根据 Java JAR 的要求提供一个 manifest 文件。请注意,若输出文件已经存在,它将被覆盖;编译器还不够智能,无法将文件添加到现有的存档中。

  • 必须提供一个或多个 .proto 文件作为输入。可以一次指定多个 .proto 文件。虽然这些文件是相对于当前目录命名的,但是每个文件必须驻留在 IMPORT_PATHs 中,以便编译器可以确定它的规范名称。
posted @ 2020-03-21 01:39  WindSun  阅读(2946)  评论(0编辑  收藏  举报
博客已停更,文章已转移,点击访问