gRPC框架学习:2、ProtocolBuffers学习

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

1. 前言

前面我们对gRPC框架有了简单了解,按照gRPC的推荐,我们一般使用Protocol Buffers来进行接口缓存,以此来适应不同的语言进行rpc交互,由于proto3具有更多的语言兼容性,所以我们这里主要是学习proto3的语法,然后再针对Go语言如何结合prot3使用做下专门的学习和总结,等有机会我们也可以使用其它语言来实践和总结。

本指南介绍了如何使用协议缓冲区语言来构造协议缓冲区数据,包括.proto文件语法以及如何从.proto文件中生成数据访问类。本总结来自:developers.google.com/protocol-bu… 如果有疑问的话可以通过查看原文。

2. 定义消息类型

首先让我们看一个非常简单的例子。假设您要定义一个搜索请求消息格式,其中每个搜索请求都有一个查询字符串,您感兴趣的特定结果页面以及每页结果数量。这是.proto用于定义消息类型的文件。

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
复制代码
  • 文件的第一行指定您正在使用proto3语法:如果不这样做,则协议缓冲区编译器将假定您正在使用proto2。这必须是文件的第一行非空,非注释行。
  • 所述SearchRequest消息定义指定了三个字段(名称/值对),一个用于每条数据要在此类型的消息包括。每个字段都有一个名称和类型。

(1). 指定字段类型

在上面的示例中,所有字段均为标量类型:两个整数(page_number和result_per_page)和一个字符串(query)。但是,您也可以为字段指定复合类型,包括枚举和其他消息类型。

(2). 分配字段编号

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

您可以指定最小的场数是1,最大为2^29- 1,或536870911。您也不能使用数字19000到19999(FieldDescriptor::kFirstReservedNumber至FieldDescriptor::kLastReservedNumber),因为它们是为协议缓冲区实现保留的-如果您在中使用这些保留数之一,则协议缓冲区编译器会抱怨.proto。同样,您不能使用任何以前保留的字段号。

(3). 指定字段规则

消息字段可以是以下内容之一:

  • 单数:格式正确的邮件可以包含零个或一个此字段(但不能超过一个)。这是proto3语法的默认字段规则。
  • repeated:在格式正确的消息中,此字段可以重复任意次(包括零次)。重复值的顺序将保留。

在proto3中,repeated标量数字类型的字段packed默认情况下使用编码。

您可以packed在协议缓冲区编码中找到有关编码的更多信息。

(4). 添加更多消息类型

可以在一个.proto文件中定义多种消息类型。如果要定义多个相关消息,这很有用–例如,如果要定义与您的SearchResponse消息类型相对应的回复消息格式,可以将其添加到相同的消息中.proto:

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

message SearchResponse {
 ...
}
复制代码

(5). 添加评论(注释)

要将注释添加到.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.
}
复制代码

(6). 保留字段

如果您通过完全删除字段或将其注释掉来更新消息类型,将来的用户可以在自己对类型进行更新时重用该字段号。如果他们以后加载旧版本的旧版本,可能会导致严重的问题.proto,包括数据损坏,隐私错误等。确保不会发生这种情况的一种方法是,将已删除字段的字段编号(和/或名称,也可能导致JSON序列化的问题)指定为reserved。如果将来有任何用户尝试使用这些字段标识符,则协议缓冲区编译器将抱怨。

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}
复制代码

请注意,您不能在同reserved一条语句中混用字段名称和字段编号。

(7). .proto产生了什么

