Protocal Buffer 语言指南(译)

2018/02/23

官方指南

Language Guide (proto3)

简介

该文章为阅读官方指南顺便翻译的。

定义消息类型

看一个简单的例子。假如你想定义一个搜索请求信息格式,搜索请求有个询问字符串。你感兴趣的特定页面的结果,以及每个结果页面的条目数。下面是 .proto 文件。

syntax = "proto3"

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

指定字段类型

在上面的例子中,所有的字段都是 scalar (标量)类型:两个整数(page_numberresult_per_page)和一个字符串(query)。当然,你也可以为你的字段指定 composite (复合)类型。

分配标签

如你所见,信息定义中的每个字段有一个独特的数字标签。这些标签被用来在二进制信息格式中识别你的字段,并且一旦你的信息类型在使用就不该修改这些字段。请注意,值为1到15的变量需要1字节编码,包括标签号和字段类型(你可以在 Protocal Buffer Encoding 中了解更多信息)。标签在16到2047之间需要2字节。所以你应该为频繁出现的消息元素保留标签1至15。请留意为将来可能添加的频繁出现的元素留出一些空间。 最小标签号码你可以指定为1,最大的为$2^{29}-1$,或536,870,911。你还不能使用数组19000至19999(FieldDescriptor::kFirstReservedNumberFieldDescriptor::kLastReservedNumber)。因为它们是为 Protocal Bufffers 接口实现保留的,如果你在.proto文件中使用这些保留数字,protocal buffer 编译器会发出警告。同样的,你不能使用任何以前保留的标签。

指定字段规则

信息字段可以是下列中一种:

在 proto3 中, repeated 的 scalar 数字类型默认使用 packed 编码。 你可以在 Protocal Buffer Encoding 中了解有关 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旧版本,可能会导致严重的问题,包括数据损坏,隐私错误,诸如此类。确保这种情况不会发生的一种方法是指定已删除字段的字段标记(和/或者名称)被保留(这可能会导致JSON序列化问题)。如果将来的任何用户试图使用这些标识符,protocal buffer 编译器将报错。

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

请注意,你不能在同一reserved语句中混合字段名称和标签号码。

你的.proto可以生成什么?

当你在protocal buffer 编译器中运行一个.proto,编译器会生成你需要的语言的代码,包括getting和setting字段的值,序列化你的信息到输出流,从输入流解析你的信息。

你可以按照所选语言的教程(即将推出proto3版本)了解更多关于使用每种语言的API的信息。有关更多API的详细信息,请参阅相关API参考(即将推出proto3版本)。

Scalar 值类型

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

.proto Type Notes C++ Type Java Type Python Type[2] Go Type Ruby Type C# Type PHP Type
double double double float float64 Float double float
float float float float float32 Float float float
int32 使用可变长度编码。负数是无效编码——如果你的字段可能含有负数,请改用sint32 int32 int int int32 Fixnum or Bignum (as required) int integer
int64 使用可变长度编码。负数是无效编码——如果你的字段可能具有负值,请改用sint64。 int64 long int/long[3] int64 Bignum long integer/string[5]
uint32 使用可变长度编码。 uint32 int[1] int/long[3] uint32 Fixnum or Bignum (as required) uint integer
uint64 使用可变长度编码。 uint64 long[1] int/long[3] uint64 Bignum ulong integer/string[5]
sint32 使用可变长度编码。带符号的int值。这些比常规的int32更有效地编码负数。 int32 int int int32 Fixnum or Bignum (as required) int integer
sint64 使用可变长度编码。带符号的int值。这些比常规的int64更有效地编码负数。 int64 long int/long[3] int64 Bignum long integer/string[5]
fixed32 总是四个字节。如果值通常大于$2^{28}$,则比uint32效率更高。 uint32 int[1] int uint32 Fixnum or Bignum (as required) uint integer
fixed64 总是八个字节。如果值通常大于$2^{56}$,则会比uint64更高效。 uint64 long[1] int/long[3] uint64 Bignum ulong integer/string[5]
sfixed32 总是四个字节。 int32 int int int32 Fixnum or Bignum (as required) int integer
sfixed64 总是八个字节。 int64 long int/long[3] int64 Bignum long integer/string[5]
bool bool boolean bool bool TrueClass/FalseClass bool boolean
string 字符串必须始终包含UTF-8编码或7位ASCII文本。 string String str/unicode[4] string String (UTF-8) string string
bytes 可能包含任何字节序列。 string ByteString str []byte String (ASCII-8BIT) ByteString string

你可以在 Protocal Buffer Encoding 中了解有关这些类型序列化消息如何编码的更多信息。

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

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

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

[4] Python字符串在解码时表示为unicode,但如果给出ASCII字符串(可能会更改),则可以为str。

