1.了解Protobuf
Protocol Buffer是Google的语言中立的,平台中立的,可扩展机制的,用于序列化结构化数据 - 对比XML,但更小,更快,更简单。您可以定义数据的结构化,然后可以使用特殊生成的源代码轻松地在各种数据流中使用各种语言编写和读取结构化数据。
简单的来说,ProtoBuf和json、xml一样是一种结构化的数据格式,用于数据通信的传输及数据的存储。但ProtoBuf相比json和xml来说具有以下的优点:
- 性能好,效率高:是一种二进制的数据格式,比xml小3-5倍,其速度是xml的20-100倍。
- 代码生成机制,数据解析类自动生成:提供了根据proto文件生成对应的源文件代码生成机制。windows(proto.exe)、linux平台动态编译生成
- 支持向后和向前兼容:兼容以前和以后的其他版本,更新数据结构,不影响破坏原有的旧程序。
- 支持多种编译语言:提供了C++、python、java多种语言的支持。
缺点:
- 其内部格式是二进制,导致数据可读性差。
1.1.Protobuf的安装
- 在官网找到适合系统的版本,下载,并解压,我的虚拟机时CentOS7,下载的是protoc-3.20.0-linux-x86_64.zip,解压: unzip protoc-3.20.0-linux-x86_64.zip
- 进入解压后的目录,把./bin/protoc放在/usr/local/bin可执行程序目录中,这样全局都可以访问到,同时把include目录的内容也复制到/usr/local/include/中
- 验证安装:protoc –version (正常有版本信息返回)
1.2.Protobuf的语法
要使用Protobuf,我们首先需要定义我们要传输的消息。消息在.proto
文件内定义。这里先看个.proto的样例(addressbook.proto),之后以此消息格式做应用。
1 syntax = "proto3"; // 指定protobuf语法版本 2 package myProto; /* 指定pkg的包名 */ 3 4 message Person { 5 string name = 1; 6 int32 id = 2; 7 string email = 3; 8 9 enum PhoneType { 10 MOBILE = 0; 11 HOME = 1; 12 WORK = 2; 13 } 14 15 message PhoneNumber { 16 string number = 1; 17 PhoneType type = 0; 18 } 19 20 repeated PhoneNumber phone = 4; 21 22 map<int32, int32> mapfield = 5; 23 } 24 25 message AddressBook { 26 repeated Person person = 1; 27 }
- syntax:语法定义了规范使用哪个版本的Protobuf。这里指定
proto3,如果您不这样做,protobuf 编译器将假定您正在使用proto2。这必须是文件的第一个非空的非注释行。
- import:如果根据另一条消息定义了一条消息,则需要使用
import
语句将其包括在内。 - package:包定义了属于同一名称空间的消息。这样可以防止名称冲突,注意名称需要唯一(如果有多个.proto文件)
- messsage:消息是我们想使用Protobuf建模的一条信息(field消息字段的集合)。
- 注释:proto的注释和C一样,有两种方式: // 或 /* 。。。*/
protobuf2中.proto文件中的数据结构由以下几部分组成:
- 关键字message:代表实体结构,由多个消息字段(field)组成。
- 消息字段: 由数据类型、字段名、字段规则、字段唯一标识、默认值组成。
- 数据类型:
- 复合型数据类型:枚举、map、message类型
- 标准数据类型:整型、浮点、字符串等
- 字段规则:
- required:必须初始化字段,如果没有赋值,在数据序列化时会抛出异常
- optional:可选字段,可以不赋值。如果没有赋值,会使用默认值
- repeated:表示该字段可以重复任意次数,包括0次。重复数据的顺序将会保存在protocol buffer中。
- 字段唯一标识:每个字段都有唯一的数字标识符。用于标记该字段在序列化后的二进制数据中输在的field,每个字段的唯一数字标识符在message内部都是独一无二的。
- 默认值:在定义消息字段时可以给出默认值
Potobuf3与Protobuf2不同的地方:
1、字段规则:
- 字段前取消了required和optional两个关键字,目前只保留了repeated关键字。
- 修饰消息的字段修饰符必须是singular、或repeated。
- singular:一个格式良好的消息应该有0个或者1个这种字段(但是不能超过1个)。
- repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。
- map数据类型前面不能加repeated修饰符
2、取消了设置默认值:
- string默认为字符串
- bytes默认为空bytes
- bool默认为false
- 数字类型默认为0
- 枚举类型默认为第一个枚举定义的第一个值。且第一个值必须为0。(注意:定义为枚举类型的数据,如果值对应的是枚举的第一个(0),则在显示时,默认不显示)
3、支持的数据类型有:
double、float、int32、int64、uint32、uint64、sint32、sint64、fixed32、fixed64、sfixed32、sfixed64、bool、string、bytes
4、分配标识符:
正如上述文件格式,在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。
注意:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。
最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报错。
1.3.Protoc的编译
要使用protobuf的消息协议,则必须把.proto文件编译生成对应语言的文件,我这里生成的是python,在同.proto文件目录下,会生成addressbook_pb2.py,这里是以syntax = "proto3"方式生成,如下图一。(用proto2生成的python代码文件有点不一样)
注意:不要修改此文件
语法:具体根据 protoc --help 查看,或参考官网,下面是简单的命令语法,后面会介绍自动化的Protobuf编译
- protoc addressbook.proto --cpp_out=. # 生成c
- protoc addressbook.proto --python_out=. # 生成python
- protoc addressbook.proto --go_out=. # 生成go,需要另外的程序protoc-gen-go
2.Python的应用
2.1.protobuf模块安装
在正式开发前需要先安装proto的python模块,如下2个命令,在安装好后,我们还需要下载protobuf-all-3.20.0.tar.gz文件(当然也可以用这个包安装protobuf模块),把压缩包中的 protobuf-all-3.20.0.tar.gz/protobuf-3.20.0/python/google/protobuf/internal/builder.py文件复制到python的protobuf模块的相应位置下:./lib/site-packages/google/protobuf/internal/builder.py,因为以proto3生成的python代码,有个builder的模块需要引入,而通过pip安装的protobuf模块,缺少这个builder.py文件。
- pip install google
- pip install protobuf
有了上面protobuf编译生成的python代码文件,以及protobuf模块,我们就可以用应用了。
2.2.用ptyhon填充protobuf对象
在序列化protobuf对象前,需要用数据来填充它,这里以上面的.protoc文件为例填充。填充的方式有2中:
- 以定义protobuf的.protoc文件的格式,一步步把数据填充上,这种方式适合protoc的消息格式不怎么变的情况,因为以这种方式,protocbuf对象的初始化是严格按照.protoc定义的,如果.protoc的格式变化,就需要修改相应程序逻辑;(网络截图)
- 还有一种是通过根据protoc的消息格式编排好的json/dict对象,结合protobuf对象的属性方法,把数据填充上去,这种方法灵活通用,protoc消息格式的改变,只要改变json/dict的数据格式,不需要修改程序代码,就能实现protoc对象的数据填充;
2.3.用Python序列化ProtoBuf对象
把数据填充到protobuf对象后,就可以通过调用SerializeToString()函数来序列化,ParseFromString()函数来反序列化。序列化后以二进制的形式呈现,对于反序列化,类似和protobuf的数据填充一样有2中方式。
注意:通过SerializeToString()函数序列化,返回的是序列化后的二进制数据,而通过ParseFromString()函数反序列化,返回的是序列化后的二进制数据的长度,并不是实际的数据,实际的数据是protobuf对象(代码中的obj对象);
2.4.测试样例
下图中,第一个引入的模块名,为protoc编译生成的python文件的模块名,后面的类名AddressBook为.proto文件中定义的message名称;
第二个引入的是对protobuf对象的操作模块类;
上面执行的结果:
第一个print是dict数据转换填充到protobuf对象,第二个print是把protobuf对象中的数据转换成json数据格式,第三个print是序列化,第四个print是反序列化及序列化后的长度;
1 2 Dict->Protobuf::person { 3 name: "John" 4 id: 1 5 phone { 6 number: "+1234567890" 7 type: WORK 8 maps { 9 mapfield { 10 key: 1 11 value: 11 12 } 13 mapfield { 14 key: 6 15 value: 66 16 } 17 } 18 } 19 phone { 20 number: "+2345678901" 21 type: HOME 22 } 23 } 24 person { 25 name: "Ben Bun" 26 id: 2 27 email: "b@bun.com" 28 phone { 29 number: "+1234567890" 30 maps { 31 mapfield { 32 key: 5 33 value: 55 34 } 35 mapfield { 36 key: 6 37 value: 66 38 } 39 } 40 } 41 } 42 43 Protobuf->Json::{"person": [{"name": "John", "id": 1, "phone": [{"number": "+1234567890", "type": 2, "maps": {"mapfield": [[1, 11], [6, 66]]}}, {"number": "+2345678901", "type": 1}]}, {"name": "Ben Bun", "id": 2, "email": "b@bun.com", "phone": [{"number": "+1234567890", "maps": {"mapfield": [[6, 66], [5, 55]]}}]}]} 44 Protobuf->Byte::b'\n8\n\x04John\x10\x01"\x1d\n\x0b+1234567890\x10\x02\x1a\x0c\n\x04\x08\x01\x10\x0b\n\x04\x08\x06\x10B"\x0f\n\x0b+2345678901\x10\x01\n3\n\x07Ben Bun\x10\x02\x1a\tb@bun.com"\x1b\n\x0b+1234567890\x1a\x0c\n\x04\x08\x06\x10B\n\x04\x08\x05\x107' 45 Byte->Protobuf::person { 46 name: "John" 47 id: 1 48 phone { 49 number: "+1234567890" 50 type: WORK 51 maps { 52 mapfield { 53 key: 1 54 value: 11 55 } 56 mapfield { 57 key: 6 58 value: 66 59 } 60 } 61 } 62 phone { 63 number: "+2345678901" 64 type: HOME 65 } 66 } 67 person { 68 name: "Ben Bun" 69 id: 2 70 email: "b@bun.com" 71 phone { 72 number: "+1234567890" 73 maps { 74 mapfield { 75 key: 5 76 value: 55 77 } 78 mapfield { 79 key: 6 80 value: 66 81 } 82 } 83 } 84 } 85 86 Byte Length=111
3.自动化的ProtoBuf编译
摘自:https://www.cnblogs.com/a00ium/p/14128974.html
在开发过程中,每次更改后必须重新编译原始文件可能会变得很乏味。要在安装开发Python软件包时自动编译原始文件,我们可以使用该setup.py
脚本。
让我们创建一个函数,该函数为.proto
目录中的所有文件生成Protobuf代码src/interfaces
并将其存储在下src/generated
:
1 import pathlib 2 import os 3 from subprocess import check_call 4 5 def generate_proto_code(): 6 proto_interface_dir = "./src/interfaces" 7 generated_src_dir = "./src/generated/" 8 out_folder = "src" 9 if not os.path.exists(generated_src_dir): 10 os.mkdir(generated_src_dir) 11 proto_it = pathlib.Path().glob(proto_interface_dir + "/**/*") 12 proto_path = "generated=" + proto_interface_dir 13 protos = [str(proto) for proto in proto_it if proto.is_file()] 14 check_call(["protoc"] + protos + ["--python_out", out_folder, "--proto_path", proto_path])
接下来,我们需要覆盖develop
命令,以便每次安装软件包时都调用该函数:
1 from setuptools.command.develop import develop 2 from setuptools import setup, find_packages 3 4 class CustomDevelopCommand(develop): 5 """Wrapper for custom commands to run before package installation.""" 6 uninstall = False 7 8 def run(self): 9 develop.run(self) 10 11 def install_for_development(self): 12 develop.install_for_development(self) 13 generate_proto_code() 14 15 setup( 16 name='testpkg', 17 version='1.0.0', 18 package_dir={'': 'src'}, 19 cmdclass={ 20 'develop': CustomDevelopCommand, # used for pip install -e ./ 21 }, 22 packages=find_packages(where='src') 23 )
下次我们运行时pip install -e ./
,Protobuf文件将在中自动生成src/generated
。
4.总结
通过对protoc和google.protobuf模块的学习,让我又多了解了一种数据格式,和json、xml一样,但在性能和效率上比后面2者要高出好多,对于网络I/O、磁盘、内存等资源有限的情况下,用protobuf来作为数据通信的传输和存储是理想的选择。protobuf的数据格式灵活方便,数据格式一目了然,先期的格式定义,便于后期开发人员的理解。
对于protobuf对象数据填充的心得:
传统的protobuf对象数据填充,就像目录树一样,先填充父节点(父目录),再往下填充(子目录/文件),一层一层直到叶子节点(文件)。根据这逻辑,就可以通过protobuf对象的属性方法,根据不同的属性值来判断嵌套迭代处理不同的逻辑,达到不用修改程序就能填充protobuf对象的灵活通用性。
在填充protobuf对象中,map类型是个特例,它不同于其他任何一种数据类型,且数据的赋值方式也不同,通过key,value的键值对存储,类似python的dict(key可以是int类型,也可以是string类型);当然,map类型的数据,也是可以通过定义子message类型(包括2个field字段),嵌套的形式来实现同等的效果,如:
map形式:
1 map<int32, int32> mapfield = 1;
非map形式:
1 message maps { 2 int32 field1 = 1; 3 int32 field2 = 2; 4 } 5 repeated maps mapfield = 1;