目录

protobuf v2

protobuf 是什么

Protocol Buffer (简称Protobuf) 是Google出品的性能优异、跨语言、跨平台的序列化库。

2001年初,Protobuf首先在Google内部创建,很多项目也采用Protobuf进行消息的通讯,还有基于Protobuf的微服务框架GRPC。

可以看做xml、json等序列化的又一种形式,只不过序列化后它是二进制的。

Protobuf支持很多语言,比如C++、C#、Dart、Go、Java、Python、Rust等,同时也是跨平台的。

序列化(serialization、marshalling)的过程是指将数据结构或者对象的状态转换成可以存储(比如文件、内存)或者传输的格式(比如网络)。反向操作就是反序列化(deserialization、unmarshalling)的过程。

protobuf 为什么要有

二十世纪九十年代后期,XML开始流行,它是一种人类易读的基于文本的编码方式,易于阅读和理解。

JSON是一种更轻量级的基于文本的编码方式,经常用在client/server端的通讯中。

除此之外还有很多序列化格式。

protobuf序列化和反序列化速度更快; 文件更小存储需要更少的空间,传输时间短。

protobuf 基础

这个教程主要介绍proto2的开发。

使用protobuf需要一个.proto文件,在这里定义要序列化的格式。可以理解为我们工作中和服务端定义的接口文档或者java bean。

举例: user.proto

syntax = "proto2";

//生成java类所在的包名
package com.example.protobuftest.bean;

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

第一行指定protobuf的版本,可以指定为proto2或3。如果没有指定,默认以proto2格式定义。

在这里我们定义了一个User类型,包括name和age字段。

package

package是可选的。对于生成的java语言对于Java,包声明符会变为java的一个包,除非在.proto文件中提供了一个明确有java_package; 如果不写package,默认是文件名作为包名。 写了包名后,就可以用包名加以区分。比如test.model.UserInfo。 显示设置包名后生成的对应语言文件就按照这个来生成包名了。

option java_package = "test.protobuf.sample";
option go_package = "test.protobuf.sample";

字段规则

所指定的消息字段修饰符必须是如下之一:

required:一个格式良好的消息一定要含有1个这种字段。表示该值是必须要设置的; optional:消息格式中该字段可以有0个或1个值(不超过1个)。 repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。表示该值可以重复,相当于java中的List。

required是永久性的:在将一个字段标识为required的时候,应该特别小心。如果在某些情况下不想写入或者发送一个required的字段,将原始该字段修饰符更改为optional可能会遇到问题——旧版本的使用者会认为不含该字段的消息是不完整的,从而可能会无目的的拒绝解析。

换句话说,字段设置了required,如果不设置就会序列化失败,同理,如果也会反序列化失败。

字段类型

.proto Type Notes Java Type Go Type
double double float64
float float float32
int32 使用可变长度编码。编码负数的效率低 - 如果您的字段可能有负值,请改用sint32。 int int32
int64 使用可变长度编码。编码负数的效率低 - 如果您的字段可能有负值,请改用sint64。 long int64
uint32 使用可变长度编码 int uint32
uint64 使用可变长度编码. long uint64
sint32 使用可变长度编码。签名的int值。这些比常规int32更有效地编码负数。 int int32
sint64 使用可变长度编码。签名的int值。这些比常规int64更有效地编码负数。 long int64
fixed32 总是四个字节。如果值通常大于228,则比uint32更有效。 int uint32
fixed64 总是八个字节。如果值通常大于256,则比uint64更有效 long uint64
sfixed32 总是四个字节 int int32
sfixed64 总是八个字节 long int64
bool boolean bool
string String string
bytes 可以包含不超过232的任意字节序列。 String []byte

标识号

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

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

类型嵌套及导入

在一个.proto文件中可以定义多个消息类型,可以引用,也可以嵌套

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

要导入其他.proto文件的定义,你需要在你的文件中添加一个导入声明,如:

import "myproject/other_protos.proto";

嵌套使用:

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

如果你想在它的父消息类型的外部重用这个消息类型,你需要以Parent.Type的形式使用它

Optional的字段和默认值

如上所述,消息描述中的一个元素可以被标记为“可选的”(optional)。一个格式良好的消息可以包含0个或一个optional的元素。当解 析消息时,如果它不包含optional的元素值,那么解析出来的对象中的对应字段就被置为默认值。默认值可以在消息描述文件中指定。

optional int32 result_per_page = 3 [default = 10];

如果没有为optional的元素指定默认值,就会使用与特定类型相关的默认值:对string来说,默认值是空字符串。对bool来说,默认值是false。对数值类型来说,默认值是0。对枚举来说,默认值是枚举类型定义中的第一个值。

枚举

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

枚举常量必须在32位整型值的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。

扩展

通过扩展,可以将一个范围内的字段标识号声明为可被第三方扩展所用。然后,其他人就可以在他们自己的.proto文件中为该消息类型声明新的字段,而不必去编辑原始文件了。

message Foo {
  // ...
  extensions 100 to 199;
}

在消息Foo中,范围[100,199]之内的字段标识号被保留为扩展用。现在,其他人就可以在他们自己的.proto文件中添加新字段到Foo里了。

extend Foo {
  optional int32 bar = 126;
}

消息Foo现在有一个名为bar的optional int32字段。然而,要在程序代码中访问扩展字段的方法与访问普通的字段稍有不同。

如果你的消息中有很多可选字段, 并且同时至多一个字段会被设置, 你可以加强这个行为,使用oneof特性节省内存.

Oneof

Oneof字段就像可选字段, 除了它们会共享内存, 至多一个字段会被设置。 设置其中一个字段会清除其它oneof字段。

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

oneof中字段不能使用 required, optional, repeated 关键字.

Map

如果你希望创建一个关联映射,protocol buffer提供了一种快捷的语法:

map<key_type, value_type> map_field = N;

其中key_type可以是任意Integer或者string类型(所以,除了floating和bytes的任意标量类型都是可以的)value_type可以是任意类型。

  • Map的字段不可以是repeated,optional,required。
  • 序列化后的顺序和map迭代器的顺序是不确定的,所以你不要期望以固定顺序处理Map

定义服务(Service)

如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口。

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