当你在运行protocol buffer compiler处理一个.proto文件时,编译器会以您选择的语言生成代码,您将需要使用文件中描述的消息类型,包括获取和设置字段值,将消息序列化为输出流,并从输入流中解析消息。

  • 对于C ++,编译器从每个生成一个.h和.cc文件.proto,并为文件中描述的每种消息类型提供一个类。
  • 对于Java,编译器会生成一个.java文件,其中包含每种消息类型的类以及Builder用于创建消息类实例的特殊类。
  • Python有所不同– Python编译器会在您的中生成一个模块,其中包含每种消息类型的静态描述符,.proto然后将该模块与元类一起使用,以在运行时创建必要的Python数据访问类。
  • 对于Go,编译器.pb.go将为文件中的每种消息类型生成一个具有相应类型的文件。
  • 对于Ruby,编译器将.rb使用包含您的消息类型的Ruby模块生成一个文件。
  • 对于Objective-C,编译器会从每个生成一个pbobjc.h和pbobjc.m文件.proto,并为文件中描述的每种消息类型提供一个类。
  • 对于C#,编译器会.cs从each生成一个文件.proto,并为文件中描述的每种消息类型提供一个类。
  • 对于Dart,编译器会为.pb.dart文件中的每种消息类型生成一个带有类的文件。

通过遵循所选语言的教程(即将推出proto3版本),您可以找到有关每种语言使用API​​的更多信息。有关API的更多详细信息,请参见相关的API参考(proto3版本也即将推出)。

3. 标量值类型

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

.proto Type Notes C++ Type Java Type Python Type[2] Go Type Ruby Type C# Type PHP Type Dart Type
double double double float float64 Float double float double
float float float float float32 Float 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 使用可变长度编码。有符号的int值。与常规int32相比,它们更有效地对负数进行编码。 int32 int int int32 Fixnum or Bignum (as required) int integer int
sint64 使用可变长度编码。有符号的int值。与常规int64相比,它们更有效地编码负数。 int64 long int/long[3] int64 Bignum long integer/string[5] Int64
fixed32 始终为四个字节。如果值通常大于2 28,则比uint32更有效。 uint32 int[1] int/long[3] uint32 Fixnum or Bignum (as required) uint integer int
fixed64 始终为八个字节。如果值通常大于2 56,则比uint64更有效。 uint64 long[1] int/long[3] uint64 Bignum ulong integer/string[5] Int64
sfixed32 始终为四个字节。 int32 int int int32 Fixnum or Bignum (as required) int integer int
sfixed64 始终为八个字节。 int64 long int/long[3] int64 Bignum long integer/string[5] Int64
bool bool boolean bool bool TrueClass/FalseClass bool boolean bool
string 字符串必须始终包含UTF-8编码或7位ASCII文本,并且不能超过2^32。 string String str/unicode[4] string String (UTF-8) string string String
bytes 可以包含任意长度不超过2^32的字节序列。 string ByteString str []byte String (ASCII-8BIT) ByteString string

当您在Protocol Buffer Encoding中对消息进行序列化时,您可以找到更多有关如何编码这些类型的信息。

[1]在Java中,无符号的32位和64位整数使用带符号的对等体表示,最高位仅存储在符号位中。

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

[3] 64位或无符号32位整数在解码时总是表示为long,但是如果在设置字段时给出了int,则可以为int。在所有情况下,该值都必须适合设置时表示的类型。参见[2]。

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

[5]在64位计算机上使用Integer,在32位计算机上使用string。

4. 默认值

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

  • 对于字符串,默认值为空字符串。
  • 对于字节,默认值为空字节。
  • 对于布尔值,默认值为false。
  • 对于数字类型,默认值为零。
  • 对于枚举,默认值为第一个定义的枚举值,必须为0。
  • 对于消息字段,未设置该字段。它的确切值取决于语言。有关详细信息,请参见生成的代码指南。

重复字段的默认值是空的(通常是使用适当语言的空列表)。

请注意,对于标量消息字段,一旦解析了一条消息,就无法告诉该字段是被显式设置为默认值(例如,是否将boolean设置为false)还是根本没有设置:您应该牢记这一点在定义您的消息类型时。例如,false如果您不希望默认情况下也发生这种行为,则在设置为时,没有布尔值会打开某些行为。还要注意的是,如果一个标消息字段被设置为默认值,该值将不会在电线上连载。

有关默认值在生成的代码中如何工作的更多详细信息,请参见所选语言的生成的代码指南。

5. 枚举

