Protocol Buffer 官网文档整理

1-1 前言

网上很多proto2的教程很多,我这里本来再看Netty的源码, 涉及到RPC的框架,加上公司有点项目也涉及的gRpc一些相关的开发,
所以我这里这整理下Protocol Buffers的官方教程。

1-2 定义消息类型

// 官网例子
message SearchRequest {
  required string query = 1;  
  optional int32 page_number = 2;
  optional int32 result_per_page = 3;
}

1-2-2 指定字段类型

上面的SearchRequest消息定义指定了三个字段(名称/值对),一个用于每条数据要在此类型的消息包括。每个字段都有一个名 称和类型。
'query’ 字段的类型是'String‘,'page_number’的字段类型'int32‘,’result_per_page' 的类型是‘int32'。也可以指定其他的类型,在后面的文档中
都会做说明。比如再实际开发过程成会遇到多个传输协议怎末处理等可能遇到的问题。

1-2-3 分配字段编号

消息定义中的每个字段都有一个唯一的编号。这些数字用于标识消息二进制格式的 (/protocol-buffers/docs/encoding) 字段,一旦使用了消息类型,
就不应更改这些数字。请注意,范围为1到15的字段编号需要一个字节来编码,包括字段编号和 字段的类型(您可以在Protocol Buffer Encoding中
 (/protocol-buffers/docs/encoding#structure)找到有关此内容的更多信息)。在 16到2047之间的字段编号占用两个字节。因此,应该为非常频繁出
 现的消息元素保留字段编号1到15。记住要留出一些空间, 以便将来可能会添加一些经常出现的元素。
 
	您可以指定小的场数是1,大为2 29日 - 1,或536870911。您也不能使用数字19000到19999 (FieldDescriptor::kFirstReservedNumber至
FieldDescriptor::kLastReservedNumber),因为它们是为Protocol Buffers实现保留的-如果您在中使用这些保留数之一,则协议缓冲区编译器会抱
怨.proto。同样,您不能使用任何以前保留的  (#reserved)字段号。

1-2-4 指定字段规则

您指定消息字段是以下内容之一:
	required:格式正确的消息必须恰好具有此字段之一。
	optional:格式正确的邮件可以包含零个或一个该字段(但不能超过一个)。
	repeated:在格式正确的邮件中,此字段可以重复任意次(包括零次)。重复值的顺序将保留。
由于历史原因,repeated标量数字类型的字段编码效率不如预期。新代码应使用特殊选项[packed=true]来获得更有效的编 码。
// 官方例子
repeated int32 samples = 4 [packed=true];
在定义字段规则的时候,需要特别注意下,“repeated”官网是不建议使用的,发送必填字段,。然而我们实际开发或者二次开发过程中可能无意
没有添加数据。还有就是,当有人向枚举添加值时,出现第二个带有必填字段的问题。在这种情况下,无法识别的枚举值将被视为丢失,这也会导
致所需 的值检查失败。所以我们在使用的时候需要特别小心时候,甚至于尽量使用optional。

1-2-5 proto文件中定义多个消息类型

虽然可以在单个.proto文件中定义多种消息类型(例如消息,枚举和服务),但当在单个文件中定义大 量具有不同依赖性的消息时,也
可能导致依赖性膨胀。建议每个.proto文件包含尽可能少的消息类型。
syntax = "proto2";

package com.zt.proto;

option optimize_for = SPEED;
option java_package = "com.zt.proto";
option java_outer_classname = "MyDataInfo";

message MyMessage {
  required string name =1;
  optional int32 age=2;
  optional string sex=3;
}

message Dog {
  required string name =1;
  optional int32 age=2;
}
上面例子,可以看到“MyMessage”和“Dog ”这两个我们自己定义的消息类型,你会发现这样很不利于我们实际开发编码,因
为没办法动态的处理的多协议传输类型,除非你写多个判断逻辑。像在Netty中就需要你自己定义多个Handler来手动编码实现
逻辑。还有一种方式就是就是 使用内部枚举类和需要定义的多协议传输类型,结合oneOf 来使用。就可以动态的实现多协议的传输类型,这样你的代码就需要你写一次就ok。那么在文章后面我提供实现的方式。

1-2-6 保留字段

如果您通过完全删除字段或将其注释掉来更新 (#updating)消息类型,将来的用户可以在自己对类型进行更新时重用该字段号。 如果他们以后加载
相同版本的旧版本,可能会导致严重的问题.proto,包括数据损坏,隐私错误等。确保不会发生这种情况的 一种方法是,将已删除字段的字段编(
和/或名称,也可能导致JSON序列化的问题)指定为reserved。请注意,您不能在同reserved一条语句中混用字段名称和字段编号。
message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}

1-2-6 .proto文件生成的是什么

编译器会以您选择的语言生成代码,您将需要使用文件中描述的消息类 型,包括获取和设置字段值,将消息序列化为输出流,并从输入流中解析
	消息。对于Java,编译器将生成一个.java文件,其中包含每种消息类型的类以及Builder用于创建消息类实例的特殊类。 
		下面演示个例子。
syntax = "proto2";

package com.zt.proto;

option optimize_for = SPEED;
option java_package = "com.zt.proto";
option java_outer_classname = "DataInfo";

message Student {
  required string name =1;
  optional int32 age=2;
  optional string adsress=3;
}
## 执行的命令
protoc --java_out=src\main\java src\protobuf\StudentVo.proto
上面生成的 .proto有点大,我就不把源码贴出来,下面是生成的.proto文件的文件结构。其实我们基本的使用就是
Builder和对象。在这里主要指PersonBuilde和Person。

android Protocol Buffer使用 protocol buffer官网_java

1-2-6 导入定义

由于Java中不支持 ,我这里不整理了。

1-3 标量值类型

下面是基本的字段的类型,可以常见的额java类型都是有的。除了一些基本的类型,我们可能在传输的数据更多的为字节数组。传输的方式很多通
过Sokect,Kafka,RabbitMQ,Redis,甚至于Http或Https。

android Protocol Buffer使用 protocol buffer官网_字段_02

1-4 可选字段和默认值

消息描述中的元素可以被标记optional。格式正确的消息可能包含也可能不包含可选元素。解析消息时,如果消息 中不包含可选元素,则解析对象
中的相应字段将设置为该字段的默认值。可以将默认值指定为消息描述的一部分。例如,假设 你想提供的10为默认值SearchRequestresult_per_page
值。
optional int32 result_per_page = 3 [default = 10];
如果未为可选元素指定默认值,则使用特定于类型的默认值:对于字符串,默认值为空字符串。对于字节,默认值为空字节字 符串。对于布尔值,
默认值为false。对于数字类型,默认值为零。对于枚举,默认值为枚举类型定义中列出的第一个值。这意 味着在将值添加到枚举值列表的开头时必须
格外小心。

1-5 枚举

1-5-1 枚举 基本概述

在定义消息类型时,您可能希望其一个字段仅具有一个预定义的值列表之一。例如,假设你想添加一个corpus字段每个 SearchRequest,其中语料
库可以UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO。您可以通过enum在消息定 义中添加来非常简单地执行此操作-具
有enum类型的字段只能使用一组指定的常量作为其值(如果尝试提供其他值,则解 析器会 将其视为未知值领域)。在下面的示例中,我们添加了一
个带有所有可能值的enum被叫项Corpus,以及一个type字段Corpus。
message SearchRequest {   
	required string query = 1;   
	optional int32 page_number = 2;   
	optional int32 result_per_page = 3 [default = 10];   
	enum Corpus {     
		UNIVERSAL = 0;     
		WEB = 1;     
		IMAGES = 2;     
		LOCAL = 3;     
		NEWS = 4;     
		PRODUCTS = 5;     
		VIDEO = 6;   
	}   
optional Corpus corpus = 4 [default = UNIVERSAL]; 
}
可以通过将相同的值分配给不同的枚举常量来定义别名。为此,您需要将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编码 (/protocol-buffers/docs/encoding),因此负值效率低下, 因此不建议使
用。	您可以enum在消息定义内定义,如上面的示例所示,enum也可以在外部定义-这些s可以在.proto文件中的任 何消息定义中重复使用。您还可以
使用enum语法将一条消息中声明的类型用作另一条消息中字段的类型 _MessageType_._EnumType_。当您在.proto使用的上运行协议缓冲区编译器
时enum,生成的代码将具有enumJava或C ++的对应代码,或者具有 EnumDescriptorPython的特殊类,用于在运行时生成的类中创建带有整数值的
符号常量集。

1-5-2 动态多协议消息类型

在上面我谈到类型的时候,我们谈到动态多协议消息类型。可以参考下面
syntax = "proto2";

package com.zt.proto;

option optimize_for = SPEED;
option java_package = "com.zt.proto";
option java_outer_classname = "MyMessageData";

message MyMessageData{

  enum MessageType {
    DOG = 1;
    Cat = 2;
  }
  optional string messageType = 1;
  oneof  messageData{
    Dog dog = 2;
    Cat cat = 3;
  }

}

message Dog{
  optional string name = 1;
  optional string age = 2;
}
message Cat{
  optional string name = 1;
  optional string age = 2;
}

1-5-3 保留值

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

1-6 使用其他消息类型

1-6-1 基本的使用其他消息类型

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

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

1-6-2 导入其他定义

在Java中,官方不支持 ,这里就不做整理

1-6-3 嵌套类型

您可以在其他消息类型内定义和使用消息类型,如以下示例所示–在此处,Result消息是在消息内定义SearchResponse
message SearchResponse {
  message Result {
    required string url = 1;
    optional string title = 2;
    repeated string snippets = 3;
  }
  repeated Result result = 1;
}
如果要在其父消息类型之外重用此消息类型,则将其称为_Parent_._Type_:
message SomeOtherMessage {
  optional SearchResponse.Result result = 1;
}
可以看到下面的层次关系
message Outer {       // Level 0
  message MiddleAA {  // Level 1
    message Inner {   // Level 2
      required int64 ival = 1;
      optional bool  booly = 2;
    }
  }
  message MiddleBB {  // Level 1
    message Inner {   // Level 2
      required int32 ival = 1;
      optional bool  booly = 2;
    }
  }
}

1-6-4 group (弃用),就不做整理

1-6-5 更新消息类型

这个小模块 ,我基本上就跟到官方文档走了。
 如果现有消息类型不再满足您的所有需求(例如,您希望消息格式具有一个额外的字段),但是您仍然希望使用以旧格
 式创建 的代码,请不要担心!在不破坏任何现有代码的情况下更新消息类型非常简单。只要记住以下规则:
  • 不要更改任何现有字段的字段编号。
  • 您添加的任何新字段应为optional或repeated。这意味着任何使用“旧”消息格式通过代码序列化的消息都可以被新生成 的代码解析,因为它们不会丢失任何required元素。您应该为这些元素设置合理的默认值 (#optional),以便新代码与 旧代码生成的消息正确交互。同样,新代码创建的消息也可以由旧代码解析:旧的二进制文件在解析时只会忽略新字段。 但是,未知字段不会被丢弃,并且如果消息随后被序列化,则未知字段也将与之一起进行序列化–因此,如果消息传递给 新代码,则新字段仍然可用。
  • 只要在更新的消息类型中不再使用该字段号,就可以删除不需要的字段。您可能想要重命名该字段,或者添加前缀“
    OBSOLETE_”,或者将字段编号保留为 (#reserved),以便将来的用户.proto不会意外地重用该编号。
  • 只要类型和数字保持不变,就可以将不需要的字段转换为扩展名 (#extensions),反之亦然。
  • 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是原始类型字段,则期望该字段的客户端将采用后一个输入值;如果是消息类型字段,则将合并所有输入元 素。请注意,这不是一般的数值类型,包括布尔变量和枚举安全。重复的数字类型字段可以以打包 (/protocol-buffers/docs/encoding#packed)格式序列化,当需要一个optional字段时,将无法正确解析该格式 .
  • 只要您记得从未通过网络发送默认值,就可以更改默认值。因此,如果程序收到未设置特定字段的消息,则该程序
    将看到 该程序的协议版本中定义的默认值。它不会看到在发送者的代码中定义的默认值。
  • enum与兼容int32,uint32,int64,和uint64电线格式条款(请注意,如果他们不适合的值将被截断),但是要知道, 客户端代码可以区别对待反序列化的消息时。值得注意的是,enum当对消息进行反序列化时,无法识别的值将被丢弃, 这会使字段的has…访问器返回false,并且其getter返回enum定义中列出的第一个值,如果指定了默认值,则返回默认值。对于重复的枚举字段,所有无法识别的值都将从列表中删除。但是,整数字段将始终保留其值。因此,在将 整数升级 为a时,enum在接收导线上的超出枚举值方面需要非常小心。
  • 在当前的Java和C ++实现中,当enum去除了无法识别的值时,它们会与其他未知字段一起存储。请注意,如果将此数据 序列化然后由识别这些值的客户端重新解析,则可能导致奇怪的行为。对于可选字段,即使在反序列化原始消息后写入了 新值,识别该值的客户端仍将读取旧值。在重复字段的情况下,旧值将出现在任何已识别和新添加的值之后,这意味着将 不保留顺序。
  • 在map<K, V>和对应的repeated消息字段之间更改字段是二进制兼容的(有关消息布局和其他限制,请参见下面的Maps (#maps))。但是,更改的安全性取决于应用程序:在反序列化和重新序列化消息时,使用repeated字段定义的客户端将 产生语义上相同的结果;但是,使用map字段定义的客户端可能会对条目进行重新排序,并删除具有重复键的条
    目。

1-6-6 Oneof

如果您的消息包含许多可选字段,并且多将同时设置一个字段,则可以使用oneof功能强制执行此行为并节省内存。
	除了共享内存中的所有字段外,oneof字段类似于可选字段,并且多可以同时设置一个字段。设置oneof中的任何成员
会自动 清除所有其他成员。您可以根据所选择的语言,使用特殊case()或WhichOneof()方法来检查其中一个设置的值(如果有)。
syntax = "proto2";

package com.zt.proto;

option optimize_for = SPEED;
option java_package = "com.zt.proto";
option java_outer_classname = "MyMessageData";

message MyMessageData{

  enum MessageType {
    DOG = 1;
    Cat = 2;
  }
  optional string messageType = 1;
  oneof  messageData{
    Dog dog = 2;
    Cat cat = 3;
  }

}

message Dog{
  optional string name = 1;
  optional string age = 2;
}
message Cat{
  optional string name = 1;
  optional string age = 2;
}

1-6-6 Map

如果要在数据定义中创建关联映射,则proto提供了方便的快捷方式语法:
map<key_type, value_type> map_field = N;
其中key_type可以是任何整数或字符串类型(因此,除浮点类型和以外的任何标量 (#scalar)类型bytes)。请注意,
enum无 效key_type。的value_type可以是任何类型的除另一地图
map<string, Project> projects = 3;
因此,例如,如果您想创建一个项目地图,其中每个Project消息都与一个字符串键相关联,则可以这样定义它:
生成的地图API当前可用于所有proto2支持的语言。您可以在相关API参考中 (/protocol-buffers/docs/reference/overview)			
找到有关所 选语言的map API的更多信息。

MAP功能:

  • 地图不支持扩展。 地图不能repeated,optional或required
  • 地图值的线格式排序和地图迭代排序是不确定的,因此您不能依赖于地图项的特定顺序。
  • 为生成文本格式时.proto,地图会按键排序。数字键按数字排序。
  • 从导线解析或合并时,如果存在重复的映射键,则使用后看到的键。从文本格式解析地图时,如果键重复,则解析可能 会失败。