protobuf一种适合传输的序列化方案

前言

protobuf是Google提出的序列化方案,此方案独立于语言和平台,目前提供了如c++,go,python等多种语言的实现,使用比较广泛,具有性能开销小,压缩率高等特点

protobuf有V2和V3两个主要差异很大的版本。下面会介绍两种版本的特点

proto2

这个版本在编写 .proto 文件时的字段有三种限定符,分别是requiredoptional repeated

  • required:必须设置该字段,如果是在debug模式下编译 libprotobuf,则序列化一个未初始化(未对required字段赋值)的 message 将导致断言失败。在release模式的构建中,将跳过检查并始终写入消息,但解析未初始化的消息将返回false表示失败。
  • optional:可以设置也可以不设置该字段。如果未设置可选字段值,则使用默认值,也可以用[default = value]进行设置。
  • repeated:该字段可以重复任意次数(包括零次),可以将 repeated 字段视为动态大小的数组。
    message定义
    定义一个简单的 message 结构如下:
1
2
3
4
5
6
message Person {
required string name = 1;
optional string email = 2;
optional int age = 3 [default = 18];
repeated bytes phones = 4;
}

观察 message 定义可以看到每个字段后面都有 = 1= 2 的标记,这些被称为 Tags,在 protobuf 中同一个 message 中的每个字段都需要有独一无二的tag,tag 为 1-15 的是单字节编码,16-2047 使用2字节编码,所以1-15应该给频繁使用的字段。

关于tag的取值,还有一种范围是[1,536870911]的说法,同时 19000 到 19999 之间的数字也不能使用,因为它们是 protobuf 的实现中保留的,也就是 FieldDescriptor::kFirstReservedNumber FieldDescriptor::kLastReservedNumber 指定的范围,如果使用其中的数字,导出 .proto 文件时会报错,此处存疑,需要验证一下。

message扩展

在使用的了 protobuf 的项目发布以后,绝对会遇到扩展原有 message 结构的需求,这一点不可避免,除非发布后的项目不再升级维护了,要想扩展就需要兼容之前的代码逻辑,这里有一些必须遵守的规则,否则就达不到兼容的目的。

  • 不能更改任何现有字段的 tag
  • 不能添加或删除任何 required 字段
  • 可以删除 optional 或 repeated 的字段
  • 可以添加新的 optional 或 repeated 字段,但必须使用新的tag,曾经使用过又删除的 tag 也不能再使用了

注意事项

proto2中对required的使用永远都应该非常小心。如果想在某个时刻停止写入或发送required字段,直接将字段更改为可选字段将会有问题。一些工程师得出的经验是,使用required弊大于利,它们更喜欢只使用optional和repeated。

proto3

proto3比proto2支持更多语言但更简洁,去掉了一些复杂的语法和特性。

  • 在第一行非空白非注释行,必须写:syntax = “proto3”;
  • 直接从语法层面上移除了 required 规则,取消了 required 限定词
  • 增加了对 Go、Ruby、JavaNano 等语言的支持
  • 移除了 default 选项,字段的默认值只能根据字段类型由系统决定

一个完整的例子

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

1
2
3
4
5
6
7
syntax = "proto3";

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

文件的第一行指定了你正在使用proto3语法:如果你没有指定这个,编译器会使用proto2。这个指定语法行必须是文件的非空非注释的第一个行。
SearchRequest消息格式有3个字段,在消息中承载的数据分别对应于每一个字段。其中每个字段都有一个名字和一种类型。

接下来我们看一个整体的例子吧,以下是demo的结构

  1. 第一步,编写addressbook.proto文件,注意以.proto结尾
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
syntax = "proto3";
package tutorial;

message AddressBook {
repeated Person people = 1;
}

message Person {
string name = 1;
int32 id = 2;
string email = 3;
float money = 4;
bool work_status = 5;

repeated PhoneNumber phones = 6;
MyMessage maps = 7;

}

message PhoneNumber {
string number = 1;
PhoneType type = 2;
}

enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}

message MyMessage {
map<int32, int32> mapfield = 1;
}
  1. 第二步,编译addressbook.proto文件
1
2
3
4
zhiliaodeMBP:protobuf_python zhiliao$ protoc ./addressbook.proto  --python_out=./
zhiliaodeMBP:protobuf_python zhiliao$ pwd
/Users/zhiliao/zhiliao/untitled1/protobuf_python
zhiliaodeMBP:protobuf_python zhiliao$

编译完毕,会自动生成addressbook_pb2.py文件
另外protoc ./addressbook.proto –python_out=./这是输入的命令行指令,意思是在当前目录输出默认的即可,也即是截图里面的addressbook_pb2.py

  1. 第三步,编译.py文件,进行序列化和凡序列化
    add_person.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#! /usr/bin/env python
from tutorial import addressbook_pb2

address_book = addressbook_pb2.AddressBook()
person = address_book.people.add()

person.id = 1
person.name = "safly"
person.email = "safly@qq.com"
person.money = 1000.11
person.work_status = True

phone_number = person.phones.add()
phone_number.number = "123456"
phone_number.type = addressbook_pb2.MOBILE

maps = person.maps
maps.mapfield[1] = 1
maps.mapfield[2] = 2

#序列化
serializeToString = address_book.SerializeToString()
print(serializeToString,type(serializeToString))



address_book.ParseFromString(serializeToString)

for person in address_book.people:
print("p_id{},p_name{},p_email{},p_money{},p_workstatu{}"
.format(person.id,person.name,person.email,person.money,person.work_status))

for phone_number in person.phones:
print(phone_number.number,phone_number.type)


for key in person.maps.mapfield:
print(key,person.maps.mapfield[key])

编译该py文件,输出结果如下:

1
2
3
4
5
6
b'\n6\n\x05safly\x10\x01\x1a\x0csafly@qq.com%\n\x07zD(\x012\x08\n\x06123456:\x0c\n\x04\x08\x01\x10\x01\n\x04\x08\x02\x10\x02' <class 'bytes'>

p_id1,p_namesafly,p_emailsafly@qq.com,p_money1000.1099853515625,p_workstatuTrue
123456 0
1 1
2 2

我们就看到了序列化和反序列化的结果