定义消息类型时,您可能希望其字段之一仅具有一个预定义的值列表之一。例如,假设你想添加一个corpus字段每个SearchRequest,其中语料库可以UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO。您可以通过enum在消息定义中为每个可能的值添加一个常量来非常简单地执行此操作。

在下面的示例中,我们添加了一个带有所有可能值的enum被叫项Corpus,以及一个type字段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作为数字默认值。
  • 零值必须是第一个元素,以便与proto2语义兼容,其中第一个枚举值始终是默认值。

您可以通过将相同的值分配给不同的枚举常量来定义别名。为此,您需要将allow_alias选项设置为true,否则协议别名会在找到别名时生成一条错误消息。

message MyMessage1 {
  enum EnumAllowingAlias {
    option allow_alias = true;
    UNKNOWN = 0;
    STARTED = 1;
    RUNNING = 1;
  }
}
message MyMessage2 {
  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编码,因此负值效率不高,因此不建议使用。您可以enum在消息定义中定义,如上面的示例所示,enum也可以在外部定义-这些s可以在.proto文件中的任何消息定义中重复使用。您还可以使用enum语法将一条消息中声明的类型用作另一条消息中字段的类型_MessageType_.EnumType

当运行在所述协议缓冲编译器.proto,它使用一个enum,生成的代码将具有对应enum于Java或C ++,一个特殊EnumDescriptor的是被用于创建一组与在所述运行时生成的类的整数值的符号常数的Python类。

注意: 生成的代码可能会受到特定于语言的枚举数限制(一种语言的成千上万个)。请查看您计划使用的语言的限制。

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

有关如何enum在应用程序中使用message的更多信息,请参见针对所选语言生成的代码指南。

(1). 保留值

如果您通过完全删除枚举条目或将其注释掉来更新枚举类型,则将来的用户在自己对类型进行更新时可以重复使用数值。如果他们以后加载旧版本的旧版本,可能会导致严重的问题.proto,包括数据损坏,隐私错误等。确保不会发生这种情况的一种方法是,将已删除的条目的数字值(和/或名称,也可能导致JSON序列化的问题)指定为reserved。如果将来有任何用户尝试使用这些标识符,则协议缓冲区编译器会抱怨。您可以使用max关键字指定保留的数值范围达到最大可能值。

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}
复制代码

请注意,您不能在同reserved一条语句中混合使用字段名和数字值。

6. 使用其他消息类型

您可以使用其他消息类型作为字段类型。例如,假设你想包括Result每个消息的SearchResponse消息-要做到这一点,你可以定义一个Result在同一个消息类型.proto,然后指定类型的字段Result中SearchResponse:

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}
复制代码

(1). 导入定义

请注意,此功能在Java中不可用。

在上面的示例中,Result消息类型与以下文件定义在同一文件中SearchResponse–如果要用作字段类型的消息类型已在另一个.proto文件中定义,该怎么办?

您可以.proto通过导入其他文件来使用它们的定义。要导入another.proto的定义,请在文件顶部添加一个import语句:

import "myproject/other_protos.proto";
复制代码

默认情况下,您只能使用直接导入.proto文件中的定义。但是,有时您可能需要将.proto文件移动到新位置。.proto现在,您可以直接.proto在原位置放置一个虚拟文件,以使用该import public概念将所有导入转发到新位置,而不必一次移动文件并一次更改所有呼叫站点。import public任何导入包含该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标志设置为项目的根,并对所有导入使用完全限定的名称。

(2). 使用proto2消息类型

可以导入proto2消息类型并在proto3消息中使用它们,反之亦然。但是,不能直接在proto3语法中使用proto2枚举(如果导入的proto2消息使用它们,也可以)。

7. 嵌套类型

您可以在其他消息类型中定义和使用消息类型,如以下示例所示–在此处,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;
    }
  }
}
复制代码

