Protocol Buffers V3中文语法指南

发布时间 2023-04-16 16:47:01作者: 分类DHCP

https://blog.csdn.net/qq_45696377/article/details/128040153

https://protobuf.dev/programming-guides/

 

最近工作中需要用到 Google 家的 Protocol Buffers,抽空把官网的 proto3 指南翻译下学习一波。

proto3 语言指南

本指南描述如何使用 protocol buffer 语言来构造 protocol buffer 数据,包括 .proto文件语法以及如何生成 .proto文件的数据访问类。 涵盖了proto3版本: 有关proto2语法的信息,请参阅 Proto2 语言指南

这是一个参考指南——对于使用本文中描述的众多特性的分步示例,请参阅本教程 (目前只有 proto2,很快将有更多的 proto3 文档)。

定义一个 Message 类型

首先让我们看一个非常简单的例子。 假设想要定义一个搜索请求的 message 格式,其中每个搜索请求都有一个查询字符串、特定页面以及每个页面有多少条结果。 下述内容是用于定义 message 类型的 .proto 文件。

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
  • 文件的第一行规定正在使用proto3 语法: 如果不这样做, protocol buffer 编译器会认为正在使用proto2。而且必须是文件的第一个非空非注释行
  • 这个名为SearchRequest 的 message 定义了三个字段(键值对) ,每个字段包含了 message 中的一段数据。 每个字段都有一个名称和一个类型。

指定字段类型

在上面的示例中,所有字段都是 标量类型:两个整数(每页的页码和结果)和一个字符串(查询)。 但是,也可以为字段指定复合数据类型,包括枚举和其他 message 类型。

分配字段编号

message 定义中的每个字段都有一个唯一编号。 这些字段编号在message 的二进制格式中标识字段,并且在使用了 message 类型后应不能更改。 注意,范围 1 到 15 中的字段编号需要一个字节进行编码,包括字段编号和字段类型(可以在 Protocol Buffer 编码中找到更多相关信息)。 范围 16 到 2047 的字段编号需要两个字节。 因此,应该为经常出现的 message 元素保留数字 1 到 15。 记住,为将来可能添加的频繁出现的元素留出一些空间。

可以指定的最小字段编号是 1,最大的字段编号是 2^29-1,即 536,870,911。 也不能使用数字 19000 到 19999(FieldDescriptor: : kFirstReservedNumber 到 FieldDescriptor: kLastReservedNumber) ,因为它们是给 Protocol Buffers 实现保留的——如果你在你的.proto文件中用了保留编号,protocol buffer 编译器会报错。 同样,也不能使用任何之前的保留字段编号。

指定字段规则

message 字段可以是下列字段之一:

  • singular: 格式良好的 message 可以有零个或一个(但不能多于一个)的该字段。 这是 proto3 语法的默认字段规则。
  • repeated: 格式良好的 message 中重复该字段任意数(包括零次)。 重复值的顺序将被保留。

在 proto3 中,标量数值类型的reepeated字段默认使用packed编码。

可以在 Protocol Buffer 编码 中找到关于packed编码的更多信息。

添加更多 message 类型

Multiple message types can be defined in a single .proto file. This is useful if you are defining multiple related messages – so, for example, if you wanted to define the reply message format that corresponds to your SearchResponse message type, you could add it to the same .proto:

可以在单个.proto文件中定义多个 message 类型。 这在定义多个相关的 message 时非常有用——例如,如果想定义与 SearchResponse message 类型对应的应答 message 格式,可以将其添加到相同的.proto文件中:

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

message SearchResponse {
 ...
}

添加注释

要向. proto 文件添加注释,请使用 C/C++风格的// 和 /* ... */ 语法。

/* SearchRequest表示了一个搜索查询,带有分页选项
 * 指示响应结果中应包含的内容。 */

message SearchRequest {
  string query = 1;
  int32 page_number = 2;  // 想要第几页?
  int32 result_per_page = 3;  // 每页的结果条数
}

保留字段

如果通过完全删除字段或将其注释掉来更新message 类型,那么未来的使用者在对该类型进行更新时可能会重用字段编号。 如果以后加载旧版本的相同.proto文件时,可能会导致严重的问题,包括数据损坏,隐私漏洞等等。 确保这种情况不会发生的一种方法是保留已删除字段的编号(或名称,这也可能导致 JSON 序列化问题)。 如果将来有任何使用者尝试使用这些字段编号, protocol buffer 编译器将报错。

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}
注意,不能在同一个reserved语句中混合字段名和字段编号。

.proto文件生成了什么?

在编译.proto时,protocol buffer 编译器以你选择的语言生成代码,你需要使用该语言来处理文件中描述的 message 类型,包括获取和设置字段值,将 message 序列化为输出流以及将输入流解析为 message。

  • 对于C++ ,编译器从.proto生成一个.h 和 .cc文件,文件中描述的每种 message 类型都有一个类。
  • 对于Java,编译器生成一个.java 文件,每种 message 类型都有一个类,还有一个用于创建 message 类实例的特殊 Builder 类
  • Python 有一点不同—— Python 编译器会根据.proto中的每种 message 类型生成一个带 static 描述符的模块,然后将该模块与metaclass一起使用,以在运行时创建必要的 Python 数据访问类。
  • 对于Go, 编译器将会为文件中的每个 message 类型生成一个.pb.go 文件。
  • 对于 Ruby,编译器生成一个 .rb文件,其中有一个包含 message 类型 Ruby 模块。
  • 对于 Objective-C,编译器从每个.proto文件生成一个 pbobjc.h 和 pbobjc.m 文件,文件中描述的每种 message 类型都有一个类。
  • 对于C#,编译器从每个 .proto文件生成一个.cs文件,文件中每种 message 类型都有一个类。
  • 对于Dart,编译器生成一个.pb.dart文件 ,文件中每种 message 类型都有一个类。

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

标量值类型

标量 message 字段可以为以下类型之一。该表显示了.proto文件的类型,以及自动生成类中的对应类型:

