这篇指南讲述如何使用Protocol Buffers来结构化你的Protocol Buffer数据,包括.proto文件语法以及如何从.proto文件生成你的访问类型。本文主要涵盖了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消息明确定义了三个字段(键值对),对应每一条你想包含在这个消息类型中的数据。每个字段都有一个名称和类型。

指明字段类型

在上面的例子中,所有的字段都是明确类型的:两个integers(page_numberresult_per_page)和一个string(query)。当然,你也可以将你的字段指定成复合类型,包括枚举和其他消息类型。

分配字段序号

如你所见,在定义的消息中的每个字段都有一个唯一的序号。这些序号用来在二进制消息结果中标识你的字段,而且一旦使用了消息类型,就不应该再变动。注意,字段序号在1到15的范围内占用1个字节来编码,包括字段序号和字段类型(详见Protocol Buffer Encoding)。字段序号在16到2047范围内占两个字节。所以你应该为经常使用的消息元素保留1到15的序号。切记为将来可能新增的常用元素预留一些空间。

你能使用的最小字段序号为1,最大为$2^{29}-1$,或 536,870,911。但是你不能使用19000到19999(FieldDescriptor::kFirstReservedNumberFieldDescriptor::kLastReservedNumber),因为它们是为Protocol Buffers实现预留的,如果在你的.proto文件中使用了,protocol buffer编译器会报错。同样,你也不能使用任何以前保留的字段序号。

标明字段规则

消息字段可以遵循下列规则之一:

  • singular:符合语法规则的消息可以拥有0个或1个该字段(但不能超过1个)。这是proto3默认的字段规则。
  • repeated:在符合语法规则的消息中,该字段可以重复任意次数(包括0次)。重复变量的顺序将被保留。

在proto3中,repeated字段的标量数字默认使用packed编码。关于packed编码,详见Protocol Buffer Encoding

新增更多消息类型

在单个.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的,这样将来用户在使用这些字段时protocol buffer编译器就会告警。

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

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

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

当你使用protocol buffer 编译器时,编译器会根据你选定的语言来生成你.proto文件中描述的消息类型,包括获取和设置字段的值,序列化你的消息到一个输出流中,从输入流中解析你的消息。

  • C++,编译器会根据每个.proto文件生成一个.h和一个.cc,你定义的每个消息类型都会变成一个类。
  • Java,编译器会生成一个.java文件,包含每个消息类型的类,同时还会指明一个Builder类来创建消息类的实例。
  • Python有点不同,Python编译器会生成一个模块,包含你.proto文件每一消息类型的静态描述,之后在运行时通过基类来创建必要的Python数据访问类。
  • 对于Go,编译器会生成一个.pb.go的文件,包含文件中每个消息类型。
  • 对于Ruby,编译器会生成一个.rb的文件,包含消息类型的Ruby模块。
  • 对于Objective-C,编译器会根据每个.proto文件生成一个pbobjc.h和一个pbobjc.m文件,你定义的每个消息类型都会变成一个类。
  • C#,编译器会根据每个.proto文件生成一个.cs文件,你定义的每个消息类型都会变成一个类。
  • Dart,编译器生成一个.pb.dart文件,文件中定义的每个消息类型都会变成一个类。

你可以在之后的教程中找关于对应语言的APIs的使用。更多APIs细节,详见API reference

标量类型

标量字段可以是下面类型中的任意一个。下表展示了.proto文件中标明的类型,以及在自动生成的类中对应的类型:

.proto 类型 说明 C++类型 Java类型 Python类型$^{[2]}$ Go类型 Ruby类型 C#类型 PHP类型 Dart类型
double   double double float float64 Float double float double
float   float float float float32 Folat 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 int32 Fixnum or Bignum(as required) int integer int
fixed64 8字节。如果变量大于2$^{56}$比uint64高效。 uint64 long$^{[1]}$ int/long$^{[3]}$ uint64 Bignum ulong integer/string$^{[5]}$ int64
sfixed32 4字节 uint32 int$^{[1]}$ int int32 Fixnum or Bignum(as required) int integer int
sfixed64 8字节 uint64 long$^{[1]}$ int/long$^{[3]}$ uint64 Bignum ulong integer/string$^{[5]}$ int64
bool   bool boolean bool bool TrueClass/FalseClass bool boolean bool
string 必须是UTF-8编码或7-bit的ASCII文本,长度不能大于2$^{32}$ string String str/unicode$^{[4]}$ string String(UTF-8) string string String
bytes 可以包含任何长度不超过232的字节序列 string ByteString str []byte String(ASCII-8BIT) ByteString string List<int>