8. 更新消息类型

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

  • 不要更改任何现有字段的字段编号。
  • 如果添加新字段,则仍可以使用新生成的代码来解析使用“旧”消息格式通过代码序列化的任何消息。您应该记住这些元素的默认值,以便新代码可以与旧代码生成的消息正确交互。同样,由新代码创建的消息可以由旧代码解析:旧的二进制文件在解析时只会忽略新字段。有关详细信息,请参见“未知字段”部分。
  • 只要在更新后的消息类型中不再使用字段号,就可以删除字段。您可能想要重命名该字段,或者添加前缀“ OBSOLETE_”,或者将字段编号保留,以使您的将来的用户.proto不会意外重用该编号。
  • int32,uint32,int64,uint64,和bool都是兼容的-这意味着你可以在现场从这些类型到另一种改变不破坏forwards-或向后兼容。如果从对应的类型不适合的导线中解析出一个数字,则将获得与在C ++中将数字强制转换为该类型一样的效果(例如,如果将64位数字读为int32,它将被截断为32位)。
  • sint32并且sint64彼此兼容,但与其他整数类型不兼容。
  • string并且bytes只要字节是有效的UTF-8即可兼容。
  • bytes如果字节包含消息的编码版本,则嵌入式消息与之兼容。
  • fixed32与兼容sfixed32,并fixed64用sfixed64。
  • 对于string,bytes和消息字段,optional与兼容repeated。给定重复字段的序列化数据作为输入,如果期望此字段optional是原始类型字段,则期望该字段的客户端将采用最后一个输入值;如果是消息类型字段,则将合并所有输入元素。请注意,这对于数字类型(包括布尔值和枚举)通常并不安全。重复的数字类型字段可以以打包格式序列化,当需要一个optional字段时,将无法正确解析该格式。
  • enum与兼容int32,uint32,int64,和uint64电线格式条款(请注意,如果他们不适合的值将被截断)。但是请注意,客户端代码在反序列化消息时可能会以不同的方式对待它们:例如,无法识别的proto3enum类型将保留在消息中,但是反序列化消息时如何表示这取决于语言。Int字段始终只是保留其值。
  • 将单个值更改为新 值的成员oneof是安全且二进制兼容的。oneof如果您确定没有代码一次设置多个字段,那么将多个字段移动到新字段中可能是安全的。将任何字段移到现有字段中oneof都是不安全的。

9. 未知字段

未知字段是格式正确的协议缓冲区序列化数据,表示解析器无法识别的字段。例如,当旧二进制文件使用新字段解析新二进制文件发送的数据时,这些新字段将成为旧二进制文件中的未知字段。

最初,proto3消息始终在解析过程中丢弃未知字段,但是在版本3.5中,我们重新引入了保留未知字段以匹配proto2行为的功能。在版本3.5和更高版本中,未知字段将在解析期间保留并包含在序列化输出中。

10. 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 ...
  }
}
复制代码

当前,正在开发用于任何类型的运行时库。

如果您已经熟悉proto2语法,则Any可以容纳任意proto3消息,类似于可以允许扩展的proto2消息。

11. Oneof

如果您的消息包含多个字段,并且最多同时设置一个字段,则可以使用oneof功能强制执行此行为并节省内存。

一个字段与常规字段类似,不同之处在于一个共享内存中的所有字段,并且最多可以同时设置一个字段。设置oneof中的任何成员会自动清除所有其他成员。您可以根据所选择的语言,使用特殊case()或WhichOneof()方法来检查其中一个设置的值(如果有)。

(1). 使用Oneof

要在您的文件中定义oneof,请.proto使用oneof关键字,后跟您的oneof名称,在这种情况下test_oneof:

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}
复制代码

然后,将您的oneof字段添加到oneof定义。您可以添加任何类型的map字段,但字段和repeated字段除外。

在生成的代码中,oneof字段具有与常规字段相同的getter和setter。您还将获得一种特殊的方法来检查oneof中的哪个值(如果有)。您可以在相关API参考中找到有关所选语言的oneof API的更多信息。

(2). 功能之一

  • 设置oneof字段将自动清除oneof的所有其他成员。因此,如果您设置多个字段中的一个,则仅您设置的最后一个字段仍将具有值。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message();   // Will clear name field.
