Google Protobuf 编解码

Google Protobuf 优点:

  • 在谷歌内部长期使用, 产品成熟度高.
  • 跨语言、支持多种语言, 包括 C++、Java 和 Python.
  • 编码后的消息更小, 更加有利于存储和传输.
  • 编解码的性能非常高.
  • 支持不同协议版本的前向兼容.
  • 支持定义可选和必选字段.

Protobuf 的入门

Protobuf 是一个灵活、高效、结构化的数据序列化框架, 相比与 xml 等传统的序列化工具, 它更小、更快、更简单.

Protobuf 支持数据结构化一次可以到处使用, 甚至跨语言使用, 通过代码生成工具可以自动生成不同语言版本的源代码, 甚至可以在使用不同版本的数据结构进程间进行数据传递, 实现数据结构前向兼容.

定义消息类型

syntax = "proto3";

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

该文件的第一行指定使用 proto3 语法, 如果不写的话表示 proto2.

分配字段编号

string query = 1; 1 就是字段编号, 字段号主要用来标识二进制格式字段的. 1 到 15 字段号占一个字节. 16 到 2047 字段号需要两个字节.

我们将对象转换为报文的时候, 是按照字段编号进行报文封装的; 我们接收到数据之后框架会帮我们按照字段号进行赋值.

不能使用数字19000到19999, 因为它们是为 Google Protobuf 保留的.

字段类型对应

.proto TypeNotesC++ TypeJava Type
double doubledouble
float floatfloat
int32使用可变长度编码, 对负数编码效率低下
如果您的字段可能有负值, 则使用sint32代替.
int32int
int64使用可变长度编码, 对负数编码效率低下
如果您的字段可能有负值, 则使用sint64代替.
int64long
uint32使用可变长度编码uint32int
uint64使用可变长度编码uint64 long
sint32使用可变长度编码
有符号的int值这些编码比常规int32更有效地编码负数
uint32int
sint64使用可变长度编码
有符号的int值这些编码比常规int64更有效地编码负数
int64long
fixed32四个字节, 如果值通常大于2的28次方, 则比uint32更有效uint32int
fixed64四个字节, 如果值通常大于2的56次方, 则比uint64更有效uint64long
sfixed32四个字节int32int
sfixed64四个字节int64long
bool boolboolean
string字符串必须始终包含UTF-8编码或7位ASCII文本stringString
bytes字符串必须始终包含UTF-8编码或7位ASCII文本stringByteString

默认值

  • 对于字符串, 默认值是空字符串.
  • 对于字节, 默认值为空字节.
  • 对于bool, 默认值为false.
  • 对于数字类型, 默认值为零.
  • 对于枚举, 默认值是第一个定义的枚举值, 必须为0.
还请注意, 如果消息字段设置为默认值, 则该值将不会序列化.

允许嵌套

Protocol Buffers 定义 message 允许嵌套组合成更加复杂的消息

message SearchResponse {
  repeated Result results = 1;
}

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

更多的例子:

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

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

导入定义

可以在文件的顶部添加一个import语句:

import "myproject/other_protos.proto";

未知字段

未知字段就是解析器无法识别的字段. 例如, 当服务端使用新消息发送数据, 客户端使用旧消息解析数据, 那么这些新字段将成为旧消息中的未知字段.

在3.5和更高版本中, 未知字段在解析过程中被保留, 并包含在序列化中输出.

Map 类型

repeated 类型可以用来表示数组, Map 类型则可以用来表示字典.

map<key_type, value_type> map_field = N;

map<string, Project> projects = 3;

key_type 可以是任何 int 或者 string 类型(任何的标量类型, 具体可以见上面标量类型对应表格, 但是要除去 floatdoublebytes)

枚举值也不能作为 key.

key_type 可以是除去 Map 以外的任何类型.

需要特别注意的是:

  • map 是不能用 repeated 修饰的.
  • map 迭代顺序的是不确定的, 所以你不能确定 map 是一个有序的.
  • .proto 生成文本格式时, map 按 key 排序. 数字的 key 按数字排序.
  • 从数组中解析或合并时, 如果有重复的 key, 则使用所看到的最后一个 key(覆盖原则).从文本格式解析映射时, 如果有重复的 key, 解析可能会失败.

Protocol Buffer 虽然不支持 map 类型的数组, 但是可以转换一下, 用以下思路实现 maps 数组:

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

repeated MapFieldEntry map_field = N;

上述写法和 map 数组是完全等价的,所以用 repeated 巧妙的实现了 maps 数组的需求.

Protocol Buffer 命名规范

message 采用驼峰命名法. message 首字母大写开头. 字段名采用下划线分隔法命名.

message SongServerRequest {
  required string song_name = 1;
}

枚举类型采用驼峰命名法. 枚举类型首字母大写开头. 每个枚举值全部大写, 并且采用下划线分隔法命名.

enum Foo {
  FIRST_VALUE = 0;
  SECOND_VALUE = 1;
}

每个枚举值用分号结束, 不是逗号.

服务名和方法名都采用驼峰命名法. 并且首字母都大写开头.

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

常用方法

getDefaultInstance(): 返回单例实例, 它与 newBuilder().build() 实例相同
getDescriptor(): 返回类型的描述符. 包括具有哪些字段以及类型. 这可以与 Message 的反射方法一起使用, 例如getField().
parseFrom(...): 返回反序列化后的 Message. 注意不会抛出 UninitializedMessageExceptionInvalidProtocolBufferException 异常.
Message.Builder: 中的 mergeFrom() 放会将数据解析为此类型的消息, 并进行消息合并.
newBuilder(): 创建一个新的构建器.

Any

Any类型允许包装任意的message类型:

import "google/protobuf/any.proto";

message Response {
    google.protobuf.Any data = 1;
}

总结

message SubscribeReq {
  int32 subReqID = 1;
  string userName = 2;
  string productName = 3;
  string address = 4;
}

可以通过 pack()unpack()(方法名在不同的语言中可能不同)方法装箱/拆箱,以下是Java的例子:

People people = People.newBuilder().setName("proto").setAge(1).build();
// protoc编译后生成的message类
Response r = Response.newBuilder().setData(Any.pack(people)).build();
// 使用Response包装people

System.out.println(r.getData().getTypeUrl());
// type.googleapis.com/example.protobuf.people.People
System.out.println(r.getData().unpack(People.class).getName());
// proto

Oneof

如果你有一些字段同时最多只有一个能被设置, 可以使用 oneof 关键字来实现, 任何一个字段被设置, 其它字段会自动被清空(被设为默认值):

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

默认值

比如我们创建了上面的消息类型, 我们在代码中设置 builder.setSubReqID(0); 为 0, 零是数值类型的默认值; 所以我们会看到序列化后的数据中, 没有对此字段进行序列化.

byte[] arry = builder.build().toByteArray();

arry 长度为 0. 对于字段类型是 string 类型的也是一样的; 也就是说显示赋值默认值也不会对其进行序列化.

保留字段

message SubscribeReq {
  
  reserved 2;
  
  int32 subReqID = 1;
  string userName = 2;
  string productName = 3;
  string address = 4;
}

顾名思义, 就是此字段会被保留可能在以后会使用此字段. 使用关键字 reserved 表示我要保留字段数 2.

上面代码我们在生成 Java 文件的时候会出现 ubscribeReqPeoro.proto: Field "userName" uses reserved number 2 错误信息, 所以我们需要将 string userName = 2; 注释, 或者删除.

保留后我们无法对其设置或序列化和反序列化.

相关推荐