[译]Protocol Buffer Language Guide (proto3)

文章目录

原文链接:Language Guide (proto3) | Protocol Buffers | Google Developers

本指导文档描述了如何使用protocol buffer语言来构建你的protocol buffer数据,包括.proto文件的语法规则,包括如何从你的.proto文件中生成数据访问类。本文介绍的是proto3版本的语言规则,关于proto2版本的信息,请参考: Proto2 Language Guide

本文是一个指导性的参考文档,关于如何使用本文中提及的特性的例子,请参考tutorial 并选择你想要的特定编程语言的例子。

定义一个消息类型

首先让我们来看一个非常简单的例子。比如说你想要定义一个搜索请求这样一个消息,这个消息里包含一个查询字符串(query string)、你所感兴趣页面的数量以及每个页面包含的结果。如下是你的.proto文件定义:

1syntax = "proto3";
2
3message SearchRequest {
4  string query = 1;
5  int32 page_number = 2;
6  int32 result_per_page = 3;
7}
  • 第一行说明你使用的是proto3版本的语言规则。如果没有这一行,那么protocol buffer编译器会认为你是使用proto2 版本的语言规则。syntax定义必须是在.proto文件的第一行

  • SearchRequest包含了三个字段(键值对),每个字段都有一个名字和对应的数据类型

指定字段类型

上面的例子中,三个字段都是标量类型(相对与复合类型,比如struct而言):两个整型变量(page_numberresult_per_page)和一个字符串变量(query)。当然,你可以定义你的字段为复合类型,比如枚举和其他消息类型。

分配字段号码

正如你所见,消息定义中的每一个字段都一个唯一的号码。这些字段号码是在二进制消息格式中(message binary format)的标识,一旦你开始使用你定义的消息,这些字段号码都不应该再改变。注意,字段号码为1到15的字段在编码的时候占用一个字节(包括字段号码和字段类型,关于这点的更多细节,请参看:Protocol Buffer Encoding)。字段号码从16到2047的字段会占用两个字节。因此你应该将1到15范围的号码留给那些在消息里经常用到的字段。并且要考虑预留一些该范围的号码,以便将来扩展你的消息。

你可以使用的最小字段号码是1,最大字段229 - 1或者536,870,911。你不可以使用字段号码为19000到19999的部分(FieldDescriptor::kFirstReservedNumberFieldDescriptor::kLastReservedNumber),这些是为protocol buffer本身的实现所保留的 - 如果你在.proto文件中使用了这些保留范围内的字段号码,编译器报错。同样的,如果你使用了那些被标记为reserved的字段号码,编译器也会报错。

指定字段规则

消息的字段可以是下面两种形式:

  • 单数形式:一个良好组织的消息可以有零个或者一个这种字段(不能超过一个)。这是proto3版本的默认字段规则。(个人理解,这里不是说只能定义零个或者一个这种单数形式的字段,而是说定义为单数形式的字段里最多包含一个元素。这个要对应repeated来理解)
  • repeated:这种字段对应的元素在一个消息里可以重复任意多次。repeated中的元素的顺序是固定的。

proto3中,标量类型的repeated字段是默认是使用packed来编码的。你可以从Protocol Buffer Encoding中了解更多关于packed编码的信息。

添加更多的消息类型

在一个.proto文件中你可以定义多种不同的消息类型,如果你要定义多个相关的消息,这就非常有用 - 比如你想要定义和SearchRequest相对应的SearchResponse消息,你可以在同一个.proto文件中添加:

1message SearchRequest {
2  string query = 1;
3  int32 page_number = 2;
4  int32 result_per_page = 3;
5}
6
7message SearchResponse {
8 ...
9}

添加注释

.proto中添加注释,你可以使用C/C++风格的语法:

1/* SearchRequest represents a search query, with pagination options to
2 * indicate which results to include in the response. */
3
4message SearchRequest {
5  string query = 1;
6  int32 page_number = 2;  // Which page number do we want?
7  int32 result_per_page = 3;  // Number of results to return per page.
8}

保留字段

如果你更新息类型的时候删除了一个字段,或者把一个字段注释掉,将来其他人在更新相同的消息的时候是可以重用之前字段的号码。之后在他们使用老版本的.proto时将会导致许多问题,包括数据出错,privacy bugs等。为了确保这些问题不会发生,有一个办法是把那些你删除的字段的使用过的字段号码(和/或名称)标记为reserved。如果你使用了这些被reserved标记的号码,编译器会报错。

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

注意,你不可以在同一个reserved语句中同时制定字段号码和字段名称。

.proto文件中生成的代码

当你编译.proto文件时,编译器会生成你指定的语言代码来和你定义在.proto文件中消息交互,包括访问和设置字段的值、序列化你的消息问输出流以及从输入流中解析为消息。

  • **C++**语言:生成.h.cc文件,每个消息都会生成一个对应的类
  • Java语言,每个消息都会生成一个包含对应类的.java文件,以及创建对应类实例的Builder
  • Kotlin语言,除了和Java语言生成的一样的代码以外,还会生成一个.kt文件包含一个用例创建类型实例的DSL
  • Pytyon语言少许不同,生成一个包含了每个消息对应的静态描述器的模块,这些被用来在运行时创建必要的Python数据访问类
  • Go语言,生成一个包含所有消息的.pb.go代码文件
  • Ruby语言,生成一个包含所有消息的.rb代码文件
  • Object-C语言,生成一个probjc.hprojc.m文件,每一个消息对应了一个类
  • **C#**语言,生成一个.cs文件,每一个消息对应了一个类
  • Dart,生成一个.pb.dart文件,每一个消息对应了一个类

关于特定语言的API的使用,你可以参考:API reference

标量类型

标量类型即下表中的一种类型。下表展示了.proto中支持的标量类型和特定语言标量类型的对应关系(原文有更多的编程语言的对应关系,译文中我只选取我关心的语言☺):

.proto TypeNotesC++ TypeJava/Kotlin Type[1]Python Type[3]Go TypeC# Type
doubledoubledoublefloatfloat64double
floatfloatfloatfloatfloat32float
int32Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead.int32intintint32int
int64Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead.int64longint/long[4]int64long
uint32Uses variable-length encoding.uint32int[2]int/long[4]uint32uint
uint64Uses variable-length encoding.uint64long[2]int/long[4]uint64ulong
sint32Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s.int32intintint32int
sint64Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s.int64longint/long[4]int64long
fixed32Always four bytes. More efficient than uint32 if values are often greater than 228.uint32int[2]int/long[4]uint32uint
fixed64Always eight bytes. More efficient than uint64 if values are often greater than 256.uint64long[2]int/long[4]uint64ulong
sfixed32Always four bytes.int32intintint32int
sfixed64Always eight bytes.int64longint/long[4]int64long
boolboolbooleanboolboolbool
stringA string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232.stringStringstr/unicode[5]stringstring
bytesMay contain any arbitrary sequence of bytes no longer than 232.stringByteStringstr[]byteByteString

你可以从Protocol Buffer Encoding了解更多关于消息序列化时的编码相关的信息。

默认值

解析消息的时候,如果原先经过编码的消息里没有包含特定的单数形式的元素(原文很拗口,个人觉得这里就是说从编码的消息里解析的时候找不到特定的字段),那些这些字段就被赋值成对应类型的默认值,规则如下:

  • strings类型的默认值是空字符串
  • bytes类型的默认值是空字节
  • bools类型的默认值是false
  • 数字类型的默认值是0
  • 枚举类型类型的默认值是枚举的第一个元素,也就是枚举值为0的元素
  • 如果字段类型是其他的消息,那么其默认值跟特定的语言相关,详见:generated code guide

repeated类型的字段的默认值一般是对应语言里的空列表。

对于标量类型的字段,实际上消息解析以后是没办法确定字段的值是人为设置的还是由于根本就没有赋值,而后解析的时候被解析成了默认值 - 你在定义消息的时候需要牢记这一点。例如说,不要定义一个boolean类型的开关是的当值为false的时候执行某些动作(如果你不希望这个动作默认是要执行的,因为boolean类型的默认值就是false)。同时还需要注意,当一个标量字段被设置为默认值的时候,值是不会被序列化的。

枚举

当你定义你的消息的时候,你可能希望某些字段的值只能从一些预先定义好的值里来。例如,你想为你的SearchRequest添加一个corpus字段,这个字段的值可以是UNIVERSAL, WEB, IMAGES, LOCAL, NEWS, PRODUCTS 或者 VIDEO。你可以通过定义枚举类满足你的需求,枚举中所有可能的值都是一个常量。

下面的例子中我们添加了一个名为Corpus的枚举,这个枚举中定义了它可能的值,同时也定义类型为Corpus的字段:

 1message SearchRequest {
 2  string query = 1;
 3  int32 page_number = 2;
 4  int32 result_per_page = 3;
 5  enum Corpus {
 6    UNIVERSAL = 0;
 7    WEB = 1;
 8    IMAGES = 2;
 9    LOCAL = 3;
10    NEWS = 4;
11    PRODUCTS = 5;
12    VIDEO = 6;
13  }
14  Corpus corpus = 4;
15}

如你所见,枚举Corpus中的第一个值UNIVERSAL是常量0:实际上所有的枚举的第一个常量值都应该是0,这是因为:

  • 一定得有0值,这样枚举可以当成数字类型,默认值就是0
  • 0值作为枚举的第一个元素,这个和proto2的语言兼容,即第一个元素的值总是默认值

你可以通过给不同的枚举元素赋相同的值来定义别名,前提是你需要把allow_alias选项设置为true,否者编译器遇到这样的别名设置会报错。

 1message MyMessage1 {
 2  enum EnumAllowingAlias {
 3    option allow_alias = true;
 4    UNKNOWN = 0;
 5    STARTED = 1;
 6    RUNNING = 1;
 7  }
 8}
 9message MyMessage2 {
10  enum EnumNotAllowingAlias {
11    UNKNOWN = 0;
12    STARTED = 1;
13    // RUNNING = 1;  // Uncommenting this line will cause a compile error inside Google and a warning message outside.
14  }
15}

枚举元素的值不能超过32位整型的范围。由于枚举值是使用varint encoding 编码,负值是无效的因此也不建议使用负值。你可以在一个消息内部定义枚举(如上面的例子所示)或者外部定义,然后可以在.proto中定义的任意消息里使用内部或者外部定义的枚举。你可以像使用消息里的字段一样来使用定义在一个消息内部的枚举:_MessageType_._EnumType_

.proto中定义的enum,对应生成的代码中,比如Java,Kotlin,C++也会有对应的enum,Pyton语言是EnumDescriptor类。

注意: 对于枚举类型的值的个数限制,这个是和编程语言本身相关,关于这个限制请查看特定的语言说明。

在反序列化的时候,对于无法识别的枚举值,也会在消息中保留下来,至于保留下来以何种方式展现,这个就和语言相关了。对于那些允许枚举值越界的变成语言,比如C++和Go,这些未知的枚举值就是被简单的以整型形式保留。对于不允许枚举值越界的语言,比如Java,可以通过特定的访问器来访问底层的整型值。不管何种情况,消息序列化的时候,会把无法识别的枚举值包含进去。

更多关于如何使用enum的信息,请参考:generated code guide中特定编程语言的部分。

枚举保留值

如果你更新消息类型的时候删除了一个枚举项或者注释掉,后面其他人在更新这个枚举的时候可以重用原枚举项对应的数值。之后在他们使用老版本的.proto时将会导致许多问题,包括数据出错,privacy bugs等。为了确保这些问题不会发生,有一个办法是把那些你删除的枚举项的使用过的数值(和/或名称)标记为reserved。如果你使用了这些被reserved标记的号码,编译器会报错。你可以指定你保留的枚举数值的范围,你可以使用max关键字来指定范围的最大值。

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

注意,你不可以在同一个reserved语句中同时指定枚举项的名称和枚举值。

使用其他消息类型

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

1message SearchResponse {
2  repeated Result results = 1;
3}
4
5message Result {
6  string url = 1;
7  string title = 2;
8  repeated string snippets = 3;
9}

导入定义

注意,这个特性不适用于Java

在上面的例子中,Result是和SearchResponse位于相同的.proto文件中 - 如果你想要使用的消息类型是定义在其他.proto文件中呢?

你可以通过导入.proto文件来使用定义在其他.proto文件中的消息类型。你可以在文件头部添加import语句来导入:

1import "myproject/other_protos.proto";

默认情况下,你只能使用直接导入的.proto文件中定义的消息类型。但是,有时候你可能需要把.proto文件移动到一个新的位置。你可以直接移动.proto文件到一个新的位置,然后然后更新所有的import语句指向.proto的新位置,你现在也可以在原来的位置放一个”假“的.proto然后使用import public语句把老的.proto中的import语句重定向到新的位置。任何导入了包含import public.proto文件也就把import public的依赖也导入了。例如:

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

protocol buffer编译器从命令行参数-I/--proto_path指定的一系列路径中查找导入的.proto文件。如果这个参数没有指定一个值,编译器就从运行的目录里查找。通常地,你应该把--proto_path的值设置为你项目的根目录,然后在import语句中使用完整的名称。

使用proto2的消息类型

proto3的消息中导入并使用proto2的消息类型是可以的,相反也是可以。但是proto2的枚举是无法在proto3中使用(如果proto2的消息体中使用proto2枚举是可以的)

嵌套类型

你可以在一个消息类型里定义嵌套的消息类型并使用它,如下面的例子所示 - Result消息是定义在SearchResponse里的:

1message SearchResponse {
2  message Result {
3    string url = 1;
4    string title = 2;
5    repeated string snippets = 3;
6  }
7  repeated Result results = 1;
8}

如果你想在定义嵌套消息的父消息外使用,你可以通过_Parent_._Type_来使用:

1message SomeOtherMessage {
2  SearchResponse.Result result = 1;
3}

你可以多层嵌套消息,如下所示:

 1message Outer {                  // Level 0
 2  message MiddleAA {  // Level 1
 3    message Inner {   // Level 2
 4      int64 ival = 1;
 5      bool  booly = 2;
 6    }
 7  }
 8  message MiddleBB {  // Level 1
 9    message Inner {   // Level 2
10      int32 ival = 1;
11      bool  booly = 2;
12    }
13  }
14}

更新消息类型

如果当前的消息已经不能满足你的需要,例如你希望你的消息里添加一个额外的字段,但是你仍然希望使用基于老的格式生成的代码。不用担心,更新消息类型而不破坏现有的代码是很简单的。你只需要记住以下规则:

  • 不要更改现有字段的字段号码
  • 如果你添加了新的字段,那些通过老的代码序列化出来的消息仍然可以被新的代码解析。你要牢记默认值 default values规则,这样才能恰当地和老代码生成的消息交互。同样的,新代码创建的消息也是可以被老代码解析:老代码程序解析的时候只是简单地忽略新加的字段。你可以参考未知字段获取更多的细节
  • 字段可以被删除,但是要确保这些要删除的字段对应的字段号码不会在消息中再次被使用。你或许可以对字段重命名,比如添加"OBSOLETE_"这样的前缀,或者把字段号码标记为保留字段,这样将来更新你的.proto文件的时候就不会意外的使用那些号码
  • int32,uint32,int64,uint64bool是相互兼容的 - 这意味着你从其中的一种类型改为另一种类型不会破坏向前或者向后的兼容性。如果一个解析一个数字的时候和目标类型不匹配,会发生和在C++中转化数字类型一样的结果,例如一个64位的数字读取为32位的时候,会被截断为32位的数字
  • sint32sint64兼容,但是和其他整型类型不兼容
  • stringbytes兼容,只要bytes是有效的UTF-8格式
  • 嵌套的消息和bytes兼容,只要bytes包含编码过的消息
  • fixed32sfixed32fixed64以及sfixed64兼容
  • 对于stringbyte和消息字段,optionalrepeated兼容。如果一个repeated类型字段的序列化数据作为输入,对于期望这个字段为optional的客户端来说,如果这个字段是基本类型,那么会获取最后一个输入的值;如果这个字段是一个消息类型,那么会把消息里的所有元素合并(这个有点拗口,不知道该怎么理解)。需要注意的是,对于数值类型(包括bools和enums)来说这是不安全的。数值类型的repeated字段可以序列化为packed 格式,如果客户端期望的是optional字段,那么不能被正确解析
  • enumint32uint32int64uint64兼容(注意如果类型不完全匹配,数据可能被截断)。然而需要注意的是,客户端的代码在反序列的时候可能会有不同的行为,比如未识别的proto3 enum类型的值会在消息中保留下来,这种行为是和语言本身相关。整型的字段的值总是保持了原来的值
  • 把一个字段的类型改为oneof的成员是安全的且二进制上兼容。把多个字段变成oneof或许是安全的,但是你要确保设置的代码只能有一次(这里也不理解是啥意思)。把任何字段移到已存在的oneof里是不安全的

未知字段

未知字段是指protocol buffer序列化数据的时候被良好的组织的数据,但是反序列化的时候解析器不认识的字段。例如,对于老代码程序解析由新代码程序序列化的带有新添加的字段的数据时,那些新添加的字段对于老代码程序就变成了未知字段。

原先proto3消息在解析的时候总是丢弃未知字段,但是在版本3.5我们重新引入了对未知字段的保留以匹配proto2的行为。在版本3.5以及之后未知字段在解析和序列化的时候又重新保留了下来。

Any

Any消息类型能让你像使用嵌套类型一样来使用一个消息而不需要有.proto的定义。一个Any消息包含一个序列化为bytes的消息以及用来解释成对应消息类型的一个全局唯一标识符URL。你需要导入google/protobuf/any.proto来使用Any类型。(注:说白了,这个Any类型就是一种序列化,把一种特定类型按照某种方式序列化为字节数组)

1import "google/protobuf/any.proto";
2
3message ErrorStatus {
4  string message = 1;
5  repeated google.protobuf.Any details = 2;
6}

默认的类型URL是type.googleapis.com/_packagename_._messagename_

不同的语言实现会提供类型安全的运行时库来打包和解包Any类型的值 - 例如在Java中,Any类型提供了pack()unpack()访问器,在C++中提供了PackFrom()UnpackTo()方法:

 1// Storing an arbitrary message type in Any.
 2NetworkErrorDetails details = ...;
 3ErrorStatus status;
 4status.add_details()->PackFrom(details);
 5
 6// Reading an arbitrary message from Any.
 7ErrorStatus status = ...;
 8for (const Any& detail : status.details()) {
 9  if (detail.Is<NetworkErrorDetails>()) {
10    NetworkErrorDetails network_error;
11    detail.UnpackTo(&network_error);
12    ... processing network_error ...
13  }
14}

当前用于和Any类型交互的运行时库还在开发中

如果你熟悉 proto2 语法,和proto2消息的允许extensions一样,Any可以用来存放proto3的消息。

Oneof

如果你的一个消息有多个字段,并且同一时刻最多只有一个字段被赋值,你可以使用oneof特性来强制这种行为和节省内存。

oneof字段和常规字段一样,只是oneof里所有的成员是共享内存的,且同一时刻最多只有一个字段被赋值。设置任何一个oneof里的成员将自动清空其他的成员。你可以一个特殊的case()或者WhichOneof()方法(取决特定的语言)来判断oneof里的成员是否被赋值了。

使用oneof

你可以使用oneof关键字跟上你的oneof名称来定义一个oneof,比如在test_oneof中:

1message SampleMessage {
2  oneof test_oneof {
3    string name = 4;
4    SubMessage sub_message = 9;
5  }
6}

然后你可以添加oneof成员到它的定义中。你可以添加除了maprepeated类型的成员到oneof定义里。在你生成的代码中,oneof有着和常规字段一样的读写访问器。你还会有一个方法(取决特定的语言)来判断oneof里的成员是否被赋值了。请参考特定语言的API reference以了解更多。

oneof特性

  • 设置任何一个oneof里的成员将自动清空其他的成员。如果你设置了多个成员,只有最后一个设置的成员才有值。

    1SampleMessage message;
    2message.set_name("name");
    3CHECK(message.has_name());
    4message.mutable_sub_message();   // Will clear name field.
    5CHECK(!message.has_name());
    
  • 解析的时候如果遇到来自同一个oneof的多个成员,只有最后一次看到的成员才会被解析

  • oneof不用标识成repeat

  • 反射API可以访问oneof字段

  • 如果oneof字段设置为默认值(比如int32类型的oneof成员设置为0),case方法就会判定这个成员被设置了,这个值会被序列化

  • 如果你使用C++,确保你的代码不会引发内存泄漏。下面的代码会奔溃原因是因为sub_message在调用了set_name()后就被删除了

    1SampleMessage message;
    2SubMessage* sub_message = message.mutable_sub_message();
    3message.set_name("name");      // Will delete sub_message
    4sub_message->set_...            // Crashes here
    
  • 还是C++,Swap()两个oneof消息后,每个消息的oneof字段发生互换。在下面的例子中,msg1将会有sub_message成员而msg2将会有name成员:

    1SampleMessage msg1;
    2msg1.set_name("name");
    3SampleMessage msg2;
    4msg2.mutable_sub_message();
    5msg1.swap(&msg2);
    6CHECK(msg1.has_sub_message());
    7CHECK(msg2.has_name());
    
    1SampleMessage msg1;
    2msg1.set_name("name");
    3SampleMessage msg2;
    4msg2.mutable_sub_message();
    5msg1.swap(&msg2);
    6CHECK(msg1.has_sub_message());
    7CHECK(msg2.has_name());
    

向后兼容的问题

新增或者移除oneof字段的是要小心 - 在检查oneof的成员是否被赋值的时候返回None/NOT_SET,这可能意味着oneof成员没有被赋值或者某个成员的赋值是基于不同版本的oneof字段。这其中的区别无从得知,因为无法确定未知字段是否是oneof的成员。

标签复用问题:

  • oneof中移出成员或者添加成员到oneof:在消息序列化和解析后,你可能会丢失一些信息(比如一些字段被清空)。但是你可以安全的把一个字段移到一个新的oneof里,或者把多个字段移到也可以,前提是你知道只能有一个字段被赋值
  • **删除一个成员或者重新添加回去:**这个可能会知道消息序列化和解析后成员的赋值丢失
  • **分离或者合并oneof成员:**这和移动常规的字段有一样的问题

Maps

如果你希望在你的数据定义中创建map,protocol buffer提供了一个方便的语法:

1map<key_type, value_type> map_field = N;

key_type可以是任何整型或者字符串类型(也就是说除了浮点和bytes之外的标量类型)。因此,如果你希望创建一个键为字符串类型值为Project消息的map,你可以定义如下:

1map<string, Project> projects = 3;
  • map类型的字段是不可以定义为repeated
  • Wire格式的顺序不固定,迭代map也是无序的,因此你不能依赖map为你提供有序的成员
  • .proto生成text格式的时候,maps是按照键排序的。数值类型的键是根据数值排序(这是啥意思,是不是和编码有关系?
  • 如果你只给定了一个键,对应的值没有赋值,这种情况在序列化的时候不同的语言有不同的行为 - C++,Java,Kotlin和Python中会用默认值来序列化,其他语言中这种情况不会被序列化

访问map的API目前已对proto3支持。你可以从 API reference中查找特定语言相关的说明。

向后兼容

map的语法和下面的定义等价,因此对那些不支持map的protocol buffer实现依然可以处理你的数据。

1message MapFieldEntry {
2  key_type key = 1;
3  value_type value = 2;
4}
5
6repeated MapFieldEntry map_field = N;

任何支持map的protocol buffer的实现必须能够基于上面的定义生成和接受数据。

你可以在.proto中添加一个可选的package指示符,这样可以防止不同的消息类型间名字冲突。

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

你可以在定义你的消息类型的时候使用package指示符:

1message Foo {
2  ...
3  foo.bar.Open open = 1;
4  ...
5}

package指示符是如何起作用的,这个取决于你选择的编程语言:

  • **C++**中,生成的类被包含在C++命名空间里。例如,Open会在命名空间foo::bar
  • JavaKotlin中,package和Java的包一样,除非你自己显式的在.proto里指定了option java_package
  • Python中,package会被忽略,因为Python modules是按照文件位置来组织的
  • Go中,package和Go的包一样,除非你自己显式的在.proto里指定了option go_package
  • **C#**中,package的名称先被转变成PascalCase作为命名空间的名称,除非你自己显式的在.proto里指定了option csharp_namespace。例如,Open会放在命名空间Foo.Bar

包和名字解析

在protocol buffer中类型名称的解析就跟C++中类似:从最内部的范围里查找,然后次之以此类推(任何一个包都被当成父包的内部包)。'.'意味着从最外层开始(比如:.foo.bar.Baz

protocol buffer编译器通过解析导入的.proto文件来解析所有的类型名称。即使不同语言有不同的范围规则,不同语言的代码生成器知道如何找到特定的类型。

定义服务

如果你希望在你的RPC服务中使用你定义的消息,你可以在.proto文件中定义RPC服务接口,而后protocol buffer会生成特定语言的服务接口代码和mock实现。因此,假如你需要定义个RPC服务的方法接受SearchRequest参数返回SearchResponse,你可以按如下方式定义:

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

gRPC是最直接使用protocol buffer的RPC系统:Google开发的一个语言和平台中立的开源的RPC系统。gRPC能非常好的和protocol buffer工作,使用一个特殊的protocol buffer插件能完美的生成gRPC代码。

如果你不使用gRPC,也是可以在你自己的RPC实现中使用protocol buffers,你可以参考:Proto2 Language Guide

有许多基于protocol buffers的RPC项目正在开发中,这里有一个我们所知道的项目的列表:third-party add-ons wiki page

Json Mapping

proto3支持通用的Json编码,这使得它可以很方便地跨系统共享数据。如下表所示,Json编译是按照type-by-type的方式进行。

如果在Json编码的数据中缺少值,或者值是null,那么在解析成protocol buffre数据的时候将被解释成默认值。如果protocol buffer编码的字段的值是对应类型的默认值,那么编码成Json的时候,为了节省空间这些值会被忽略。一种实现或许可以提供选项给用户使得把默认值也包括在Json编码的数据中。

proto3JSONJSON exampleNotes
messageobject{"fooBar": v, "g": null, …}Generates JSON objects. Message field names are mapped to lowerCamelCase and become JSON object keys. If the json_name field option is specified, the specified value will be used as the key instead. Parsers accept both the lowerCamelCase name (or the one specified by the json_name option) and the original proto field name. null is an accepted value for all field types and treated as the default value of the corresponding field type.
enumstring"FOO_BAR"The name of the enum value as specified in proto is used. Parsers accept both enum names and integer values.
map<K,V>object{"k": v, …}All keys are converted to strings.
repeated Varray[v, …]null is accepted as the empty list [].
booltrue, falsetrue, false
stringstring"Hello World!"
bytesbase64 string"YWJjMTIzIT8kKiYoKSctPUB+"JSON value will be the data encoded as a string using standard base64 encoding with paddings. Either standard or URL-safe base64 encoding with/without paddings are accepted.
int32, fixed32, uint32number1, -10, 0JSON value will be a decimal number. Either numbers or strings are accepted.
int64, fixed64, uint64string"1", "-10"JSON value will be a decimal string. Either numbers or strings are accepted.
float, doublenumber1.1, -10.0, 0, "NaN", "Infinity"JSON value will be a number or one of the special string values "NaN", "Infinity", and "-Infinity". Either numbers or strings are accepted. Exponent notation is also accepted. -0 is considered equivalent to 0.
Anyobject{"@type": "url", "f": v, … }If the Any contains a value that has a special JSON mapping, it will be converted as follows: {"@type": xxx, "value": yyy}. Otherwise, the value will be converted into a JSON object, and the "@type" field will be inserted to indicate the actual data type.
Timestampstring"1972-01-01T10:00:20.021Z"Uses RFC 3339, where generated output will always be Z-normalized and uses 0, 3, 6 or 9 fractional digits. Offsets other than "Z" are also accepted.
Durationstring"1.000340012s", "1s"Generated output always contains 0, 3, 6, or 9 fractional digits, depending on required precision, followed by the suffix "s". Accepted are any fractional digits (also none) as long as they fit into nano-seconds precision and the suffix "s" is required.
Structobject{ … }Any JSON object. See struct.proto.
Wrapper typesvarious types2, "2", "foo", true, "true", null, 0, …Wrappers use the same representation in JSON as the wrapped primitive type, except that null is allowed and preserved during data conversion and transfer.
FieldMaskstring"f.fooBar,h"See field_mask.proto.
ListValuearray[foo, bar, …]
ValuevalueAny JSON value. Check google.protobuf.Value for details.
NullValuenullJSON null
Emptyobject{}An empty JSON object

JSON 选项

proto3的JSON实现可能提供以下选项(这下面说的选项原文也没有说明是什么以及怎么使用):

  • **包含默认值在JSON编码中:**默认情况下,具有默认值的字段在编码成JSON的时候其值被忽略。一种实现或许可以提供一个选项来覆盖这种默认行为使得把默认值包含进去
  • **忽略未知字段:**Proto3 JSON解析器默认会拒绝未知的字段,但是可能可以提供一个选项在解析的时候忽略未知字段
  • **使用字段名称来代替lowerCamelCase格式的:**默认情况,proto3 JSON printer会把字段名称转换为lowerCamelCase格式作为Json名称。一种实现可能可以提供一个选项来直接使用字段名称作为Json名称。proto3 JSON解析器应该都要接受字段名称和lowerCamelCase格式的名称
  • **把枚举解释成值而不是枚举项:**默认情况下,枚举项被解释成Json。一种实现可能可以提供一个选项来直接使用枚举值还是枚举项(枚举项就是枚举名称,枚举值就是枚举项对应的数值)

选项option

.proto文件中的声明可以用一些列的option来标记。option不会改变这些声明的整体的含义,但是可能会按照某种方式影响这些声明被处理的方式。完整可用的option是定义在google/protobuf/descriptor.proto中。

有些option是文件级别的,意味着他们应该放在作用范围的顶层而不是放在任何一个消息、枚举或者服务的定义里。有些option是消息级别的,意味着这些应该放在消息里面。有些则是字段级别的,应该放在字段的定义里。除此之外,还可以放在enum类型、enum值、oneof字段、服务类型和服务方法里,只是目前还有有用的option作用这以上这些(那原文说这个干啥?!)。

(原文中列举了一些最重要的option适用于Java,C++等我不使用的语言我就没有放进来,偷个懒省点翻译的时间)

自定义Option

Protocol Buffers允许你自定义选项然后使用。这是一个大多数人都不需要用到的高级选项。如果你确实觉得你需要这些自定义选项,你可以参考Proto2 Language Guide。注意,创建自定义选项需要扩展extensions的支持,这些扩展只能用于在proto3中自定义选项。

生成你的类

你需要运行Protocol buffer编译器.protoc来编译你的.proto文件来生成和消息交互的Java/Kotlin/Python/C++/Go/Ruby/Objective-C/C# 代码。如果你还没哟uanzhuang编译器,请先下载然后参考README的说明来安装。对Go语言,你需要安装一个特定的代码生成器插件:你可以从golang/protobuf 找到这个插件以及这个插件的安装说明。

Protocol编译器是按照以下的方式来调用:

1protoc --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指定了从什么位置导入.proto文件。如果没有指定,默认从当前位置导入。如果有多个导入位置,可以多次指定--proto_path并设置路径,这些路径将被按顺序查找和导入。-I=_IMPORT_PATH_--proto_path的简短形式。

  • 你可以提供以下一个或者多个导出目录(这里我只列举了生成Go语言的目录,其他语言请参考原文)

  • 你必须指定一个或者多个.proto文件作为输入。可以同时指定多个.proto文件,前提是这些文件都位于IMPORT_PATH,这样编译器才能根据名字找到这些文件。