CHECK(!message.has_name());
复制代码
  • 如果解析器在线路上遇到同一个对象的多个成员,则在解析的消息中仅使用最后看到的成员。

  • 一个不能是repeated。

  • 反射API适用于其中一个字段。

  • 如果将oneof字段设置为默认值(例如将int32 oneof字段设置为0),则将设置该oneof字段的“大小写”,并且该值将在线路上序列化。

  • 如果您使用的是C ++,请确保您的代码不会导致内存崩溃。以下示例代码将崩溃,因为sub_message已通过调用该set_name()方法将其删除。

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情况下:在下面的例子中,msg1将具有sub_message与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());
复制代码

(3). 向后兼容问题

添加或删除字段之一时请多加注意。如果检查oneof的值返回None/ NOT_SET,则可能意味着尚未设置oneof或已将其设置为oneof的不同版本中的字段。由于无法知道导线上的未知字段是否是oneof的成员,因此无法分辨出两者之间的区别。

标签重用问题

  • 将字段移入或移出oneof:在对消息进行序列化和解析后,您可能会丢失一些信息(某些字段将被清除)。但是,您可以安全地将单个字段移动到新字段中,并且如果已知只设置了一个字段,则可以移动多个字段。
  • 删除一个oneof字段并将其添加回:序列化并解析消息后,这可能会清除您当前设置的oneof字段。
  • 拆分或合并其中一个:与移动常规字段有类似的问题。

12. map(映射mapping)

如果要创建关联映射作为数据定义的一部分,则协议缓冲区提供了一种方便的快捷方式语法:

map<key_type, value_type> map_field = N;
复制代码

...其中key_type可以是任何整数或字符串类型(因此,除浮点类型和以外的任何标量类型bytes)。请注意,枚举不是有效的key_type。的value_type可以是任何类型的除另一map。

因此,例如,如果您想创建一个project map,其中每个Project消息都与一个字符串键相关联,则可以这样定义它:

map<string, Project> projects = 3;
复制代码
  • map字段不能为repeated。
  • map值的线格式排序和地图迭代排序是不确定的,因此您不能依赖于地图项的特定顺序。
  • 为生成文本格式时.proto,map会按键排序。数字键按数字排序。
  • 从导线解析或合并时,如果存在重复的映射键,则使用最后看到的键。从文本格式解析map时,如果键重复,则解析可能会失败。
  • 如果为映射字段提供键但没有值,则序列化字段时的行为取决于语言。在C ++,Java和Python中,类型的默认值是序列化的,而在其他语言中,则没有序列化的值。

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

(1). 向后兼容

映射语法与网上的以下语法等效,因此不支持映射的协议缓冲区实现仍可以处理您的数据:

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

repeated MapFieldEntry map_field = N;
复制代码

任何支持映射的协议缓冲区实现都必须生成并接受可以被上述定义接受的数据。

13. packages

您可以package在.proto文件中添加可选的说明符,以防止协议消息类型之间的名称冲突。

package foo.bar;
message Open { ... }
复制代码

然后,您可以在定义消息类型的字段时使用包说明符:

message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}
复制代码

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

  • 在C ++中,生成的类包装在C ++名称空间中。例如,Open将在命名空间中foo::bar。
  • 在Java中,除非您option java_package在.proto文件中明确提供了,否则该包将用作Java包。
  • 在Python中,package指令将被忽略,因为Python模块是根据其在文件系统中的位置进行组织的。
  • 在Go中,除非您option go_package在.proto文件中明确提供,否则该包将用作Go包的名称。
  • 在Ruby中,生成的类包装在嵌套的Ruby名称空间中,并转换为所需的Ruby大写样式(首字母大写;如果首字符不是字母,PB_则为前缀)。例如,Open将在命名空间中Foo::Bar。
  • 在C#中,除非您option csharp_namespace在.proto文件中明确提供,否则在转换为PascalCase后,该程序包将用作命名空间。例如,Open将在命名空间中Foo.Bar。