.proto 类型说明C++类型Java 类型Pythonl 类型[2]Go 类型Ruby 类型C# 类型PHP 类型Dart 类型doubledoubledoublefloatfloat64Floatdoublefloatdoublefloatfloatfloatfloatfloat32Floatfloatfloatdoubleint32使用变长编码。 对负数进行编码效率不高——如果字段可能具有负值,则使用 sint32。int32intintint32Fixnum 或 Bignum (as required)intintegerintint64使用变长编码。 对负数进行编码效率不高——如果字段可能具有负值,则使用 sint64。int64longint/long[3]int64Bignumlonginteger/string[5]Int64uint32使用变长编码。uint32int[1]int/long[3]uint32Fixnum 或 Bignum (as required)uintintegerintuint64使用变长编码。uint64long[1]int/long[3]uint64Bignumulonginteger/string[5]Int64sint32使用变长编码。有符号的 int 值。与常规 int32 相比,它们更有效地编码负数。int32intintint32Fixnum 或 Bignum (as required)intintegerintsint64使用变长编码。有符号的 int 值。与常规 int64 相比,它们更有效地编码负数。int64longint/long[3]int64Bignumlonginteger/string[5]Int64fixed32总是 4 个字节。如果值通常大于 2^28,则比 uint32 更有效。uint32int[1]int/long[3]uint32Fixnum 或 Bignum (as required)uintintegerintfixed64总是 8 个字节。如果值通常大于 2^56,则比 uint64 更有效。uint64long[1]int/long[3]uint64Bignumulonginteger/string[5]Int64sfixed32总是 4 个字节。int32intintint32Fixnum 或 Bignum (as required)intintegerintsfixed64总是 8 个字节int64longint/long[3]int64Bignumlonginteger/string[5]Int64boolboolbooleanboolboolTrueClass/FalseClassboolbooleanboolstringstring 必须始终包含 UTF-8 编码的或 7 位 ASCII 文本,并且不能长于 2^32。stringStringstr/unicode[4]stringString (UTF-8)stringstringStringbytes可以包含不超过 2^32 的任意字节序列。stringByteStringstr[]byteString (ASCII-8BIT)ByteStringstringList

在使用 Protocol Buffer Encoding 序列化 message 时,可以了解有关这些类型如何编码的更多信息。

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

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

[3]64 位或无符号的 32 位整数在被解码时总是被表示为等长的,但是如果在设置字段时给出一个 int,则可以是一个 int。 在所有情况下,值必须与设置时表示的类型相匹配。 见[2]。

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

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

默认值

当解析 message 时,如果编码 message 不包含特定的 singular 元素,则解析对象中的相应字段将设置为该字段的默认值。 这些默认值是根据类型指定的:

  • 对于字符串,默认值为空字符串。
  • 对于字节,默认值为空字节。
  • 对于 bools,默认值为 false。
  • 对于数值类型,默认值为零。
  • 对于枚举类型, 默认值为第一个定义的枚举值, 那一定是 0。
  • 对于 message 的字段,未设置该字段。其确切值依赖于用什么语言。请参见代码生成指南了解详情

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

请注意,对于标量 message 字段,一旦 message 被解析,就无法判断字段是否显式设置为默认值(例如将布尔值设置为 false):在定义 message 类型时应该牢记这一点。例如,如果不希望默认情况下也发生这种行为,则在将布尔值设置为“ false”时,该布尔值不会开启某些行为。还要注意,如果将标量 message 字段设置为其默认值,则该值在过程中将不会被序列化。

有关代码的默认生成方式的更多详细信息,请参阅所选语言的 代码生成指南

枚举类型

在定义 message 类型时,可能希望其中一个字段只具有一个预定义的值列表。 例如,假设想为每个 SearchRequest 添加一个corpus字段,其中corpus可以是 UNIVERSAL、 WEB、 IMAGES、 LOCAL、 NEWS、 PRODUCTS 或 VIDEO。 可以通过在 message 定义中添加一个enum ,为每个可能的值添加一个常量来非常简单地完成这项工作。

在下面的例子中,添加了一个名为 Corpus 的enum ,包含所有可能的值,以及一个类型为 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 枚举的第一个常量映射为零:每个 enum 定义必须包含一个常量,该常量映射为零作为它的第一个元素。 这是因为:

  • 必须有一个零值,这样就可以将 0 作为一个数值默认值.
  • 零值必须是第一个元素,以便与proto2语义兼容,其中第一个枚举值总是默认值。

可以通过将相同的值分配给不同的枚举常量来定义别名。 为此,需要将 allow_alias 选项设置为 true,否则当发现别名时,protocol buffer 编译器将生成错误的 message。

message MyMessage1 {
  enum EnumAllowingAlias {
    option allow_alias = true;
    UNKNOWN = 0;
    STARTED = 1;
    RUNNING = 1;
  }
}
message MyMessage2 {
  enum EnumNotAllowingAlias {
    UNKNOWN = 0;
    STARTED = 1;
    // RUNNING = 1;  // 取消这一行的注释将导致 Google 内部编译错误和外部警告message。
  }
}

枚举数常数必须在 32 位整数的范围内。 由于enum 值在过程中使用 变长编码,负值效率低,因此不推荐使用。 可以在 message 定义中定义enum (如上面的例子所示) ,也可以在.proto文件的 message 定义中重用这些enum 。 还可以使用 _MessageType_._EnumType_. 语法将一条 message 中声明的enum 类型用作另一条不同的 message 中的字段类型。

对象运行 protocol buffer 编译器时。 如果 .proto使用enum,那么生成的代码将有一个对应于 Java 或 C++ 的enum ,对于 Python,会创建一个特殊 EnumDescriptor 类,用于在运行时生成的类中创建一组带有整数值的符号常量。

警告: 生成的代码可能受到特定语言的枚举数限制(低于千)。 请检查计划使用的语言的限制。

尽管当 message 被反序列化时,枚举值的表示方式依赖于语言,在过程中,无法识别的枚举值将被保留在 message 中。 在支持值超出指定符号范围(如 C++ 和 Go)的开放枚举类型的语言中,未知枚举值仅存储为其底层的整数表示形式。 在如 Java 这样具有封闭枚举类型的语言中,枚举中的一个实例用于表示一个无法识别的值,并且底层的整数可以通过特殊的访问器访问。 在这两种情况下,如果 message 被序列化,那么不可识别的值仍然会与 message 一起被序列化。

有关如何在应用程序中使用 messageenum的详细信息,请参阅为所选语言代码生成指南

保留值

如果通过完全删除 enum 条目或注释掉该类型来更新enum 类型,那么未来的使用者在对该类型进行更新时可能会重用该数值。如果加载旧版本的相同.proto文件, 会导致严重的问题,包括数据损坏,隐私漏洞等等。 确保不发生这种情况的一种方法是保留已删除条目的数值(或名称,这也可能导致 JSON 序列化问题)。 如果任何未来的使用者试图使用这些标识符,protocol buffer 编译器将报错。 可以使用 max 关键字指定保留的数值范围最大值。

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}
注意,不能在相同的reserved 语句中混合字段名和数值。

使用其他 message 类型

可以使用其他 message 类型作为字段类型。 例如,希望在每个 SearchResponse message 中包含 Result message——为此,可以在同一个 message 中定义 Result message 类型。 然后在 SearchResponse 中指定 Result类型的字段:

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}

导入定义

在上面的示例中,Result message 类型定义与 SearchResponse 在相同的文件中。如果要用作字段类型的 message 类型是在另一个.proto文件中定义的,该怎么办?

可以通过导入来使用其他.proto文件的定义。 为了引用其他 .proto文件的的定义,可以在文件顶部添加一个 import 语句:

import "myproject/other_protos.proto";

默认情况下,只能使用直接导入的.proto文件中的定义。 但是,有时可能需要将.proto文件移动到新位置。现在,无需在直接移动.proto文件并更新所有调用位置,而是可以在旧位置放置一个虚拟.proto文件,以便使用import public将所有导入转发到新位置。任何导入包含 import public 声明的.proto文件都可以过渡地依赖 import public 依赖项。例如:

// new.proto
// 所有的定义都移动到了这里
// old.proto
// 这是所有client都要导入的proto。
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// 可以使用 old.proto 和 new.proto 内的定义,但不可以使用 other.proto内的定义

protocol buffer 编译器使用-I/--proto_path 标志在命令行上指定的一组目录中搜索导入的文件。 如果没有给出标志,则查看调用编译器的目录。 一般来说,应该将 -- proto path 标志设置为项目的根目录,并对所有导入使用绝对路径名。

使用 proto2 message 类型

可以在 proto3 message 中导入proto2 message 类型并使用它们,反之亦然。 然而,proto2 enums 不能直接在 proto3 语法中使用(但是导入的 proto2 message 可以使用它们)。

嵌套类型

可以在其他 message 类型中定义和使用 message 类型,如下面的例子,这里的结果Resultmessage 是在 SearchResponse message 中定义的:

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}

如果要在其父 message 类型之外重用此 message 类型,则使用 _Parent_._Type_来引用:

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

可以随心所欲地将 message 深度嵌套:

message Outer {                  // 级别 0
  message MiddleAA {  // 级别 1
    message Inner {   // 级别 2
      int64 ival = 1;
      bool  booly = 2;
    }
  }
  message MiddleBB {  // 级别 1
    message Inner {   // 级别 2
      int32 ival = 1;
      bool  booly = 2;
    }
  }
}

更新一个 message 类型

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

  • 不要更改任何现有的字段编号。
  • 如果添加新字段,那么任何使用“旧”message 格式通过代码序列化的 message 仍然可以通过新生成的代码进行解析。应记住这些元素的默认值,以便新代码可以与旧代码生成的 message 正确交互。同样,由新代码创建的 message 也可以由旧代码解析:旧的二进制文件在解析时只会忽略新字段。有关详细信息,请参见“未知字段”部分。
  • 只要在更新的 message 类型中不再使用该字段号,就可以删除字段。可能想要重命名该字段,或者添加前缀“ OBSOLETE_”,或者保留该字段编号,确保该.proto的未来使用者不会意外重用该编号。
  • int32uint32int64uint64, and ,及bool 都是兼容的——这意味着可以在不破坏向前或向后兼容性的情况下将字段从这些类型中的一个更改为另一个。 如果一个数字从不适合相应类型中解析出来,会得到与在 C++ 中将数字转换为该类型相同的效果(例如,如果一个 64 位的数字被读为 int32,它将被截断为 32 位)。
  • sint32sint64互相兼容,但是 与其他整数类型兼容。
  • 只要字节是有效的 UTF-8,stringbytes就可以互相兼容。
  • 如果bytes包含 message 的编码版本,则嵌入的 message 与bytes兼容。
  • fixed32 与 sfixed32互相兼容,fixed64与 sfixed64也互相兼容。
  • 对于stringbytes和 message 字段,optional 字段与repeated字段兼容。 给定repeated字段的序列化数据作为输入,如果该字段是基本类型字段,期望该字段为optional字段的客户端将接受最后一个输入值; 如果该字段是 message 类型字段,则合并所有输入元素。 注意,对于数字类型(也包括 bool 和 enum) ,这通常是不安全的。 可以按打包的格式序列化repeated的数值类型字段,如果需要optional字段,则无法正确解析这些字段。
  • enum 与 int32uint32, int64,和 uint64 兼容(注意若值不匹配会被截断)。但要注意当客户端反序列化 message 时会采用不同的处理方案:例如,未识别的 proto3 enum 类型会被保存在 message 中,但是当 message 反序列化时如何表示是依赖于编程语言的。整型字段总是会保持其的值。
  • 将单个值更改为新值的成员是安全的,并且是二进制兼容的。 如果确保没有代码一次设置多个字段,那么将多个字段移动到新的字段中可能是安全的。 将任何字段移动到现有的字段中都是不安全的。
  • 将一个单独值更改为新的 oneof 类型成员之一是安全并且兼容二进制的。 若确定没有代码一次性设置多个字段,那么将多个字段移入一个新 oneof 类型也是可行的。将任何字段移入已存在的 oneof 类型是不安全的。

未知字段

protocol buffer 序列化数据格式良好,但是解析器不能识别的字段为未知字段。 例如,当旧二进制解析由新二进制发送的带有新字段的数据时,这些新字段将成为旧二进制中的未知字段。

最初,proto3 message 在解析过程中总是丢弃未知字段,但在 3.5 版本中,重新引入了未知字段的保存机制来匹配 proto2 行为。 在 3.5 及以后的版本中,解析器保留未知字段,并将其包含在序列化输出中。

Any 类型

Any message 类型允许将 message 作为嵌入类型使用,而不需要 .proto 文件定义。 一个 Any 包含一个类似 bytes 的任意序列化 message,以及一个 URL 来作为解析 message 类型的全局唯一标识符。 若要使用 Any 类型,需要 导入 google/protobuf/any.proto

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}

对于给定的 message 类型的默认 URL 为 type.googleapis.com/packagename.messagename 。

不同的语言实现会支持运行时的助手函数来完成类型安全地 Any 值的打包和拆包工作——例如,Java 中,Any 类型会存在特定的 pack() 和 unpack() 访问器,而 C++ 中会是 PackFrom() 和 UnpackTo() 方法:

// 在Any中存储任意一个message类型。
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);

// 从Any中读取任意一个message类型。
ErrorStatus status = ...;
for (const Any& detail : status.details()) {
  if (detail.Is()) {
    NetworkErrorDetails network_error;
    detail.UnpackTo(&network_error);
    ... processing network_error ...
  }
}

当前处理 Any 类型的运行库正在开发中

若你已经熟悉了 proto2 语法,Any 类型的位于 扩展 部分。

Oneof

如果有一条包含许多字段的 message,并且最多同时设置一个字段,那么可以通过使用 oneof 特性来强制执行此行为同时节省内存。

Oneof 字段类似于常规字段,只不过共享内存中的 oneof 字段中的所有字段都是常规字段,而且最多可以同时设置一个字段。 设置其中的任何成员都会自动清除所有其他成员。 可以使用case() 或 WhichOneof()方法检查 oneof 值是否已设置,这取决于选择的语言。

使用 Oneof

使用 oneof 关键字在 .proto 文件中定义 oneof,同时需要跟随一个 oneof 的名字,就像本例中的 test_oneof

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}

然后将字段添加到 oneof 的定义中。可以增加任意类型的字段,但不能使用 map 和 repeated字段。

在生成的代码中,oneof 字段和常规字段一致具有 getters 和 setters 。同时也会获得一个方法以用于检测哪个值被设置。更多所选编程语言中关于 oneof 的 API 可以参考 API 参考 。

Oneof 特点

  • 设置一个字段将自动清除该字段的所有其他成员。 因此,如果您设置了多个 oneof 字段,那么只有最后设置的字段生效。
    SampleMessage message;
    message.set_name("name");
    CHECK(message.has_name());
    message.mutable_sub_message(); // 将会清空name字段
    CHECK(!message.has_name());

  • 若解析器在解析得到的数据时碰到了多个 oneof 的成员,最后一个的是最终结果。
  • oneof 不能是 repeated
  • 反射 API 可作用于 oneof 字段。
  • 若将一个 oneof 字段设为了默认值(就像为 int32 类型设置了 0 ),那么 oneof 字段会被设置为 "case",同时在序列化编码时使用。
  • 若使用 C++ ,确认代码不会造成内存崩溃。以下的示例代码就会导致崩溃,因为 sub_message 在调用 set_name() 时已经被删除了。
    SampleMessage message;
    SubMessage* sub_message = message.mutable_sub_message();
    message.set_name("name"); // 将会删除 sub_message
    sub_message->set_... // 此处崩溃

  • 同样在 C++ 中,若 Swap() 两个 oneof message,那么 message 会以另一个 message 的 oneof 的情况:下例中,msg1会是 sub_message1 而 msg2 中会是 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 的值是 None/NOT_SET,这意味着 oneof 未被设置或被设置为一个不同版本的 oneof 字段。没有方法可以区分,因为无法确定一个未知字段是否是 oneof 的成员。

重用问题

  • 移入或移出 oneof 字段: message 序列化或解析后,可能会丢失一些信息(某些字段将被清除)。但是可以安全地将单个字段移入新的 oneof 中,如果确定每次操作只有一个字段被设置则可以移动多个字段。
  • 删除一个 oneof 字段并又将其加回: message 序列化和解析后,可能会清除当前设置的 oneof 字段。
  • 拆分或合并 oneof:这与移动常规字段有相似的问题。

Map 映射

若需要创建关联映射表作为定义的数据的一部分,protocol buffers 提供了方便的快捷语法:

map map_field = N;

其中key_type可以是任何整型或字符串类型(因此,除了浮点类型和 bytes 之外的任何标量类型)。 注意,enum 不是有效的key_type。 value_type 可以是除 map 以外的任何类型。

例如,如果想创建一个项目映射,其中每个Project message 都与一个字符串键相关联,可以这样定义:

map projects = 3;
  • map 字段不能为 repeated
  • 映射表的编码和迭代顺序是未定义的,因此不能依赖映射表元素的顺序来操作。
  • 当基于 .proto 生成文本格式时,映射表的元素基于 key 来排序。数值型的 key 基于数值排序。
  • 当解析或合并时,若出现冲突的 key 以最后一个 key 为准。当从文本格式解析时,若 key 冲突则会解析失败。
  • 若仅仅指定了映射表中某个元素的 key 而没有指定 value,当序列化时的行为是依赖于编程语言。在 C++,Java,和 Python 中使用类型的默认值来序列化,但在有些其他语言中可能不会序列化任何东西。

The generated map API is currently available for all proto3 supported languages. You can find out more about the map API for your chosen language in the relevant API reference.

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

向后兼容性

映射表语法与以下代码是对等的,因此 protocol buffers 的实现即使不支持映射表也可以正常处理数据:

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

repeated MapFieldEntry map_field = N;

任何支持映射表的 protocol buffers 实现都必须同时处理和接收上面代码的数据定义。

可以向.proto文件添加一个可选package说明符,以防止 protocol message 类型之间的名称冲突。

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

然后,可以在定义 message 类型的字段时使用package说明符:

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

包说明符生成代码的方式取决于选择的语言:

  • C++ 中生成的类位于命名空间中。例如,Open 会位于命名空间 foo::bar 中。
  • Java 中,使用 Java 的包,除非在 .proto 文件中使用 option java_pacakge 做成明确的指定。
  • Python 中,package 说明符被忽略,这是因为 Python 的模块是基于文件系统的位置来组织的。
  • Go 中,作为 Go 的包名来使用,除非在 .proto 文件中使用 option java_pacakge 做成明确的指定。
  • Ruby 中,生成的类包裹于 Ruby 的命名空间中,还要转换为 Ruby 所需的大小写风格(首字母大写;若首字符不是字母,则使用 PB_ 前缀)。例如,Open 会位于命名空间 Foo::Bar 中。
  • C# 中作为命名空间来使用,同时需要转换为 PascalCase 风格,除非在 .proto 使用 option csharp_namespace 中明确的指定。例如,Open 会位于命名空间 Foo.Bar 中。

包名解析

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

最简单易用的 RPC 系统就是 gRPC:Google 开发的一个语言和平台无关的开源 RPC 系统。gRPC 特别适用于 protocol buffers,它可以让你直接从你的.proto使用特殊的 protocol buffer 编译器插件生成相关的 RPC 代码。

如果不想使用 gRPC,也可以在你自己的 RPC 实现中使用 protocol buffe。 可以在Proto2 语言指南中找到更多相关信息。

还有一些开发 RPC 实现 Protocol Buffers 的第三方项目。 有关我们所知道的项目的链接列表,请参阅第三方项目 WIKI 页面

JSON 映射

Proto3 支持 JSON 的编码规范,使得不同系统之间更容易共享数据。 下表按类型逐一描述这些编码。

若 JSON 编码中不存在某个值或者值为 null,当将其解析为 protocol buffer 时会解析为合适的默认值。若 procol buffer 中使用的是字段的默认值,则默认情况下 JSON 编码会忽略该字段以便于节省空间。实现上应该提供一个选项以用来将具有默认值的字段生成在 JSON 编码中。

proto3JSONJSON 示例说明messageobject{"fooBar": v, "g": null,…}生成 JSON 对象。message 字段名映射为对象的 lowerCamelCase(译著:小驼峰) 的 key。若指定了 json_name 选项,则使用该选项值作为 key。解析器同时支持 lowerCamelCase 名称(或 json_name 指定名称)和原始 proto 字段名称。全部类型都支持 null 值,是当做对应类型的默认值来对待的。enumstring"FOO_BAR"使用 proto 中指定的枚举值的名称。解析器同时接受枚举名称和整数值。map<K,V>object`{"k": v, …}所有的 key 被转换为字符串类型。repeated Varray[v, …]null 被解释为空列表 []。booltrue, falsetrue, falsestringstring"Hello World!"bytesbase64 string"YWJjMTIzIT8kKiYoKSctPUB+"JSON 值是使用标准边界 base64 编码的字符串。不论标准或 URL 安全还是携带边界与否的 base64 编码都支持。int32, fixed32, uint32number1, -10, 0JSON 值是 10 进制数值。数值或字符串都可以支持。int64, fixed64, uint64string"1", "-10"JSON 值是 10 进制字符串。数值或字符串都支持。float, doublenumber1.1, -10.0, 0, "NaN","Infinity"JSON 值是数值或特定的字符串之一:"NaN","Infinity" 和 "-Infinity" 。数值和字符串都支持。指数表示法同样支持。Anyobject{"@type": "url", "f": v, … }若 Any 类型包含特定的 JSON 映射值,则会被转换为下面的形式: {"@type": xxx, "value": yyy}。否则,会被转换到一个对象中,同时会插入一个 "@type" 元素用以指明实际的类型。Timestampstring"1972-01-01T10:00:20.021Z"采用 RFC 3339 格式,其中生成的输出总是 Z 规范的,并使用 0、3、6 或 9 位小数。除 “Z” 以外的偏移量也可以。Durationstring"1.000340012s", "1s"根据所需的精度,生成的输出可能会包含 0、3、6 或 9 位小数,以 “s” 为后缀。只要满足纳秒精度和后缀 “s” 的要求,任何小数(包括没有)都可以接受。Structobject{ … }任意 JSON 对象。参见 struct.proto.Wrapper typesvarious types2, "2", "foo", true,"true", null, 0, …包装器使用与包装的原始类型相同的 JSON 表示,但在数据转换和传输期间允许并保留 null。FieldMaskstring"f.fooBar,h"参见field_mask.proto。ListValuearray[foo, bar, …]ValuevalueAny JSON valueNullValuenullJSON nullEmptyobject{}空 JSON 对象

JSON 选项

一个 proto3 JSON 实现可能提供以下选项:

  • 省略使用默认值的字段:默认情况下,在 proto3 的 JSON 输出中省略具有默认值的字段。该实现可以使用选项来覆盖此行为,来在输出中保留默认值字段。
  • 忽略未知字段:默认情况下,proto3 的 JSON 解析器会拒绝未知字段,同时提供选项来指示在解析时忽略未知字段。
  • 使用 proto 字段名称代替 lowerCamelCase 名称: 默认情况下,proto3 的 JSON 编码会将字段名称转换为 lowerCamelCase(译著:小驼峰)形式。该实现提供选项可以使用 proto 字段名代替。Proto3 的 JSON 解析器可同时接受 lowerCamelCase 形式 和 proto 字段名称。
  • 枚举值使用整数而不是字符串表示: 在 JSON 编码中枚举值是使用枚举值名称的。提供了可以使用枚举值数值形式来代替的选项。

选项

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

.proto 文件中的单个声明可以被一组选项来设置。选项不是用来更改声明的含义,但会影响在特定上下文下的处理方式。完整的选项列表定义在 google/protobuf/descriptor.proto 中。

有些选项是文件级的,意味着可以卸载顶级作用域,而不是在消息、枚举、或服务的定义中。有些选项是 message 级的,意味着需写在 message 的定义中。有些选项是字段级的,意味着需要写在字段的定义内。选项还可以写在枚举类型,枚举值,oneof 类型,service 类型和 service 方法上;然而,目前还没有任何可用于以上位置的选项。

下面是几个最常用的选项:

  • java_package (文件选项):要用在生成 Java 代码中的包。若没有在 .proto 文件中对 java_package 选项做设置,则会使用 proto 作为默认包(在 .proto 文件中使用 "package" 关键字设置)。 然而,proto 包通常不是合适的 Java 包,因为 proto 包通常不以反续域名开始。若不生成 Java 代码,则此选项无效
    option java_package = "com.example.foo";
  • java_multiple_files(文件选项):导致将顶级 message、枚举、和服务定义在包级,而不是在以 .proto 文件命名的外部类中。
option java_multiple_files = true;
  • java_outer_classname(文件选项):要生成的最外层 Java 类(也就是文件名)。若没有在 .proto 文件中明确指定 java_outer_classname 选项,类名将由 .proto 文件名转为 camel-case 来构造(因此 foo_bar.proto 会变为 FooBar.java)。若不生成 Java 代码,则此选项无效。
    option java_outer_classname = "Ponycopter";
  • optimize_for (文件选项): 可被设为 SPEED, CODE_SIZE,或 LITE_RUNTIME。这会影响 C++ 和 Java 代码生成器(可能包含第三方生成器) 的以下几个方面:
    • SPEED (默认): protocol buffer 编译器将生成用于序列化、解析和 message 类型常用操作的代码。生成的代码是高度优化的。
    • CODE_SIZE :protocol buffer 编译器将生成最小化的类,并依赖于共享的、基于反射的代码来实现序列化、解析和各种其他操作。因此,生成的代码将比 SPEED 模式小的多,但操作将变慢。类仍将实现与 SPEED 模式相同的公共 API。这种模式在处理包含大量 .proto 文件同时不需要所有操作都要求速度的应用程序中最有用。
    • LITE_RUNTIME :protocol buffer 编译器将生成仅依赖于 “lite” 运行库的类(libprotobuf-lite 而不是 libprotobuf)。lite 运行时比完整的库小得多(大约小一个数量级),但会忽略某些特性,比如描述符和反射。这对于在受限平台(如移动电话)上运行的应用程序尤其有用。编译器仍然会像在 SPEED 模式下那样生成所有方法的快速实现。生成的类将仅用每种语言实现 MessageLite 接口,该接口只提供 Message 接口的一个子集。

option optimize_for = CODE_SIZE;

  • cc_enable_arenas(文件选项):为生成的 C++ 代码启用 arena allocation 。
  • objc_class_prefix (文件选项): 设置当前 .proto 文件生成的 Objective-C 类和枚举的前缀。没有默认值。你应该使用 Apple 建议 的 3-5 个大写字母作为前缀。注意所有 2 个字母前缀都由 Apple 保留。
  • deprecated (字段选项):若设置为 true, 指示该字段已被废弃,新代码不应使用该字段。在大多数语言中,这没有实际效果。在 Java 中,这变成了一个 @Deprecated 注释。将来,其他语言的代码生成器可能会在字段的访问器上生成弃用注释,这将导致在编译试图使用该字段的代码时发出警告。如果任何人都不使用该字段,并且您希望阻止新用户使用它,那么可以考虑使用保留语句替换字段声明。
    int32 old_field = 6 [deprecated = true];

自定义选项

protocol buffer 还允许使用自定义选项。大多数人都不需要此高级功能。若确认要使用自定义选项,请参阅 Proto2 语言指导 了解详细信息。注意使用 扩展 来创建自定义选项,只允许用于 proto3 中。

生成自定义类

若要生成操作 .proto 文件中定义的消息类型的 Java、Python、C++、Go、Ruby、Objective-C 或 C# 代码,需要对 .proto 文件运行 protocol buffer 编译器 protoc。若还没有安装编译器,请 download the package 并依据 README 完成安装。对于 Go ,还需要为编译器安装特定的代码生成器插件:可使用 GitHub 上的 golang/protobuf 库。

protocol buffer 编译器的调用方式如下:

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 选项实现;这些目录会依顺序检索。 -I=*IMPORT_PATH* 可作为 --proto_path 的简易格式使用。
  • 可以提供一个或多个输出指令:
    • --cpp_out 在 DST_DIR目录 生成 C++ 代码。参阅 C++ generated code reference 获取更多信息。
    • --java_out 在 DST_DIR目录 生成 Java 代码。参阅 Java generated code reference 获取更多信息。
    • --python_out在 DST_DIR目录 生成 Python 代码。参阅 Python generated code reference 获取更多信息。
    • --go_out 在 DST_DIR目录 生成 Go 代码。参阅 Go generated code reference 获取更多信息。
    • --ruby_out 在 DST_DIR目录 生成 Ruby 代码。 coming soon!
    • --objc_out 在 DST_DIR目录 生成 Objective-C 代码。参阅 Objective-C generated code reference 获取更多信息。
    • --csharp_out 在 DST_DIR目录 生成 C# 代码。参阅 C# generated code reference 获取更多信息。
    • --php_out 在 DST_DIR目录 生成 PHP 代码。参阅 PHP generated code reference 获取更多信息。作为额外的便利,若 DST_DIR 以 .zip 或 .jar 结尾,编译器将会写入给定名称的 ZIP 格式压缩文件,.jar 还将根据 Java JAR 的要求提供一个 manifest 文件。请注意,若输出文件已经存在,它将被覆盖。编译器还不够智能,无法将文件添加到现有的存档中。

 

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

风格指南

本文档提供了.proto文件的风格。 通过遵循这些约定,将使 protocol buffer message 定义及其对应的类保持一致并易于阅读。

注意,protocol buffer 样式随着时间的推移而演变,因此可能会看到用不同的约定或样式编写的 .proto 文件。 修改这些文件时,请使用已有的样式。 保持一致性才是关键。 然而,当创建一个新的.proto 文件时,最好是采用当前的最好的风格。

标准文件格式

  • 保持行长度为 80 个字符内。
  • 使用 2 个空格的缩进

文件结构

文件应命名为lower_snake_case.proto

所有文件应以下列方式排列:

  1. 许可证头(如果适用)
  2. 文件概览
  3. 语法
  4. imports(已排序)
  5. 文件选项
  6. 其他

包名应该是小写的,并且应该与目录层次结构相对应。 例如,如果一个文件在 my/package/中,那么包名应该是 my.package

message 和字段名

对 message 名使用CamelCase (首字母大写)——例如SongServerRequest。 对字段名使用underscore_separated_names(包括 oneof 字段和 extension 名)——例如song_name

message SongServerRequest {
  required string song_name = 1;
}

使用这个字段的命名原则,可以获得如下的访问器:

C++:
  const string& song_name() { ... }
  void set_song_name(const string& x) { ... }

Java:
  public String getSongName() { ... }
  public Builder setSongName(String v) { ... }

如果字段名包含数字,数字应该出现在字母之后,而不是下划线之后。 使用 song_name1而不是song_name_1

repeated 字段

对 repeated 字段使用复数名称。

  repeated string keys = 1;
  ...
  repeated MyMessage accounts = 17;

枚举类型

对枚举类型名使用 CamelCase (首字母大写) ,对值名使用CAPITALS_WITH_UNDERSCORES:

enum Foo {
  FOO_UNSPECIFIED = 0;
  FOO_FIRST_VALUE = 1;
  FOO_SECOND_VALUE = 2;
}

每个枚举值应以分号而不是逗号结尾。 应在枚举值前面加上前缀,而不是将它们包围在一个封闭 message 中。 零值枚举应该具有 UNSPECIFIED 后缀。

服务

If your .proto defines an RPC service, you should use CamelCase (with an initial capital) for both the service name and any RPC method names:

如果.proto 定义了一个 RPC 服务,应该对服务名和任何 RPC 方法名使用 CamelCase (首字母大写) :

service FooService {
  rpc GetSomething(FooRequest) returns (FooResponse);
}

应避免的事情

  • 必填字段(仅用于 proto2)
  • Groups (仅用于 proto2)

编码

本文档描述了 protocol buffer messages 的二进制线格式。 在应用程序中使用 protocol buffer 时不需要理解这一点,但了解不同的 protocol buffer 格式如何影响 message 编码的大小可能非常有用。

一个简单的 message

假设有以下非常简单的 message 定义:

message Test1 {
  optional int32 a = 1;
}

在应用中,创建一条 Test1消息并将 a 设置为 150。 然后将 message 序列化为输出流。 如果检查 message 的编码,将看到三个字节:

08 96 01

就是这么小和这么数字化。但它意味着什么? 继续看下去..

Base 128 Varints 编码算法

要理解简单的 protocol buffer 编码,首先需要理解 varints。 Varint 是一种使用一个或多个字节序列化整数的方法。 较小的数字占用较少的字节数。

除了最后一个字节以外,varint 中的每个字节都设置了最高有效位(most significant bit ,msb)——表示还有更多的字节要处理。 每个字节较低的 7 位用于存储数字的两位补码表示形式,以 7 位为一组,最低有效组优先。

例如,数字 1 是一个字节,所以 msb 没有设置:

0000 0001

再例如,数字 300 情况有点复杂:

1010 1100 0000 0010

怎么知道这是 300? 首先从每个字节中删除 msb,因为它只是用来告诉我们是否已经到达数字的末尾(如下所示,它在第一个字节中设置,因为在 varint 中多于一个字节) :

1010 1100 0000 0010
→ 010 1100  000 0010

将这两组 7 位反转过来,varint 首先存储最低有效组的数字。 然后将它们串联起来得到最终值:

000 0010  010 1100
→  000 0010 ++ 010 1100
→  100101100
→  256 + 32 + 8 + 4 = 300

message 结构

protocol buffer message 是一系列键值对。 message 的二进制版本只使用字段的数字作为键——每个字段的名称和声明类型只能在解码端通过引用 message 类型的定义(即.proto文件)。

当对 message 进行编码时,键和值被连接成一个字节流。 当 message 被解码时,解析器需要能够跳过它不识别的字段。 通过这种方式,可以将新字段添加到 message 中,而不会破坏不知道这些字段的旧程序解析过程。 为此,wire 格式的 message 中每一对的“键”实际上是两个值——.proto文件和一个 wire 类型,该 wire 类型提供了足够的信息来查找下列值的长度。 在大多数语言实现中,这个键被称为标记。

现有的 wire 类型如下:

类型含义用途0Varintint32, int64, uint32, uint64, sint32, sint64, bool, enum164-bitfixed64, sfixed64, double2Length-delimitedstring, bytes, embedded messages, packed repeated fields3Start groupgroups (deprecated)4End groupgroups (deprecated)532-bitfixed32, sfixed32, float

流式 message 中的每个键都是一个 varint,其值为(field_number << 3) | wire_type ——换句话说,数字的最后三位存储 wire 类型。

现在再看一下这个简单的例子。 现在知道流中的第一个数字总是 varint 键,这里是 08,或者删除 msb :

000 1000

You take the last three bits to get the wire type (0) and then right-shift by three to get the field number (1). So you now know that the field number is 1 and the following value is a varint. Using your varint-decoding knowledge from the previous section, you can see that the next two bytes store the value 150.

取最后三个位得到 wire 类型(0) ,然后右移三个位得到字段数字(1)。 现在已知字段数字是 1,下面的值是 varint。 使用上一节中的 varint 解码知识,可以看到接下来的两个字节存储值 150。

96 01 = 1001 0110  0000 0001
       → 000 0001  ++  001 0110 (删掉 msb 并且调换7位组的顺序)
       → 10010110
       → 128 + 16 + 4 + 2 = 150

更多的值类型

有符号整数

正如上一节中所示,与 wire 类型 0 相关联的所有 protocol buffer 类型都被编码为 varint。 然而,在负数的编码中,带符号的 int 类型(sint32 和 sint64)和“标准” int 类型(int32 和 int64)之间有一个重要的区别。 如果使用 int32 或 int64 作为负数的类型,那么结果的 varint 总是长度位 10 个字节——实际上,它被视为一个非常大的无符号整数。 如果使用有符号类型,则生成的 varint 使用 ZigZag 编码,这种编码效率要高得多。

ZigZag encoding maps signed integers to unsigned integers so that numbers with a small absolute value (for instance, -1) have a small varint encoded value too. It does this in a way that "zig-zags" back and forth through the positive and negative integers, so that -1 is encoded as 1, 1 is encoded as 2, -2 is encoded as 3, and so on, as you can see in the following table:

Zigzag 编码将有符号的整数映射到无符号的整数,因此绝对值较小(例如-1)的数字也具有较小的 varint 编码值。 它通过正整数和负整数来回“zig-zags” ,因此-1 被编码为 1,1 被编码为 2,-2 被编码为 3,以此类推,如下表所示:

有符号整数编码00-1112-2321474836474294967294-21474836484294967295

In other words, each value n is encoded usingsint32s

换句话说,每个值 n 都使用sint32

(n << 1) ^ (n >> 31)

或 64 位的版本。

(n << 1) ^ (n >> 63)
注意,第二个移位—— (n >> 31)部分——是一个算术移位。 所以移位的结果要么是一个全部为零位的数字(如果 n 是正数) ,要么是一个全部为一位的数字(如果 n 是负数)。

解析 sint32 或 sint64 时,将其值解码回原始的有符号版本。

非 varint 数字

非 varint 数字类型比较简单—— double 和fixed64 的 wire 类型为 1,它告诉解析器需要一个固定的 64 位数据块; 类似地,float 和 fixed32 具有 wire 类型 5,它告诉解析器需要 32 位。 在这两种情况下,值都以小端字节顺序存储。

字符串

wire 类型为 2(长度受限)意味着该值是编码后的 varint 的长度,后跟指定数量的数据字节。

message Test2 {
  optional string b = 2;
}

将 b 的值设置为“testing” ,可以得到:

12 07 74 65 73 74 69 6e 67

字节是“ testing”的 UTF8,这里的键是 0x12→

0001 0010

00010 010

→field_number = 2, wire_type = 2。值中的 varint 长度是 7,后面跟着 7 个字节——“testing”字符串。

嵌入式 message

下面是一个 message 定义,其中嵌入了我们的示例类型的 message,Test1:

message Test3 {
  optional Test1 c = 3;
}

这是经过编码的版本,同样,Test1 的字段设置为 150:

1a 03 08 96 01

如上所示,最后三个字节与我们的第一个示例(08 96 01)完全相同,前面是数字 3,嵌入的 message 与字符串是完全相同的处理方式(wire type = 2)。

可选和重复的元素

如果 proto2 消息定义包含repeated 元素(不包含[packed=true]选项) ,则编码后的 message 包含具有相同字段编号的零个或多个键值对。 这些重复值不一定要连续出现,可能与其他字段交错出现。 解析时保留了元素相对于其他元素的顺序,但丢失了相对于其他字段的顺序。 在 proto3 中,重复字段使用 packed encoding,可以接着往下阅读。

对于 proto3 中的任何非重复字段,或 proto2 中的optional 字段,编码的 message 有可能没有该字段编号的键值对。

通常,一个编码后 message 不会有多于一个不重复字段的实例。 但是,解析器应该处理这种有可能遇到的情况。 对于数值类型和字符串,如果同一字段出现多次,解析器将接受它发现的最后一个值。 对于嵌入式 message 字段,解析器合并同一字段的多个实例,就像使用Message::MergeFrom 方法一样,即后一个实例中的所有单独标量字段替换前一个实例中的所有单独标量字段,合并嵌入的单独 message,并连接重复字段。 这些规则的效果是,解析两个编码后 message 的连接所产生的结果与分别解析两个 message 合并结果完全相同。 如下所示:

MyMessage message;
message.ParseFromString(str1 + str2);

等同于:

MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);

此属性有时很有用,因为即使不知道它们的类型,也允许合并两个 message。

Packed 重复字段

2.1.0 版本引入了 packed 的 repeated 字段,在 proto2 中,这些字段被声明为类似 repeated 字段,但是带有特殊的[packed=true]选项。 在 proto3 中,标量数值类型的 repeated 字段默认是 packed 的。 这些功能类似于 repeated 字段,但是编码方式不同。 包含零个元素的 packed repeated 字段不会出现在编码 message 中。 否则,该字段的所有元素都被 packed 到一个键值对中,该键值对使用 wire 类型 2(长度受限)。 每个元素的编码方式与正常情况下相同,只是前面没有键。

例如,假设你有一个 message 类型:

message Test4 {
  repeated int32 d = 4 [packed=true];
}:

现在,假设构造了一个 Test4,为 repeated 字段 d 提供值 3、270 和 86942,那么,编码的形式应该是:

22        // 键 (字段编码 4, wire 类型 2)
06        // payload 大小 (6 bytes)
03        // 第一个元素 (varint 3)
8E 02     // 第二个元素 (varint 270)
9E A7 05  // 第三个元素 (varint 86942)

只有基本数值类型的重复字段(使用 varint、32 位或 64 位 wire 类型)可以声明为“ packed”。

注意,虽然通常没有理由为一个 packed 重复字段编码多个键值对,但编码器必须准备好接受多个键值对。 在这种情况下,应该连接 payload。 每一对都必须包含全部数量的元素。

protocol buffer 解析器必须能够解析已编译为packed 的重复字段,就像它们没有packed 一样,反之亦然。 这允许以向前和向后兼容的方式向现有字段添加[packed=true]

字段顺序

.proto中,字段编号可以按任意顺序排列。 所选择的顺序对 message 的序列化过程没有影响。

在序列化 message 时,不能保证如何编写其已知或 未知字段的顺序。 序列化顺序是一个实现时的细节,任何特定实现的细节在将来都可能发生变化。 因此,protocol buffer 解析器必须能够以任何顺序解析字段。

影响

  • 不要假设序列化消息的字节输出是稳定的。 尤其是对于具有传递字节字段表示其他 protocol buffer messages 的 message 来说更是如此。
  • 默认情况下,对同一个 protocol buffer messages 实例重复调用序列化方法可能不会返回相同的字节输出; 也就是说,默认序列化是不确定的
    • 序列化只保证在特定二进制文件的情况下有相同字节输出。 字节输出可能会随着二进制文件的不同版本而改变

 

  • 对于 protocol buffer message 实例foo,下列检查可能失败
    • foo.SerializeAsString() == foo.SerializeAsString()
  • Hash(foo.SerializeAsString()) == Hash(foo.SerializeAsString())
    • CRC(foo.SerializeAsString()) == CRC(foo.SerializeAsString())

 

  • FingerPrint(foo.SerializeAsString()) == FingerPrint(foo.SerializeAsString())
  • 下面是逻辑上等价的 protocol buffer message 实例foo and bar可能会序列化输出不同字节的示例场景。
    • bar 由旧的服务序列化,该服务将某些字段视为未知。
  • bar 使用不同的编程语言实现并按不同的顺序序列化字段的服务来序列化。
    • bar 具有以非确定方式序列化的字段。

 

  • bar 具有一个字段,该字段存储以不同方式序列化的 protocol buffer message 的序列化字节输出
    • bar 由一个新的服务序列化,该服务由于实现的更改而按不同的顺序序列化字段
    • foo 和 bar 都是单个 message 的连接,但是顺序不同。

技巧

本节描述了一些常用的设计模式来处理 Protocol Buffers。 也可以把设计和使用的问题发送到Protocol Buffers 讨论组

串流多条 message

如果希望将多条 message 写入一个文件或流,则需要跟踪一条 message 的结束位置和下一条 message 的开始位置。 Protocol Buffer wire 格式不是自限定的,因此 protocol buffer 解析器无法自己确定 message 的结束位置。 解决这个问题的最简单方法是在编写 message 本身之前编写每个 message 的大小。 当读回 message 时,先读取大小,然后将字节读入一个单独的缓冲区,然后从该缓冲区解析。 (如果希望避免将字节复制到单独的缓冲区,可以查看 CodedInputStream 类(在 C++ 和 Java 中) ,该类可以将读操作限制到一定数量的字节。)

大型数据集

Protocol Buffers 并不是设计用来处理大 message 的。 作为一般的经验法则,如果处理的 message 每个都大于兆字节,那么可能是时候考虑其他策略了。

也就是说,Protocol Buffers 非常适合处理大数据集的单个 message。 通常,大型数据集实际上只是很多小块的集合,其中每个小块可能是一个结构化的数据块。 尽管 Protocol Buffers 不能一次处理整个集合,但是使用 Protocol Buffers 编码可以极大地简化问题: 现在所需要做的就是处理一组字节串而不是一组结构。

由于不同的情况需要不同的解决方案,Protocol Buffers 不包括对大型数据集的任何内置支持。 有时候一个简单的记录列表就可以了,而有时候可能想要一个类似数据库的东西。 每个解决方案都应该作为一个单独的库来开发,这样只有那些需要它的人才需要支付开发成本。

自描述 message

Protocol Buffers 不包含它们自己类型的描述。 因此,只给定原始消息而不给定对应的 .proto文件定义其类型,很难提取任何有用的数据。

但是,请注意.proto文件本身可以使用 protocol buffers 表示。 源代码包中的文件src/google/protobuf/descriptor.proto 定义了所涉及的 message 类型。 protoc可以使用选项--descriptor_set_out输出一个 FileDescriptorSet(表示一组.proto 文件)。 这样就可以定义一个自描述的 protocol message,如下所示:

syntax = "proto3";

import "google/protobuf/any.proto";
import "google/protobuf/descriptor.proto";

message SelfDescribingMessage {
  // 设置可以描述类型和依赖的FileDescriptorProtos
  google.protobuf.FileDescriptorSet descriptor_set = 1;

  // 这个message和类型编码为了一个Any message.
  google.protobuf.Any message = 2;
}

通过使用像 DynamicMessage (在 C++ 和 Java 中可用)这样的类,可以编写操作 SelfDescribingMessage的工具。

尽管如此,这个功能之所以没有包含在 Protocol Buffer 库中,是因为我们从来没有在 Google 中使用过它。

这种技术需要支持使用描述符的动态 message。 在使用自描述 message 之前,请检查平台是否支持此功能。

第三方附加组件

许多开源项目试图在 google Protocol Buffers 的基础上添加有用的功能。 有关我们所知道的项目的链接列表,请参阅第三方附加组件 wiki 页

参考