Protobuf

[翻译] ProtoBuf 官方文档(二)- 语法指引(prot

2018-07-29  本文已影响0人  401

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

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

翻译为意译,不会照本宣科的字字对照翻译
以下为原文内容翻译

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

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

指定字段规则

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

由于一些历史原因,标量数字类型的 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 的旧版本,这可能会导致一些严重问题,包括数据损坏,隐私错误等。确保不会发生这种情况的一种方法是指定已删除字段的字段编号(有时也需要指定名称为保留状态,英文名称可能会导致 JSON 序列化问题)为 “保留” 状态。如果将来的任何用户尝试使用这些字段标识符,protocol buffer 编译器将会抱怨。

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

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

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

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

标量值类型

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

.proto Type Notes C++ Type Java Type Python Type[2] Go Type
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 的旧版本,这可能会导致严重问题,包括数据损坏,隐私错误等。确保不会发生这种情况的一种方法是指定已删除字段的字段编号(有时也需要指定名称为保留状态,英文名称可能会导致 JSON 序列化问题)为 “保留” 状态。如果将来的任何用户尝试使用这些字段标识符,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 类型非常简单。请记住以下规则:

扩展 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 的原始定义中。

译者注:
第一段翻译过来的语义实在是太别扭了(因为站在了被扩展字段所在的 .proto 文件的角度来看待扩展),实际站在扩展字段所在的 .proto 文件的角度-就是可以在自己的 .proto 文件中扩展其他人定义的另一个 .proto 中的 message。

当用户的 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;
}

最大值为 229 - 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 的值返回 None/NOT_SET,则可能意味着 oneof 尚未设置或已设置为 oneof 的另一个字段。这种情况是无法区分的,因为无法知道未知字段是否是 oneof 成员。

标签重用问题

Maps

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

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 特性

向后兼容性

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 影响生成的代码的方式取决于你所选择的语言:

请注意,即使 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 消息定义中。一些选项是字段级选项,这意味着它们应该写在字段定义中。选项也可以写在枚举类型、枚举值、服务类型和服务方法上,但是,目前在这几个项目上并没有任何有用的选项。

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

option java_outer_classname = "Ponycopter";
// This file relies on plugins to generate service code.
option cc_generic_services = false;
option java_generic_services = false;
option py_generic_services = false;
message Foo {
  option message_set_wire_format = true;
  extensions 4 to max;
}

自定义选项

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

import "google/protobuf/descriptor.proto";

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

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

这里我们通过扩展 MessageOptions 定义了一个新的 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

上一篇下一篇

猜你喜欢

热点阅读