(1). 软件包和名称解析

协议缓冲语言中的类型名称解析类似于C ++:首先搜索最内层的作用域,然后搜索下一个最内层的作用,依此类推,每个包都被视为其父包“内在”。领先的“。” (例如,.foo.bar.Baz)表示从最外面的范围开始。

协议缓冲区编译器通过解析导入的.proto文件来解析所有类型名称。每种语言的代码生成器都知道如何引用该语言中的每种类型,即使它具有不同的范围规则。

14. 定义服务

如果要将消息类型与RPC(远程过程调用)系统一起使用,则可以在.proto文件中定义RPC服务接口,并且协议缓冲区编译器将以您选择的语言生成服务接口代码和存根。因此,例如,如果您想使用一种方法来定义RPC服务,该方法接受您的方法SearchRequest并返回SearchResponse,则可以在.proto文件中按以下方式对其进行定义:

service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}
复制代码

与协议缓冲区一起使用的最直接的RPC系统是gRPC:这是Google开发的与语言和平台无关的开源RPC系统。gRPC与协议缓冲区配合使用特别好,它使您可以.proto使用特殊的协议缓冲区编译器插件直接从文件中生成相关的RPC代码。

如果您不想使用gRPC,也可以在自己的RPC实现中使用协议缓冲区。您可以在《Proto2语言指南》中找到有关此内容的更多信息。

还有许多正在进行的第三方项目正在为协议缓冲区开发RPC实现。有关我们知道的项目的链接列表,请参阅第三方加载项Wiki页面。

15. json映射

Proto3支持JSON中的规范编码,从而使在系统之间共享数据更加容易。下表中按类型对编码进行了描述。

如果JSON编码的数据中缺少某个值,或者该值为null,则在解析为协议缓冲区时,它将被解释为适当的默认值。如果字段在协议缓冲区中具有默认值,则默认情况下会在JSON编码的数据中将其省略以节省空间。实现可以提供选项,以在JSON编码的输出中发出具有默认值的字段。

proto3 JSON JSON example Notes
message object {"fooBar": v, "g": null, …} 生成JSON对象。消息字段名被映射到lowerCamelCase并成为JSON对象键。如果指定了' json_name '字段选项,则将使用指定的值作为键值。解析器接受lowerCamelCase名称(或由' json_name '选项指定的名称)和原始字段名称。' null '是所有字段类型的可接受值,并作为相应字段类型的默认值处理。
enum string "FOO_BAR" 使用proto中指定的枚举值的名称。解析器同时接受枚举名称和整数值。
map<K,V> object {"k": v, …} 所有键都被转换为字符串。
repeated V array [v, …] ' null '被接受为空列表'[]'。
bool true, false true, false
string string "Hello World!"
bytes base64 string "YWJjMTIzIT8kKiYoKSctPUB+" JSON值将是使用带填充的标准base64编码的字符串编码的数据。可以接受标准或url安全的base64编码(带/不带填充)。
int32, fixed32, uint32 number 1, -10, 0 JSON值将是一个十进制数。可以接受数字或字符串。
int64, fixed64, uint64 string "1", "-10" JSON值将是一个十进制字符串。可以接受数字或字符串。
float, double number 1.1, -10.0, 0, "NaN", "Infinity" JSON值将是一个数字或特殊字符串值“NaN”、“Infinity”和“-Infinity”中的一个。可以接受数字或字符串。指数表示法也被接受。-0被认为等同于0。
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标准化的,并使用0、3、6或9小数位数。除“Z”外的偏移也是可以接受的。
Duration string "1.000340012s", "1s" 生成的输出总是包含0、3、6或9个小数,这取决于所需的精度,然后加上后缀“s”。只要符合纳秒精度,并且需要后缀“s”,就可以接受任何小数(也可以是零)。
Struct object { … } 任何一个JSON对象。看到“struct.proto”。
Wrapper types various types 2, "2", "foo", true, "true", null, 0, … 包装器在JSON中使用与包装原始类型相同的表示,除了在数据转换和传输期间允许并保留“null”。
FieldMask string "f.fooBar,h" 看到“字段mask.proto”。
ListValue array [foo, bar, …]
Value value 任何JSON值。查看google.protobuf.Value获取详细信息。
NullValue null JSON null
Empty object {} 空JSON对象

