当前位置: 首页 > 工具软件 > Proto.Menu > 使用案例 >

Proto3使用指南

井修雅
2023-12-01

这篇指南讲述如何使用Protocol Buffers来结构化你的Protocol Buffer数据,包括.proto文件语法以及如何从.proto文件生成你的访问类型。本文主要涵盖了proto3的语法,proto2的语法参见Proto2 Language Guide

这是一篇参考教程 -- 本文中诸多功能的分步示例,详见tutorial

目录

  • 定义消息类型
  • 标量类型
  • 默认值
  • 枚举
  • 使用其他消息类型
  • 嵌套类型
  • 更新消息类型
  • 未知字段
  • Any
  • Oneof
  • Maps
  • 定义服务
  • Json Mapping
  • 可选项
  • 编译生成

定义消息类型

首先来看一个非常简单的例子。假设你想定义一个搜素请求的消息格式,其中每个搜索请求都包含一个检索字段、特定的结果页(你感兴趣的结果所在的页面)以及每个页面的结果数量。你可以使用下面的.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,最大为229−1229−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][2]Go类型Ruby类型C#类型PHP类型Dart类型
double doubledoublefloatfloat64Floatdoublefloatdouble
float floatfloatfloatfloat32Folatfloatfloatdouble
int32使用可变长度编码。编码负数低效,如果字段可能有负数,使用sint32代替。int32intintint32Fixnum or Bignum(as required)intintegerint
int64使用可变长度编码。编码负数低效,如果字段可能有负数,使用sint64代替。int64longint/long[3][3]int64Bignumlonginteger/string[5][5]int64
uint32使用可变长度编码。uint32int[1][1]int/long[3][3]uint32Fixnum or Bignum(as required)uintintegerint
uint64使用可变长度编码。uint64long[1][1]int/long[3][3]uint64Bignumulonginteger/string[5][5]}$int64
sint32使用可变长度编码。有符号整数。编码负数比普通int32高效。int32intintint32Fixnum or Bignum(as required)intintegerint
sint64使用可变长度编码。有符号整数。编码负数比普通int64高效。int64longint/long[3][3]int64Bignumlonginteger/string[5][5]int64
fixed324字节。如果变量大于22828比uint32高效。uint32int[1][1]intint32Fixnum or Bignum(as required)intintegerint
fixed648字节。如果变量大于25656比uint64高效。uint64long[1][1]int/long[3][3]uint64Bignumulonginteger/string[5][5]int64
sfixed324字节uint32int[1][1]intint32Fixnum or Bignum(as required)intintegerint
sfixed648字节uint64long[1][1]int/long[3][3]uint64Bignumulonginteger/string[5][5]int64
bool boolbooleanboolboolTrueClass/FalseClassboolbooleanbool
string必须是UTF-8编码或7-bit的ASCII文本,长度不能大于23232stringStringstr/unicode[4][4]stringString(UTF-8)stringstringString
bytes可以包含任何长度不超过232的字节序列stringByteStringstr[]byteString(ASCII-8BIT)ByteStringstringList<int>

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

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

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

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

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

[5][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编码输出中,实现可以提供带有默认字段的选项。

proto3JsonJson示例备注
messageobject{"fooBar":v,"g":null,_}生成Json对象。消息字段名称被映射为lowerCamelCase并成为Json对象的键。如果指定了json_name字段选项,则指定的值将被作为键使用。解析器既接受lowerCamelCase名称(或使用json_name指定的名称),也接受原生的proto字段名称。所有字段类型都可接受null,并被视为该类型的默认值。
enumstring"FOO_BAR"使用proto中指定的enum值名称。解析器既接受枚举名称,也接受整数值。
map<K,V>object{"K":v,_}所有的键都被转换成string。
repeated Varray[v, ...]null被当做空列表[]。
booltrue,falsetrue,false 
stringstring"Hello World!" 
bytesbase64 string"YWJjMTIzIT8kKiYoKSctPUB+"Json值会变成使用添加padding的标准base64编码的string。标准的或url安全的base64编码,带/不带padding也都可以接受。
int32,fixed32,uint32number1,-10,0Json值会变成十进制的数字。数字或string都可被接受。
int64,fixed64,uint64string"1","-10"Json值会变成十进制的string。数字或string都可被接受。
float,doublenumber1.1,-10.0,0,"NaN","Infinity"Json值会变成数字或"NaN"、"Infinity"、"-Infinity"其中之一。数字或string都可被接受。指数表示法也被接受。
Anyobject{"@type":"url","f":v,...}如果Any包含的值有特定的Json映射,它将被转换为如下格式:{"@type": xxx, "value": yyy}。否则,该值会被转换为Json对象,且”@type“字段会被插入以指示实际数据类型。
Timestampstring"1972-01-01T10:00:20.021Z"使用RFC 3339,其生成的输出总是Z-normalized后的,并使用0、3、6或9位小数。除“Z”以外的偏移量也可以接受。
Durationstring"1.000340012s","1s"根据所需的精度,生成的输出总是包含0、3、6或9位小数,跟后缀”s“。只要符合纳秒精度和后缀“s”的要求,任何小数(也可以没有)都可以接受。
Structobject{ ... }任意的Json对象。参见struc.proto
Wrapper typesvarious types2,"2","foo",true,"true",null,0,...包装器使用与包装的原始类型相同的JSON表示,但在数据转换和传输期间允许并保留null。
FieldMaskstring"f.fooBar,h"参见field_mask.proto
ListValuearray[foo,bar, ...] 
Valuevalue 任意的Json值
NullValuenull Json null
Emptyobject{}任意的空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中的一个,以便编译器可以确定它的规范名称。

 

转载自:https://www.cnblogs.com/lianshuiwuyi/p/12221913.html

 类似资料: