Protobuf 语言指南(proto3)

时间:2018-10-04 09:59:44   收藏:0   阅读:1563

技术分享图片

Protobuf 语言指南(proto3)

Protocol Buffer是Google的语言中立的,平台中立的,可扩展机制的,用于序列化结构化数据 - 对比XML,但更小,更快,更简单。您可以定义数据的结构化,然后可以使用特殊生成的源代码轻松地在各种数据流中使用各种语言编写和读取结构化数据。

定义消息类型

先来看一个非常简单的例子。假设你想定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数,以及每一页多少条查询结果。可以采用如下的方式来定义消息类型的.proto文件了:

1 syntax = "proto3";
2 
3 message SearchRequest {
4   string query = 1;
5   int32 page_number = 2;
6   int32 result_per_page = 3;
7 }

指定字段类型

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

分配标识号

正如上述文件格式,在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。

最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报错。

指定字段规则

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

在proto3中,repeated数字类型的字段默认使用packed编码。

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

添加更多消息类型

可以在单个.proto文件中定义多种消息类型。当你要定义多个相关消息时,这就很有用 了。 例如,如果要定义与SearchResponse消息类型对应的回复消息格式,那么我在同一个.proto文件中:

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

 

添加注释

要为.proto文件添加注释,请使用C / C ++ - 样式///* ... */语法。

1  / * SearchRequest表示搜索查询,带有分页选项
2    *表明响应中包含哪些结果。* /
3   ?
4   message SearchRequest {
5     string query = 1;
6     int32 page_number = 2; //我们想要哪个页码?
7     int32 result_per_page = 3; //每页返回的结果数。
8   }

 

保留字段

当你在某次更新消息中屏蔽或者删除了一个字段的话,未来的使用着可能在他们的更新中重用这个标签数字来标记他们自己的字段。然后当他们加载旧的消息的时候就会出现很多问题,包括数据冲突,隐藏的bug等等。指定这个字段的标签数字(或者名字,名字可能在序列化为JSON的时候可能冲突)标记为reserved来保证他们不会再次被使用。如果以后的人试用的话protobuf编译器会提示出错。

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

 

注意一个reserved字段不能既有标签数字又有名字。

.proto文件最终生成什么

当你使用protoc 来编译一个.proto文件的时候,编译器将利用你在文件中定义的类型生成你打算使用的语言的代码文件。生成的代码包括getting setting 接口和序列化,反序列化接口。

您可以按照所选语言的教程(即将推出的proto3版本)了解有关为每种语言使用API的更多信息。有关更多API详细信息,请参阅相关API参考(proto3版本即将推出)。

标量值类型

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

.proto typenotesC ++ typeJava typePython type [2]TypeRuby typeC# typePHP type
double   double double float float64 float double float
float   float float float FLOAT32 float float float
INT32 使用可变长度编码。编码负数的效率低 - 如果您的字段可能有负值,请改用sint32。 INT32 INT INT INT32 Fixnum or Bignum (as needed) INT Integer
Int64 使用可变长度编码。编码负数的效率低 - 如果您的字段可能有负值,请改用sint64。 Int64 long int / long [3] Int64 TWINS long Integer/string[5]
UINT32 使用可变长度编码。 UINT32 int [1] int / long [3] UINT32 Fixnum or Bignum (as needed) UINT Integer
UINT64 使用可变长度编码。 UINT64 Long [1] int / long [3] UINT64 TWINS ULONG Integer/string[5]
SINT32 使用可变长度编码。签名的int值。这些比常规int32更有效地编码负数。 INT32 INT INT INT32 Fixnum or Bignum (as needed) INT Integer
sint64 使用可变长度编码。签名的int值。这些比常规int64更有效地编码负数。 Int64 long int / long [3] Int64 TWINS long Integer/string[5]
fixed32 总是四个字节。如果值通常大于2 28,则比uint32更有效。 UINT32 int [1] int / long [3] UINT32 Fixnum or Bignum (as needed) UINT Integer
fixed64 总是八个字节。如果值通常大于2 56,则比uint64更有效。 UINT64 Long [1] int / long [3] UINT64 TWINS ULONG Integer/string[5]
sfixed32 总是四个字节。 INT32 INT INT INT32 Fixnum or Bignum (as needed) INT Integer
sfixed64 总是八个字节。 Int64 long int / long [3] Int64 TWINS long Integer/string[5]
Boolean   Boolean Boolean Boolean Boolean TrueClass / FalseClass Boolean Boolean
string 字符串必须始终包含UTF-8编码或7位ASCII文本。 string string str / unicode[4] string String (UTF-8) string string
byte 可以包含任意字节序列。 string Byte string Strait []byte String (ASCII-8BIT) Byte string string

protobuf 编码序列化消息时,您可以找到有关如何编码这些类型的更多信息。

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

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

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

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

[5] Integer用于64位计算机,字符串用于32位计算机。

默认值

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

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

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

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

枚举

当你定义一个消息的时候,你可能希望它其中的某个字段一定是预先定义好的一组值中的一个。你如说我要在SearchRequest中添加corpus字段。它只能是 UNIVERSAL, WEB , IMAGES , LOCAL, NEWS ,PRODUCTS, 或者 VIDEO 。你可以很简单的在你的消息中定义一个枚举并且定义corpus字段为枚举类型,如果这个字段给出了一个不再枚举中的值,那么解析器就会把它当作一个未知的字段。

在下面的示例中,我们添加了一个带有所有可能值的枚举方法Corpus,以及一个类型的字段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枚举的第一个常量映射为零:每个枚举定义必须包含一个映射到零的常量作为其第一个元素。这是因为:

只需要将相同的值赋值给不同的枚举项名字,你就在枚举中你可以定义别名 。当然你得先将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编码,因此负值效率低,因此不建议使用。您可以enum在消息定义中定义s,如上例所示,enum也可以在外部定义 - 这些可以在.proto文件的任何消息定义中重用。您还可以使用enum语法将一个消息中声明的类型用作另一个消息中的字段类型。 *MessageType*.*EnumType*

如果你想要使用在消息内定义的枚举的话,使用语法 MessageType.EnumType 在你编译带有枚举的.proto文件的时候,如果生成的是C++或者Java代码, 那么生成的代码中会有对应的枚举。

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

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

保留值

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

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

 

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

使用其他消息类型

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

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

 

导入定义

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

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

import“myproject / other_protos.proto”;

 

默认情况下,您只能使用直接导入.proto文件中的定义。但是,有时您可能需要将.proto文件移动到新位置。.proto现在,您可以.proto在旧位置放置一个虚拟文件,以使用该import public概念将所有导入转发到新位置,而不是直接移动文件并在一次更改中更新所有调用站点。import public任何导入包含该import public语句的proto的人都可以传递依赖关系。例如:

 // new.proto
  // All definitions are moved here
  // old.proto
  //This is the proto that all clients are importing.
  import publicnew.proto”;
  import“other.proto”;
  // client.proto
  import "old.proto";
  //您使用old.proto和new.proto中的定义,但不使用other.proto

 

协议编译器使用-I/ --proto_pathflag 在协议编译器命令行中指定的一组目录中搜索导入的文件 。如果没有给出标志,它将查找调用编译器的目录。通常,您应该将--proto_path标志设置为项目的根目录,并对所有导入使用完全限定名称。

使用proto2消息类型

可以导入proto2消息类型并在proto3消息中使用它们,反之亦然。但是,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;
      }
    }
  }

 

更新消息类型

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

未知字段

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

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

任何

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*

不同的语言实现将支持运行时库佣工类型安全的方式打包和解包的任何值-例如,在Java中,任何类型都会有特殊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的任何成员会自动清除所有其他成员。您可以使用特殊case()WhichOneof()方法检查oneof中的哪个值(如果有),具体取决于您选择的语言。

使用Oneof

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

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

 

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

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

Oneof特性

向后兼容性问题

添加或删除其中一个字段时要小心。如果检查oneof返回的值None/ NOT_SET,这可能意味着oneof尚未设置或已在不同版本的oneof的被设置为一个字段。没有办法区分,因为没有办法知道线上的未知字段是否是其中一个成员。

标签重用问题

Maps

如果要在数据定义中创建关联映射,protobuf提供了一种方便的快捷方式语法:

map < key_type ,value_type > map_field = N ;

 

这里key_type可以是任意整形或者字符串。而value_tpye 可以是任意类型。 举个例子,如果你打算创建一个Project表,每个Project关联到一个字符串上的话 :

 map < string ,Project > projects = 3 ;  

 

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

向后兼容性

在通讯中,map等价与下面的定义, 这样不支持Map的版本也可以解析你的消息:

message MapFieldEntry {
    key_type key = 1;
    value_type value = 2;
  }
  ?
  repeated MapFieldEntry map_field = N;

 

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

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

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

 

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

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

 

包名字的实现取决于你工作的具体编程语言:

包和名称解析

协议缓冲区语言中的类型名称解析与C ++类似:首先搜索最里面的范围,然后搜索下一个范围,依此类推,每个包被认为是其父包的“内部”。一个领先的‘。‘ (例如,.foo.bar.Baz)意味着从最外层的范围开始。

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

定义服务

如果要将消息类型与RPC(远程过程调用)系统一起使用,则可以在.proto文件中定义RPC服务接口,protobuf 编译器将使用您选择的语言生成服务接口代码和存根。因此,例如,如果要定义RPC服务请求方法为:SearchRequest和返回方法为:SearchResponse,可以.proto按如下方式在文件中定义它:

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

 

与协议缓冲区一起使用的最简单的RPC系统是gRPC:一种由Google开发的,平台中立的开源RPC系统。gRPC特别适用于protobuf,并允许在您的.proto文件中使用特殊的protobuf 编译器插件直接生成相关的RPC代码。

如果您不想使用gRPC,也可以将protobuf与您自己的RPC实现一起使用。您可以在Proto2语言指南中找到更多相关信息。

还有一些正在进行的第三方项目使用Protocol Buffers开发RPC实现。有关我们了解的项目的链接列表,请参阅第三方加载项wiki页面

JSON映射

Proto3支持JSON中的规范编码,使得在系统之间共享数据变得更加容易。在下表中逐个类型地描述编码。

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

proto3JSONJSON示例笔记
message object {"fooBar": v, "g": null,…} 生成JSON对象。消息字段名称映射到小写驼峰并成为JSON对象键。如果json_name指定了field选项,则指定的值将用作键。解析器接受小写驼峰名称(或json_name选项指定的名称)和原始proto字段名称。null是所有字段类型的可接受值,并将其视为相应字段类型的默认值。
eunm 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 string 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”。接受数字或字符串。指数表示法也被接受。
any object {"@type": "url", "f": v, … } 如果Any包含具有特殊JSON映射的值,则将按如下方式进行转换:。否则,该值将转换为JSON对象,并将插入该字段以指示实际的数据类型。{"@type": xxx, "value": yyy}``"@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" 见。field_mask.proto
ListValue array [foo, bar, …]  
Value value   任何JSON值
NullValue null   JSON null

JSON选项

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

选项

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

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

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

option java_multiple_files = true;
 option java_outer_classname =“Ponycopter”;

 

自定义选项

Protocol Buffers还允许您定义和使用自己的选项。这是大多数人不需要的高级功能。如果您确实认为需要创建自己的选项,请参阅Proto2语言指南以获取详细信息。请注意,创建自定义选项使用的扩展名仅允许用于proto3中的自定义选项。

生成您的类

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

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

其它

[Protocol Buffers 官网?]  https://developers.google.com/protocol-buffers/docs/proto3 Language Guide(proto3)

关于Protobuf 语言指南(proto3)详解到这里就结束了。

对了,也有使用protobuf整合的Netty项目工程地址: https://github.com/sanshengshui/netty-learning-example/tree/master/netty-springboot-protobuf

原创不易,如果感觉不错,希望给个推荐!您的支持是我写作的最大动力!

版权声明: 作者:穆书伟 博客园出处:https://www.cnblogs.com/sanshengshui github出处:https://github.com/sanshengshui     个人博客出处:https://sanshengshui.github.io/

评论(0
© 2014 mamicode.com 版权所有 京ICP备13008772号-2  联系我们:gaon5@hotmail.com
迷上了代码!