(1). json选项

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

  • 发出具有默认值的字段:默认情况下,proto3 JSON输出中会省略具有默认值的字段。一个实现可以提供一个选项,以使用其默认值覆盖此行为和输出字段。
  • 忽略未知字段:默认情况下,Proto3 JSON解析器应拒绝未知字段,但可以提供在解析时忽略未知字段的选项。
  • 使用proto字段名称而不是lowerCamelCase名称:默认情况下,proto3 JSON打印机应将字段名称转换为lowerCamelCase并将其用作JSON名称。一个实现可以提供一个选项,改为使用原型字段名称作为JSON名称。Proto3 JSON解析器必须接受转换后的lowerCamelCase名称和原型字段名称。
  • 将枚举值发送为整数而不是字符串:默认情况下,JSON输出中使用枚举值的名称。可以提供一个选项来代替使用枚举值的数字值。

16. option

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

一些选项是文件级选项,这意味着它们应在顶级范围内编写,而不是在任何消息,枚举或服务定义内。一些选项是消息级别的选项,这意味着它们应该写在消息定义中。一些选项是字段级选项,这意味着它们应在字段定义中编写。选项也可以写在枚举类型,枚举值,字段,服务类型和服务方法中;但是,目前没有针对这些选项的有用选项。

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

  • java_package(文件选项):您要用于生成的Java类的包。如果文件中未提供任何显式java_package选项.proto,则默认情况下将使用原型包(在.proto文件中使用“ package”关键字指定)。但是,proto软件包通常不能成为良好的Java软件包,因为proto软件包不应以反向域名开头。如果未生成Java代码,则此选项无效。
option java_package = "com.example.foo";
复制代码
  • java_outer_classname(文件选项):您要生成的包装Java类的类名(以及文件名)。如果java_outer_classname.proto文件中未指定任何显式名称,则通过将.proto文件名转换为驼峰大小写来构造类名(因此foo_bar.proto成为FooBar.java)。如果java_multiple_files禁用该选项,则所有其他类/枚举/等。为.proto文件生成的文件将在此外部包装Java类中作为嵌套类/枚举/等生成。如果未生成Java代码,则此选项无效。
option java_outer_classname = "Ponycopter";
复制代码
  • java_multiple_files(文件选项):如果为false,.java则将为此.proto文件仅生成一个文件,以及所有Java类/枚举/等。为顶级消息,服务和枚举生成的消息将嵌套在外部类的内部(请参阅参考资料java_outer_classname)。如果为true,.java则将为每个Java类/枚举/等生成单独的文件。是为顶级消息,服务和枚举生成的,而为此.proto文件生成的包装Java类将不包含任何嵌套的类/枚举/等。这是一个布尔选项,默认为false。如果未生成Java代码,则此选项无效。
option java_multiple_files = true;
复制代码
  • optimize_for(文件选项):可以设置为SPEEDCODE_SIZELITE_RUNTIME。这会通过以下方式影响C ++和Java代码生成器(可能还有第三方生成器):
    • SPEED(默认值):协议缓冲区编译器将生成用于对消息类型进行序列化,解析和执行其他常见操作的代码。此代码已高度优化。
    • CODE_SIZE:协议缓冲区编译器将生成最少的类,并将依赖于基于反射的共享代码来实现序列化,解析和其他各种操作。因此,生成的代码将比使用的代码小得多SPEED,但操作会更慢。类仍将实现与SPEED模式下完全相同的公共API 。此模式在包含大量.proto文件且不需要所有文件快速快速运行的应用程序中最有用。
    • LITE_RUNTIME:协议缓冲区编译器将生成仅依赖于“精简版”运行时库(libprotobuf-lite而不是libprotobuf)的类。精简版运行时比完整库要小得多(大约小一个数量级),但省略了某些功能,例如描述符和反射。这对于在受限平台(如手机)上运行的应用程序特别有用。如同在SPEED模式下一样,编译器仍将生成所有方法的快速实现。生成的类将仅以MessageLite每种语言实现该接口,该语言仅提供完整Message接口的方法的子集。
