Protocol Buffer

Protocol Buffer 是Google提供的一种数据序列化协议,它是一种轻便高效的结构化数据存储格式,可以用于结构化数据序列化,很适合做数据存储或 RPC 数据交换格式。它可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。

相较于XML来说,protobuf 更小更快更简单,支持自定义的数据结构,用 protobuf 编译器生成特定语言的源代码,如 C++、Java、Python、Go等,目前 protobuf 对主流的编程语言都提供了支持,非常方便的进行序列化和反序列化。

Protocol Buffer 序列化之后得到的数据不是可读的字符串,而是二进制流。通过将结构化的数据(拥有多种属性)进行序列化,从而实现(内存与硬盘之间)数据存储和交换的功能。

  • 序列化: 按照 .proto 协议文件将数据结构或对象转换成二进制流的过程;
  • 反序列化:将在序列化过程中所生成的二进制流转换成数据结构或对象的过程。

Protocol Buffer 定义数据格式的文件一般保存在 .proto文件中,每一个 message代表了一类结构化的数据,message 里面定义了每一个属性的类型和名字

Mac 下安装 Protobuf 工具:

1
2
3
4
5
# 安装homebrew
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

# 安装protobuf
brew install protobuf

一、Message 消息结构体

1
2
3
4
5
6
7
8
9
syntax = "proto3"; // 指定proto版本,默认为proto2,而且必须在第一行

package helloworld; // 指定所在包名

message Helloworld {
string name = 1;
int32 id = 2;
int32 opt = 3;
}

如上,一个最简单的 .proto 文件 需要包含的几个要素:

  • proto版本声明:不写时默认为 proto2
  • 包名:package
  • message:消息体

对于消息的结构定义:

1、field 编号

  • 每个 field 都被分配了一个编号,这个编号是这个 field 的唯一标识,
  • 标识 1-15 在编码的时候只占用一个字节,16-2047占用两个字节,所以为了进一步优化我们的程序,对于那种经常出现的 element,建议使用1-15来作为他们的唯一标识,对那些不被经常使用的 element,建议使用16-2047
  • 编号的范围是 1-2^29
  • 19000-19999是系统预留的,不要使用

2、field 类型

Messagefiled 约束类型:

  • required:消息体中必填字段,不设置会导致编解码异常。

  • optional: 消息体中可选字段,可通过default关键字设置默认值。

  • repeated: 消息体中可重复字段,重复的值的顺序会被保留。其中,proto3 默认使用 packed 方式存储,这样编码方式比较节省内存。

    • singular:一个遵循 singular 规则的字段,在一个结构良好的 message 消息体(编码后的message) 可以有0或1个该字段(但是不可以有多个)。这是 proto3 语法的默认字段规则。
  • reserved:在开发过程中可能会涉及到对 proto 文件中 message 各个 fields 的修改,可能是更新、删除某个 field 及其表示,这样可能会导致调用的服务失败。其中一个防止这种问题的方式是,确保你要删除的 field 的标识(或是名字)是reserved ,具体protobuf 的编译器会决定未来这个 field 表示能否被使用

    1
    2
    3
    4
    5
    //数字标识和命名不能在同一条语句中混合声明
    message Foo {
    reserved 2, 15, 9 to 11;
    reserved "foo", "bar";
    }

3、多message结构

在一个proto文件中可以定义多个protobuf

4、默认值

Proto Message 各种数据类型与高级编程语言数据类型的对照在本文末附录。

  • 对于string和byte类型,默认值为空;
  • 对于bool类型,默认值是false;
  • 对于数值类型,默认值是0;
  • 对于枚举类型,默认值是第一个枚举值,默认为0;
  • 对于message类型,默认值由编程语言决定;
  • 对于repeated field,默认值为空

5、枚举类型

1
2
3
4
5
6
7
8
9
10
11
12
message Helloworld {
required string name = 1;
required int32 id = 2;
optional int32 opt = 3;

enum EnumAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
RUNNING = 2;
}
required EnumAllowingAlias alias = 4;
}

通常枚举类型的第一个值初始化为0,而且在 message 中使用枚举类型,必须要给定一个为0的值,而且这个为0的值应该为第一个元素。