[5] Integer用于64位机器,字符串用于32位机器。

默认值

当一条消息被解析,如果编码的信息不包含特定的 singular 元素,则解析对象中的对应字段将设置为该字段的默认值。这些默认值是特定于类型的:

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

请注意,对于标量(scalar)消息字段,一旦解析了消息,就无法判断字段是否被显式设置为默认值(例如布尔值是否设置为false)或者根本没有设置:在定义消息类型时应该记住这一点。举个例子,如果你不希望该行为在默认情况下发生,请将其设置为false时切换某些行为的布尔值。另请注意,如果标量(scalar)消息字段被设置为其默认值,则该值不会在连线上序列化。有关如何在生成的代码中使用默认值的更多详细信息,请参阅所选语言的生成代码指南

枚举

当你定义一个消息类型时,你可能希望它的一个字段只有一个预定义的值。例如,假设你想为每个SearchRequest添加一个corpus(语料库)字段,其中语料库可以是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRRDUCTSVIDEO。你可以非常简单地通过为每个可能值添加一个常量来为消息定义添加枚举。 下面的示例中,我们添加一个名为Corpus的枚举,其中包含所有可能的值以及一个类型为Corpus的字段:

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的常量作为第一个元素。这是因为:

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

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将一个消息中声明的枚举类型用作不同消息中字段的类型。

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

在反序列化过程中,无法识别的枚举值将保留在消息中,但是当消息被反序列化时如何表示是依赖于语言的。在支持指定符号范围之外的值的开放枚举类型的语言(如C++和Go)中,未知枚举值仅作为其基础整数表示形式存储。在具有封闭枚举类型的语言(如Java)中,枚举中的一个用于表示无法识别的值,并且可以使用特殊访问器访问基础整数。在任何一种情况下,如果消息被序列化,则无法识别的值仍将与消息一起序列化。有关如何在应用程序中使用消息枚举的更多信息,请参阅所选语言的生成代码指南

保留值

如果你通过完全删除枚举条目或将其注释掉来更新枚举类型,未来的用户可以在对该类型进行自己的更新时重新使用数值。如果稍后加载相同的.proto的旧版本,包括数据损坏,隐私错误等,则可能会导致严重问题。确保这种情况不会发生的一种方法指定已删除条目的数字值(和/或名称)被保留(这也可能会导致JSON序列化的问题)。如果将来的任何用户试图使用这些标识符,protocol buffer 编译器将会报错。你可以使用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 "myproject/other_protos.proto";

默认情况下,你只能使用直接导入的.proto文件中的定义。但是,有时你可能需要将.proto文件移至新位置。不是直接移动.proto文件,而是在一次更改中更新所有调用站点,现在你可以在旧位置放置一个虚拟.proto文件,以使用import public概念将所有导入转移到新位置。import public依赖可以被过渡到任何包含import public语句的proto中。例如:

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

协议编译器使用-I / --proto_path标志在协议编译器命令行中指定一组目录中搜索导入的文件。 如果没有给标志,它将在调用编译器的目录中查找。通常,你应该将--proto_path标志设置为根目录,并为所有导入使用完整名称。

使用proto2消息类型

可以导入proto2消息类型并在proto3消息中使用它们,反之亦然。然而,proto2枚举不能直接用在proto3语法中(如果导入的proto2消息使用它们,这是可以的)。

嵌套类型

你可以在其他消息类型中定义和使用消息类型,如下例所示这里ResultResponse消息中定义了Result消息——这里ResultResponse消息中定义了Result消息:

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

更新消息类型

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

未知字段

未知字段是格式良好的 protocal buffer 序列化数据,表示解析器无法识别的字段。例如,当一个旧的二进制文件的解析被包含新字段的新二进制文件发送时,这些新的字段将成为旧的二进制文件中的未知字段。 Proto3可以成功解析未知字段的消息,但是,是否能保留这些未知字段就不确定了。你不应该以来保存或删除未知字段。对于大多数 Google protocol buffer的实现,未知字段在proto3中无法通过相应的 proto runtimes 访问,并且在反序列化时被丢弃或遗忘。这是 proto2 的不同行为吗,其中未知字段总是与消息一起保存并序列化。

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 不同语言实现将支持runtime库帮助程序以类型安全的方式打包和解压缩Any的值——例如,在Java中,Any类型将具有特殊的pack()unpack()访问器,而在C++中则有PackForm()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 ...
  }
}

当前用于处理Ang类型的的runtime库都在开发中 如果你已经熟悉proto2语法,则Any类型会替换拓展名

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定义中。你可以添加任意类型的字段,但不能使用repeated字段。 在你生成的代码中,oneof字段与常规字段具有相同的gettersetter。你还可以获得一种特殊的方法检查oneof中的哪个值(如果有)被设置。你可以在相关的API参考中找到更多关于你所选语言的API。