option optimize_for = CODE_SIZE;
复制代码
  • cc_enable_arenas(文件选项):启用C ++生成代码的arena allocation
  • objc_class_prefix(文件选项):设置Objective-C类的前缀,该前缀是所有Object-C生成的类和此.proto枚举的前缀。没有默认值。您应该使用Apple推荐的3-5个大写字符之间的前缀。请注意,所有2个字母前缀均由Apple保留。
  • deprecated(字段选项):如果设置为true,则表明该字段已弃用,并且不应被新代码使用。在大多数语言中,这没有实际效果。在Java中,这成为@Deprecated注释。将来,其他特定于语言的代码生成器可能会在字段的访问器上生成弃用注释,这反过来将导致在编译尝试使用该字段的代码时发出警告。如果该字段未被任何人使用,并且您想阻止新用户使用该字段,请考虑使用保留语句替换该字段声明。
int32 old_field = 6 [deprecated = true];
复制代码

(1). 自订选项

协议缓冲区还允许您定义和使用自己的选项。这是大多数人不需要的高级功能。如果您确实需要创建自己的选项,请参阅《Proto2语言指南》以获取详细信息。请注意,创建自定义选项使用扩展名,只有proto3中的自定义选项才允许使用扩展名

17. 生成classes

要生成在规定的消息类型的使用Java,Python,C ++,Go,Ruby,Objective-C的,或C#的代码的.proto文件,你需要运行协议缓冲编译器protoc.proto。如果尚未安装编译器,请下载软件包并按照自述文件中的说明进行操作。对于Go,您还需要为编译器安装一个特殊的代码生成器插件:您可以在GitHub上的golang / protobuf存储库中找到此代码和安装说明。

协议编译器的调用如下:

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指定.proto解析import指令时要在其中查找文件的目录。如果省略,则使用当前目录。可以通过--proto_path多次传递选项来指定多个导入目录。将按顺序搜索它们。-I=_IMPORT_PATH_可以用作的简写形式--proto_path
  • 您可以提供一个或多个输出指令:
    • --cpp_out在中生成C ++代码DST_DIR。有关更多信息,请参见C ++生成的代码参考
    • --java_out在中生成Java代码DST_DIR。有关更多信息,请参见Java生成的代码参考
    • --python_out在中生成Python代码DST_DIR。有关更多信息,请参见Python生成的代码参考
    • --go_out在中生成Go代码DST_DIR。有关更多信息,请参见Go生成的代码参考
    • --ruby_out在中生成Ruby代码DST_DIR。Ruby生成的代码参考即将推出!
    • --objc_out在中生成Objective-C代码DST_DIR。有关更多信息,请参见Objective-C生成的代码参考
    • --csharp_out在中生成C#代码DST_DIR。有关更多信息,请参见C#生成的代码参考
    • --php_out在中生成PHP代码DST_DIR。参见PHP生成的代码参考以获取更多信息。为方便起见,如果DST_DIR结尾为.zip.jar,编译器会将输出写入具有给定名称的单个ZIP格式的存档文件中。.jar根据Java JAR规范的要求,还将为输出提供清单文件。请注意,如果输出归档文件已经存在,它将被覆盖;否则,输出归档文件将被覆盖。编译器不够智能,无法将文件添加到现有存档中。
  • 您必须提供一个或多个.proto文件作为输入。.proto可以一次指定多个文件。尽管这些文件是相对于当前目录命名的,但是每个文件都必须位于IMPORT_PATHs之一中,以便编译器可以确定其规范名称。

猜你喜欢

转载自juejin.im/post/7120033190508986404