也可以给不同的元素以相同的alias,但是需要指定 option allow_alias = true; 如下:

1
2
3
4
5
6
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}

除此之外,枚举类型不仅可以定义在 message 内部,也可以定义在 message 外部,而且在不同 message 中可以重用enum。

6、嵌套和引用其他message结构

(1)嵌套结构
1
2
3
4
5
6
7
8
9
10
message Response {
message User {
required int id = 0;
required string name = 1;
}

required int code = 0;
required string msg = 1;
repeated User users = 2;
}
(2)引用内部message
1
2
3
4
5
6
7
8
9
10
message User {
required int id = 0;
required string name = 1;
}

message Response {
required int code = 0;
required string msg = 1;
repeated User users = 2;
}
(3)跨文件引用message
1
2
3
4
5
// 被引用的文件 user.proto
message User {
required int id = 0;
required string name = 1;
}
1
2
3
4
5
6
7
import "user.proto"; // 引入指定文件

message Response {
required int code = 0;
required string msg = 1;
repeated User users = 2;
}

7、修改 message

当 message 结构因业务变更等原因需要调整时,有如下注意事项

  • 不要更改已经存在的 fields 的数字标识
  • 如果添加新的 field ,利用旧代码序列化得到的 message 可以使用新的代码进行解析,你需要记住各个元素的默认值。新代码创建的 field 同样可以由旧代码进行加解析
  • field 可以被删除,但是需要保证其对应的数字标识不再被使用,你可以通过加前缀的方式来重新使用这个field name,或者指定数字标识为 reserved 来避免这种情况
  • int32、int64、uint32、uint64、bool 这些类型都是互相兼容的,并不会影响前向、后向兼容性
  • sint32 和 sint64之间是互相兼容的,但是和其他数字类型是不兼容的
  • string 和 bytes是互相兼容的,只要使用的是UTF-8编码
  • 如果 byte 包含 message 的编码版本,则嵌套的 message 和 bytes 兼容
  • flexed32 兼容 sfixed32, fixed64, sfixed64
  • enum 兼容 int32, uint32, int64, uint64

二、Service 服务接口

如果想将 Message 用在RPC系统中,需要在 .proto 文件中定义一个 RPC 服务接口,protobuf 编译器会根据所选择语言生成对应语言的服务接口代码及存根。例如定义一个RPC服务并具有一个方法,该方法能够接受 SearchRequest 并返回一个 SearchResponse,此时就可以在 .proto 文件进行如下定义:

1
2
3
4
service SearchService {
//rpc(rpc关键字) 服务函数名 (传入参数) 返回(返回参数)
rpc Search (SearchRequest) returns (SearchResponse) ;
}

gRPC 就是使用 Protobuf 的一个RPC系统,gRPC 在使用 Protobuf 时候非常有效。

三、编译 Proto 文件

1
protoc --go_out=./ path/filename.proto

输出文件:filename.pb.go


参考文档:

1、Protocol Buffer详解(一)

2、Language Guide (proto3)

3、什么是protobuf序列化协议


附:变量类型对照表

.proto Type C++ Type Java/Kotlin Type Python Type 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 int32 int int int32 Fixnum or Bignum (as required) int integer int
int64 int64 long int/long[4] int64 Bignum long integer/string[6] Int64
uint32 uint32 int[2] int/long[4] uint32 Fixnum or Bignum (as required) uint integer int
uint64 uint64 long[2] int/long[4] uint64 Bignum ulong integer/string[6] Int64
sint32 int32 int int int32 Fixnum or Bignum (as required) int integer int
sint64 int64 long int/long[4] int64 Bignum long integer/string[6] Int64
fixed32 uint32 int[2] int/long[4] uint32 Fixnum or Bignum (as required) uint integer int
fixed64 uint64 long[2] int/long[4] uint64 Bignum ulong integer/string[6] Int64
sfixed32 int32 int int int32 Fixnum or Bignum (as required) int integer int
sfixed64 int64 long int/long[4] int64 Bignum long integer/string[6] Int64
bool bool boolean bool bool TrueClass/FalseClass bool boolean bool
string string String str/unicode[5] string String (UTF-8) string string String
bytes string ByteString str (Python 2) bytes (Python 3) []byte String (ASCII-8BIT) ByteString string List