0%

[Protobuf&gRPC] 1.protobuf简介

目录#

[TOC]

Protobuf是什么#

Protobuf全称是Google Protocol Buffer,是一种高效轻便的结构化数据存储方式,可用于网络通信、数据存储等。

其具有以下优点:

  • 平台无关、语言无关
  • 支持Java, C++, Python等多种语言,支持多平台。
  • 轻便高效,比XML更小(3~10倍),更快(20 ~ 100倍),更为简单。
  • 扩展性,兼容性好
  • 序列化数据结构的协议,可以更新数据结构,而不影响和破坏原有的旧程序。

序列化#

将数据结构或对象转换成能够被存储和传输(例如网络传输)的格式(网络传输传输的是二进制数据),同时应当要保证这个序列化结果在之后(可能是另一个计算环境中)能够被重建回原来的数据结构或对象

protobuf语法介绍#

目前有Protobuf2和Protobuf3。

protobuf2语法简介#

.proto文件中数据类型可以分为两大类:

  • 复合数据类型包括:枚举和message类型
  • 标准数据类型包含:整型,浮点,字符串

数据类型前面修饰词:

  • required: 必须赋值,不能为空,否则该条message会被认为是“uninitialized”。除此之外,“required”字段跟“optional”字段并无差别。
  • optional:字段可以赋值,也可以不赋值。假如没有赋值的话,会被赋上默认值。
  • repeated: 该字段可以重复任意次数,包括0次。重复数据的顺序将会保存在protocol buffer中,将这个字段想象成一个可以自动设置size的数组就可以了。

注:每个字段要给数字 该Number是用来标记该字段在序列化后的二进制数据中所在的field,每个字段的Number在message内部都是独一无二的。也不能进行改变,否则数据就不能正确的解包

关于 proto2 定义 message 消息的更多语法细节,例如具有支持哪些类型,字段编号分配、import 导入定义,reserved 保留字段等知识请参阅 ProtoBuf 官方文档(二)- 语法指引(proto2)

关于定义时的一些规范请参阅 [翻译] ProtoBuf 官方文档(四)- 规范指引

protobuf3语法介绍#

  • 字段前取消了required和optional两个关键字,目前可用的只有repeated关键字。

  • 不可以设置默认值了。

    1. string默认为空串
    2. 枚举默认为第一个枚举定义的第一个值。并且必须是0,必须有有一个0值,我们可以用这个0值作为默认值。这个零值必须为第一个元素,为了兼容proto2语义,枚举类的第一个值总是默认值。
    3. bytes默认为空bytes
    4. bool默认为false
    5. 数字类型默认为0
  • protoType类型如下: double、float、int32、int64、uint32、uint64、sint32、sint64、fixed32、fixed64、sfixed32、sfixed64、bool、string、bytes

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

  • 标识号区间[1,2^29 - 1]。不可以使用其中的[19000-19999](从FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber)的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。同样你也不能使用早期保留的标识号。

  • 指定字段规则 所指定的消息字段修饰符必须是如下之一: singular:一个格式良好的消息应该有0个或者1个这种字段(但是不能超过1个)。 repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。 在proto3中,repeated的标量域默认情况下使用packed。

安装#

使用Python的话简便的安装方法如下(linux)

1
2
pip install protobuf    # 安装protobuf库
sudo apt-get install protobuf-compiler # 安装protobuf编译器

使用#

目前有Protobuf2和Protobuf3,本文以Protobuf3为例。

第一步,创建.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
34
35
36
37
//指定正在使用proto3语法:如果没有指定这个,编译器会使用proto2
//这个指定语法行必须是文件的非空非注释的第一个行
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;
}
}

第二步,protoc 编译 .proto 文件生成读写接口#

我们在 .proto 文件中定义了数据结构,这些数据结构是面向开发者和业务程序的,并不面向存储和传输。

当需要把这些数据进行存储或传输时,就需要将这些结构数据进行序列化、反序列化以及读写。那么如何实现呢?答案就是通过 protoc 这个编译器。

利用protoc.exe编译proto文件,cmd切换到当前目录,执行以下命令:

1
2
3
4
5
// $SRC_DIR: .proto 所在的源目录
// --python_out: 生成 python 代码
// $DST_DIR: 生成代码的目标目录
// xxx.proto: 要针对哪个 proto 文件生成接口代码
protoc -I=$SRC_DIR --python_out=$DST_DIR xxx.proto
这里我们使用如下编译语句
1
protoc -I=. --python_out=./ addressbook.proto

编译好之后你就会在目标目录里面看到输出的结果文件,如下:addressbook_pb2.py

第三步,编译.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
我们就看到了序列化和反序列化的结果

这篇简介就介绍到这,后期会继续

进阶使用#

addressbook.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
34
35
36
37
38
39
syntax = "proto3";
package tutorial;
//引入外部proto
import "emu.proto";


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;
//外部引用map
repeated MyMessage maps = 7;

//内部嵌套message
repeated Hobby hobby = 8;
message Hobby{
string interest = 1;
}

}

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

enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
我们将addressbook.proto中的repeated MyMessage maps = 7;进行了外部引用, emu.proto如下:

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


message MyMessage {
map<int32, int32> mapfield = 1;
}

然后首先对emu.proto进行编译, protoc ./emu.proto --python_out=./

然后会addressbook.proto进行编译 protoc ./addressbook.proto --python_out=./ 然后会默认生成上述截图中的emu_pb2.py、addressbook_pb2.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
39
40
41
42
43
44
45
46
47
48
49
#! /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.add()
maps.mapfield[1] = 1
maps.mapfield[2] = 2

hobby = person.hobby.add()
hobby.interest = "python"


#序列化
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)
print(person.phones[0].number)


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


for hobby in person.hobby:
print(hobby.interest)
最后输出结果如下:
1
2
3
4
5
6
7
8
9
10
/Users/zhiliao/miniconda3/bin/python /Users/zhiliao/zhiliao/untitled1/tutorial/add_person.py
b'\n@\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\x02B\x08\n\x06python' <class 'bytes'>
p_id1,p_namesafly,p_emailsafly@qq.com,p_money1000.1099853515625,p_workstatuTrue
123456 0
123456
2 ------- 2
1 ------- 1
python

Process finished with exit code 0

reference#

  1. python基础--protobuf的使用(一)
  2. Protobuf学习
  3. Protobuf Python 示例

拓展阅读#

  1. 深入 ProtoBuf - 编码
  2. 深入 ProtoBuf - 序列化源码解析
  3. 深入 ProtoBuf - 反射原理解析