Oneof 的特点

SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message();   // Will clear name field.
CHECK(!message.has_name());
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name");      // Will delete sub_message
sub_message->set_...            // Crashes here
SampleMessage msg1;
msg1.set_name("name");
SampleMessage msg2;
msg2.mutable_sub_message();
msg1.swap(&msg2);
CHECK(msg1.has_sub_message());
CHECK(msg2.has_name());

向后兼容性问题

添加或删除一个字段时请小心。如果检查一个返回值的值为None/NOT_SET,则可能意味着oneof的值没有被设置,或者被设置为不同版本的oneof字段。没有办法分辨这种差异,因为无法知道线路上的未知字段是否为oneof的成员。

标记重用问题

Maps

如果你想创建一个关联映射(map)作为数据定义的一部分,protocol buffer提供了一个方便的快捷语法。

map<key_type, value_type> map_field = N;

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

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

map<string, Project> projects = 3;

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

向后兼容性

映射语法等同于线路中的以下内容,因此不支持映射的 protocol buffer 接口实现仍然可以处理你的数据:

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

repeated MapFieldEntry map_field = N;

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

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

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

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

包和名称解决方案

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

与 protocol buffer 一起使用的最直接的RPC系统是gRPC:由谷歌开发的语言无关和平台无关的开源RPC系统。gRPC特别适用于 protocol buffer ,并且可以使用特殊的 protocol buffer 编译器插件直接从.proto文件生成相关的RPC代码。 如果你不想使用gRPC,也可以在你自己的RPC接口实现中使用 protocol buffer 。你可以在Proto2语言指南中找到更多关于此的信息。 还有一些正在进行的第三方项目为 Protocol Buffers 开发RPC实现。有关我们了解的项目的链接列表,请参阅第三方附加组件wiki页面

JSON映射

Proto3支持JSON中的规范编码,使系统之间共享数据变得更加容易。编码在下表中按类型逐个描述。 如果JSON编码数据中缺少值或其值为空,则在解析为 protocol buffer 时,它将被解释为适当的默认值。如果一个字段在 protocol buffer 中具有默认值,默认情况下它将在JSON编码数据中省略以节省空间。实现可能提供选项以在JSON编码的输出中发送具有默认值的字段。

proto3 JSON JSON example Notes
message object {“fBar”: v, “g”: null, …} 生成JSON对象。消息字段名称映射到lowerCamelCase并成为JSON对象键。接受null并将其视为相应字段类型的默认值。
enum string “FOO_BAR” 使用proto中指定的枚举值的名称。
map<K,V> object {“k”: v, …} 所有的密钥都转换为字符串。
repeated V array [v, …] null被接受为空list[]
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值将是一个十进制数。数字或字符串都被接受。
int64, fixed64, uint64 string “1”, “-10” JSON值将是一个十进制字符串。数字或字符串都被接受。
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}。否则,该值将被转换为JSON对象,并且将插入“@type”字段以指示实际的数据类型。
Timestamp string “1972-01-01T10:00:20.021Z” 使用RFC 3339,其中生成的输出始终是 Z-normalized ,并使用0,3,6或9小数位。
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” fieldmask.proto.
ListValue array [foo, bar, …]
Value value 任何JSON值
NullValue null JSON null

选项 (option)

.proto文件中的各个声明可以用多个选项批注。选项不会更改声明的整体含义,但可能会影响在特定上下文中处理它的方式。可用选项的完整列表在google/protobuf/descriptor.proto中定义。 有些选项是文件级选项,这意味着它们应该写在顶层作用域中,而不是任何消息,枚举或服务定义中。有些选项是消息级选项,意味着它们应该写在消息定义中。有些选项是字段级选项,这意味着它们应该写在字段定义中。选项也可以写在枚举类型,枚举值,服务类型和服务方法上;但是,目前没有任何有用的选项。

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

option java_package = "com.example.foo";
option java_multiple_files = true;
option java_outer_classname = "Ponycopter";
option optimize_for = CODE_SIZE;
int32 old_field = 6 [deprecated=true];

自定义选项

Protocol Buffers还允许你定义和使用你自己的选项。这是大多数人不需要的高级功能。如果你认为需要创建自己的选项,请参阅Proto2语言指南了解详细信息。请注意,创建自定义选项使用的扩展只允许proto3中的自定义选项。

生成你的类

要生成需要使用.proto文件中定义的消息类型的Java,Python,C ++,Go,Ruby,JavaNano,Objective-C或C#代码,需要在.proto文件上运行 Protocol Buffers 编译器协议。如果你尚未安装编译器,请下载软件包并按照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 --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto

其他

RPC framework

RPC (Remote Procedure Call - 远程过程调用)