更多编码细节,详见Protocol Buffer Encoding

$^{[1]}$在Java中,无符号32位和64位整数使用它们的有符号对应的整数表示,顶部的整数只存储在符号位中。

$^{[2]}$在所有场景中,给字段设置值时将调用类型检查来确保有效。

$^{[3]}$64位或无符号32位整数会解码成对应的长度,但如果在设置字段时给定的是int,也可以解码为int。

$^{[4]}$Python strings会解码成unicode,但如果给定的是ASCII string,会被解码成str。

$^{[5]}$Integer用在64位机器上,string用于32位机器。

默认值

在解析消息时,如果编码的消息不包含特定的singular元素,则解析对象中的相应字段将设置为该字段的默认值。这些默认值与类型有关:

  • 对于string,默认值为空字符串。
  • 对于bytes,默认值是空bytes。
  • 对于bool,默认值为false。
  • 对于数字类型,默认值为0。
  • 对于枚举,默认值为第一个定义的枚举变量,其值必须为0。
  • 对于消息字段,未设置。他都值因语言不同而不同,详见generated code guide

对于repeated字段,其默认值为空(通常是目标语言的空列表)。

对于标量消息字段来说,一旦消息被解析,就无法判断该字段是真实被设为默认值(例如bool变量被设为false)还是就没有设置:在定义消息类型时需要牢记这一点。例如,如果你不想在默认情况向执行某种行为,那么就不要用boolean被设置为false来切换这些行为。同时,如果一个标量消息字段被设为它的默认值,那么改值在传输时将不会被序列化。

在生成代码时,在你所选的语言中默认值如何工作,详见generated code guide

枚举

当你在定义消息类型时,你可能想让它的字段只是用预定义列表中的一个值。例如,假如你想给SearchRequest添加一个corpus字段,其值可以是UNIVERSALWEBIMAGELOCALNEWSPRODUCT或者VIDEO。通过enum,在你的消息中定义一个包含每个可能值得枚举变量,可以很简单地做的。

在下面的例子中,我们添加了一个名为Corpusenum类型,它包含了所有可能的值,和一个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的常量作为第一个元素。因为:

  • 必须有一个0值,这样我们可以使用0作为数字默认值
  • 0值必须作为第一个元素,以便于proto2兼容,它的第一枚举变量总是默认值。

你可以定义别买来给不同的枚举常量分配相同的值。这样就需要你将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位正整数之间。因为enum变量在传输时使用varint encoding,负数是低效且不推荐的。你可以如上述例子一样在消息定义内部定义enum,也可以在外部定义,这样的enum可以被.proto文件中的其它消息定义使用。你也可以使用MessageType.EnumType语法将一个消息的enum声明类型作为另一个不同消息的字段类型。

当你编译一个使用了enum.proto文件时,生成的代码中会包含Java或C++对应的enum,针对Python的特定的EnumDescriptor类,用来在执行生成的类中创建一系列包含数值的符号常量。

在反序列化期间,无法识别的enum值将保留在消息中,尽管在反序列化消息时如何表示该值取决于语言。在支持指定符号范围之外使用值的开放枚举类型的语言,如c++和Go,未知的枚举值只是作为其基础整数表示形式存储。在具有封闭枚举类型的语言,如Java,枚举中的大小写用于表示无法识别的值,并且可以使用特殊的访问器访问底层整数。在任何一种情况下,如果消息被序列化,未被识别的值仍将与消息一起序列化。

在你所选的语言中,带有enum的消息如何工作,详见generated code guide

保留变量

如果你通过完全删除或注释一个字段来更新枚举类型时,那么之后的用户在更新他们自己的类型时将可以重用该字段的序号。如果之后他们使用旧版的.proto时,会引起严重的问题,包括数据损坏、隐私bug等。避免给问题的途径之一就是指明你要删除的字段需要(或者会在JSON序列化时会引起问题的名称)是reserved的,这样将来用户在使用这些字段时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文件中导入它们来使用这些定义。要使用其它.proto的定义,你需要在你的文件头部导入声明:

import "myproject/other_protos.proto";

默认情况下,你只能使用直接导入的.proto文件。但有时候你可能需要将.proto文件移到新的路径。相比于直接移到.proto文件然后更新所有用到它的地方,现在你可以在旧的路径下放置一个虚拟的.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消息类型

可以在你的proto3消息中导入并使用proto2的消息类型,反之亦可。然而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是相互兼容的,但不与其它整型兼容。
  • stringbytes兼容,bytesUTF-8兼容。
  • 如果字节包含消息的编码版本,则嵌入的消息与bytes兼容。
  • fixed32sfixed32fixed64sfixed64兼容。
  • 在传输格式中enumint32uint32int64uint64兼容(注意变量不兼容的部分将被截断)。然而需要留意的是在消息反序列化时,客户端代码会被区别对待:例如,尽管无法识别的proto3中的enum类型会被保存在消息中,但是在消息反序列化时,它是如何表示这取决于语言。int字段总会保留它的值。
  • 修改new 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类型取代扩展

Oneof

如果有有一个包含多个字段的消息,在同一时间最多只能设置一个字段,那么你可以通过使用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字段有着与常规字段一样的getterssetters。必要时,你也可以使用特定的方法来确定oneof中的哪个值被设置。关于你所选语言的oneof API,详见API reference

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的多个成员,在解析消息时仅使用最后看到的成员。
  • 不能使用repeated
  • oneof字段使用反射 APIs。
  • 如果你设置oneof字段为默认值(比如设施int32字段为0),该字段的“case”将被设置,且在传输时被序列化。
  • 如果你使用C++,请确保你的代码不会引起内存崩溃。下面的代码会引起崩溃,因为在调用set_name()方法时sub_message已经删除。
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 case结束:在下面的例子中,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(msg2.has_name());

向后兼容问题

在新增或移除oneof字段时要慎重。如果检测到oneof的返回值为None/Not_SET,可能意味着这个oneof尚未设置或已在不同版本的oneof中设置。无法区分这两者之间的不同,因为无法确定传输中的未知字段是否是给oneof的成员。

Tag重用问题

  • 移入/移出字段到oneof:在消息序列化和解析后,你可能会丢失部分消息(有些字段被清理了)。但是,你可以安全地将单个字段移动到一个新的oneof字段中,如果知道只设置了一个字段,则可以移动多个字段。
  • 删除一个oneof字段后有添加:在消息序列化和解析后,可能会将你当前的设置清零。
  • 切割/合并 oneof:与移动常规字段问题相似。

Maps

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

map<key_type, value_type> map_field = N;

这里的key_type可以是任意的integral或string类型(即除了浮点型和bytes外的所有标量类型)。注意enum不是有效的key_typevalue_type可以是除了其它Map外的所有类型。

那么,假如你想创建一个项目映射,每个项目关联一个string键,定义如下:

map<string, Project> projects = 3;

  • Map字段不可以是repeated
  • 映射值的网络格式排序和映射迭代排序是未定义的,所以在特定的排序中你不能依赖你的映射元素组成。
  • .proto生成文本格式时,映射根据键排序。数字键按数字大小排序。
  • 从网络解析/合并时,如果键有多个副本,那么使用最后遇到的键。当从文本格式中解析映射时,如果键存在副本,则可能解析失败。
  • 如果你仅提供了Map字段的键而没有提供值,字段序列化时的行为因语言而异。在C++、Java和Python中,值会被序列化为该类型的默认值,在其它语言中并不会被序列化。

Proto3现已全面支持生成Map API。关于不同语言的Map API,详见API reference

向后兼容

在网络中,Map语法等效如下示例,因此即便proto buffers实现不支持的Maps也能处理你的数据:

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

repeated MapFieldEntry map_field = N;

所有的protocol buffers实现都必须能产生和接受上述定义所接受的数据。

你可以在.proto文件中添加package说明符来避免协议消息类型键的名称冲突。

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

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

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

package说明符影响生成代码的方式依赖于你所选的语言:

  • C++中,生成的类会被打包到C++的命名空间中。例如:Open位于foo::bar命名空间中。
  • Java中,package作为Java包使用,除非在.proto文件中额外提供option java_package
  • Python中,package指令会被忽略,Python模块是根据它们在文件系统中的位置来组织的。
  • Go中,package将被用作Go的包名,除非在.proto文件中额外提供option go_package
  • Ruby中,生成的类会被打包嵌入到Ruby的命名空间中,并转换为所需的Ruby大小写样式(第一个字母大写;如果第一个字符不是字母,PB_是前缀)。例如:Open位于foo::bar命名空间中。
  • C# 中,package在被转换为PascalCase后作为命名空间使用,除非在.proto文件中额外提供option csharp_namespace

包和名称解析

Protocol buffer语言中的类型名称解析类似于C++:首先在最内层查找,之后是下一层,一次类推,每个包在其父包的“内部”。“.”开头(例如,.foo.bar.Baz)意味着从最外层作用域开始查找。

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

定义服务

如果你现在RPC(远程调用)系统中使用你的消息类型,你可以在.proto文件中定义RPC服务接口,之后protocol buffer编译器会生成所选语言的服务接口代码和存根。比如,你要定义一个RPC服务,它使用你的SearchRequest并返回SearchResponse,在.proto文件中你可以这样定义:

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

使用protocol buffer最直接的RPC系统是gRPC:由Google开发的,与语言和平台无关的开源RPC系统。gRPC与protocol buffer协同良好,它允许你使用特殊的protocol buffer插件直接从.proto文件中生成相关的RPC代码。

如果你不想使用gRPC,你也可以在你自己的RPC实现中使用protocol buffer。详见Proto2 Language Guide

也有一些正在进行的第三方项目来为protocol buffer开发RPC实现。有关我们所知项目的链接列表,请参阅third-part add-ons wiki page

Json Mapping

Proto3支持Json编码规范,这使得在不同系统间共享数据变得更加方便。在下面的表中,将逐个类型地描述编码。

如果一个值在JSON编码中丢失或为null,在解析到protocol buffer时它会被解释为合适的默认值。如果protocol buffer中的字段有默认值,那么在Json编码的数据中将默认省略该字段,以节省空间。在Json编码输出中,实现可以提供带有默认字段的选项。

proto3 Json Json示例 备注
message object {“fooBar”:v,”g”:null,_} 生成Json对象。消息字段名称被映射为lowerCamelCase并成为Json对象的键。如果指定了json_name字段选项,则指定的值将被作为键使用。解析器既接受lowerCamelCase名称(或使用json_name指定的名称),也接受原生的proto字段名称。所有字段类型都可接受null,并被视为该类型的默认值。
enum string “FOO_BAR” 使用proto中指定的enum值名称。解析器既接受枚举名称,也接受整数值。
map<K,V> object {“K”:v,_} 所有的键都被转换成string。
repeated V array [v, …] null被当做空列表[]。
bool true,false true,false  
string string “Hello World!”  
bytes base64 string “YWJjMTIzIT8kKiYoKSctPUB+” Json值会变成使用添加padding的标准base64编码的string。标准的或url安全的base64编码,带/不带padding也都可以接受。
int32,fixed32,uint32 number 1,-10,0 Json值会变成十进制的数字。数字或string都可被接受。
int64,fixed64,uint64 string “1”,”-10” Json值会变成十进制的string。数字或string都可被接受。
float,double number 1.1,-10.0,0,”NaN”,”Infinity” Json值会变成数字或”NaN”、”Infinity”、”-Infinity”其中之一。数字或string都可被接受。指数表示法也被接受。
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位小数。除“Z”以外的偏移量也可以接受。
Duration string “1.000340012s”,”1s” 根据所需的精度,生成的输出总是包含0、3、6或9位小数,跟后缀”s“。只要符合纳秒精度和后缀“s”的要求,任何小数(也可以没有)都可以接受。
Struct object { … } 任意的Json对象。参见struc.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   任意的Json值
NullValue null   Json null
Empty object {} 任意的空Json对象。

JSON 选项

Proto3的Json实现可支持下列选项:

  • 带默认值得空字段:默认情况下,在proto3 JSON输出中会省略具有默认值的字段。实现可以提供一个选项来覆盖此行为,并使用其默认值输出字段。
  • 忽略未知类型:默认情况下,Proto3 Json解析器会驳回未知字段,但在解析时可以提供选项来忽略未知字段。
  • 使用proto字段来代替lowerCamelCase名称:默认情况下,proto3 Json的输出应该将字段名转换为lowerCamelCase并作为Json名称使用。该实现可以通过提供选项来使用proto字段作为Json名称。Proto3 Json解析器被设计为可同时接受转换后的lowerCamelCase名称和proto字段名称。
  • 指明enum值作为整数而不是string:默认情况下,在Json输出中使用枚举值的名称。通过选项可指定使用数字代替枚举值。

可选项

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

有些选项是文件级别的,意味着它们应该写在开头位置,而不是在消息、枚举或服务定义中。有些选项是消息级别的,意味着它们应该写在消息定义中。有些选项是字段选项,意味着它们应该写在字段定义中。选项也可以写在枚举类型、枚举值、服务类型和服务方法中,然而,当前不存在对Any有用的选项。

下面是一些常用的选项:

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

option java_outer_classname = “Ponycopter”;

  • optimize_for(文件级):可被设为SPEEDCODE_SIZELITE_RUNTIME。这将通过以下方式影响C++和Java代码生成(也可能影响第三方生成):
    • SPEED(默认):Protocol buffer编译器会为你的消息类型生成序列化、解析和其它常用操作的代码。此代码高度优化。
    • CODE_SIZE:Protocol buffer编译器会生成最小的类,其依赖共享、反射的代码来实现序列化、解析和其它操作。因此生成的代码比SPEED小很多,但操作也会比较慢。Classes仍会实现与SPEED模式相同的公共API。这种模式在包含大量.proto文件且不是所有文件都需要快速生成的应用程序中最有用。
    • LITE_RUNTIME:Protocol buffer编译器依赖“轻量的”运行时库(使用libprotobuf-lite而不是libprotobuf)。lite运行时比完整的库小得多(大约小一个数量级),但是忽略了某些特性,比如描述符和反射。这对于在受限平台(如移动电话)上运行的应用程序尤其有用。编译器仍然会像在SPEED模式下那样生成所有方法的快速实现。生成的类将仅用每种语言实现MessageLite接口,该接口只提供完整Message接口方法的一个子集。

      option optimize_for = CODE_SIZE;

  • cc_enable_arenas(文件级):为C++代码生成启用arena allocation
  • objc_class_prefix(文件级):为.proto文件生成的所有Objective-C类设置前缀。没有默认值。你应该使用苹果推荐的前缀,即3-5个大写字母。注意苹果保留所有的2个字母的前缀。
  • deprecated(文件级):如果设置为true,则表示该字段已被废弃,新代码不应使用该字段。在大多数语言中,该选项并没有实际效果。在Java中,会变成一个@Deprecated注释。将来,其它语言的代码生成器可能会在字段访问器上生成弃用注释,这将使得编译器在尝试使用该字段时发出警告。如果该字段将不再使用且你也不希望有新的用户使用它,那么可以考虑使用保留语句替换字段声明。

int32 old_field = 6 [deprecated=true]

自定义选项

Protocol buffer也允许你定义并使用自定义的选项。这是大多数人用不到的高级功能。如果你真的想创建自定义选项,详见Proto2 语言指南。注意,用扩展来创建自定义选项,这是proto3中唯一允许使用的自定义选项。

编译生成

要从.proto文件中包括你定义的消息类型的Java、Python、C++、Go、Ruby、Objective-C或C#代码,你需要允许protocol buffer编译器protoc。如果你还没安装编译器,可从这里下载并根据README编译安装。对于Go,你还需要安装特定的生成插件:在这里你可以找到它。

Protocol编译器使用如下:

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选项指明,它们将按顺序检索。--proto_path可简写为-I=IMPORT_PATH
  • 你可以提供一个或多个输出命令:

    作为额外的便利,如果DST_DIR.zip.jar,编译器将生成指定名称的ZIP格式的压缩包。.jar输出还将根据Java JAR规范的要求提供一个清单文件。注意如果输出文件已存在,那么它将被重写,编译器并不会生成一个新的副本。

  • 你必须提供一个或者多个.proto文件作为输入。多个.proto文件可以一次指定。虽然这些文件是相对于当前目录命名的,但是每个文件必须驻留在IMPORT_PATH中的一个,以便编译器可以确定它的规范名称。

声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin92
Github: mengbin92
cnblogs: 恋水无意