目录
- 楔子
- 编写一个简单的 protobuf 文件
- 在服务端之间传输 protobuf
- protobuf 的基础数据类型
- repeat 和 map
- message 的嵌套
- 枚举类型
- .proto 文件的导入
- 一些常用的方法
- 小结
楔子
本次我们来聊一聊 protobuf,它是一个数据序列化和反序列化协议,因此它和 json 的定位是一样的。当客户端需要传递数据给服务端时,会将内存中的对象序列化成一个可以在网络中传输的二进制流,服务端收到之后再反序列化,得到内存中的对象。
不过既然都有 json 了,还会出现 protobuf,那就说明 protobuf 相较于 json 有着很大的优势。来看一下优缺点:
总结一下,protobuf 全称为 Protocol Buffer,它是 Google 开发的一种轻量并且高效的结构化数据存储格式,性能要远远优于 json 和 xmhttp://www.devze.coml。另外 protobuf 经历了两个版本,分别是 protobuf2 和 protobuf3,目前主流的版本是 3,因为更加易用。
下面就来开始学习 protobuf 吧。
但是别忘记安装,直接 pip3 install grpcio grpcio-tools protobuf 即可
编写一个简单的 protobuf 文件
protobuf 文件有自己的语法格式,所以相比 json 它的门槛要高一些。我们创建一个文件,文件名为 girl.proto。
protobuf 文件的后缀是 .proto
//syntax负责指定使用哪一种protobuf服务 //注意:syntax 必须写在非注释的第一行 syntax="proto3"; //包名,这个目前不是很重要,你删掉也是无所谓的 packagegirl; //把 UserInfo 当成 python 中的类 //name 和 age 当成绑定在实例上的两个属性 messageUserInfo{ stringname=1;//=1表示第1个参数 int32age=2; }
protobuf 文件编写完成,然后我们要用它生成相应的 Python 文件,命令如下:
我们要用 protobuf 文件生成 Python 文件,所以 --python_out 负责指定 Python 文件的输出路径,这里是当前目录;-I 表示从哪里寻找 protobuf 文件,这里也是当前目录;最后的 girl.proto 就是指定的 protobuf 文件了。
我们执行该命令,会发现执行完之后多了一个 girl_pb2.py,我们直接用即可。注意:这是基于 protobuf 自动生成的 Python 文件,我们不要修改它。如果参数或返回值需要改变,那么应该修改 protobuf 文件,然后重新生成 Python 文件。
然后我们来看看采用 protobuf 协议序列化之后的结果是什么,不是说它比较高效吗?那么怎能不看看它序列化之后的结果呢,以及它和 json 又有什么不一样呢?
importorjson importgirl_pb2 #在protobuf文件中定义了messageUserInfo #那么我们可以直接实例化它,而参数则是name和age #因为在messageUserInfo里面指定的字段是name和age user_info=girl_pb2.UserInfo(name="satori",age=17) #如果不使用protobuf,那么我们会选择创建一个字典 user_info2={"name":"satori","age":17} #然后来看看序列化之后的结果 #调用SerializeToString方法会得到序列化之后的字节串 print(user_info.SerializeToString()) """ b'\n\x06satori\x10\x11' """ #如果是json的话 print(orjson.dumps(user_info2)) """ b'{"name":"satori","age":17}' """
可以看到使用 protobuf 协议序列化之后的结果要比 json 短,平均能得到一倍的压缩。序列化我们知道了,那么如何反序列化呢?
importorjson importgirl_pb2 #依旧是实例化一个对象,但是不需要传参 user_info=girl_pb2.UserInfo() #传入序列化之后的字节串,进行解析(反序列化) user_info.ParseFromString(b'\n\x06satori\x10\x11') print(user_info.name)#satori print(user_info.age)#17 #json也是同理,通过 loads 方法反序列化 user_info2=orjson.loads(b'{"name":"satori","age":17}') print(user_info2["name"])#satori print(user_info2["age"])#17
所以无论是 protobuf 还是 json,都是将一个对象序列化成二进制字节串。然后根据序列化之后的字节串,再反序列出原来的对象。只不过采用 protobuf 协议进行序列化和反序列化,速度会更快,并且序列化之后的数据压缩比更高,在传输的时候耗时也会更少。
然后还有一个关键地方的就是,json 这种数据结构比较松散。你在返回 json 的时候,需要告诉调用你接口的人,返回的 json 里面都包含哪些字段,以及类型是什么。但 protobuf 则不需要,因为字段有哪些、以及相应的类型,都必须在文件里面定义好。别人只要拿到 .proto 文件,就知道你要返回什么样的数据了,一目了然。
在服务端之间传输 protobuf
如果两个服务需要彼此访问,那么最简单的方式就是暴露一个 HTTP 接口,服务之间发送 HTTP 请求即可彼此访问,至于请求数据和响应数据,则使用 JSON。
所以通过 HTTP + JSON 是最简单的方式,也是业界使用最多的方式。但这种方式的性能不够好,如果是同一个内网的多个服务,那么更推荐使用 gRPC + protobuf。关于 gRPC 以后再聊,我们来看看 protobuf 数据在 HTTP 请求中是如何传递的。
首先还是编写 .proto 文件。
//文件名:girl.proto syntax="proto3"; packagegirl; messageRequest{ stringname=1; int32age=2; } messageResponse{ stringinfo=1; }
一个 protobuf 文件中可以定义任意个 message,在生成 Python 文件之后每个 message 会对应一个同名的类。然后我们执行之前的命令,生成 Python 文件。
接下来使用 Tornado 编写一个服务:
fromabcimportABC fromtornadoimportweb,ioloop importgirl_pb2 classGetInfoHandler(web.RequestHandler,ABC): asyncdefpost(androidself): #拿到客户端传递的字节流 #这个字节流应该是由girl_pb2.Request()序列化得到的 content=self.request.body #下面进行反序列化 request=girl_pb2.Request() request.ParseFromString(content) #获取里面的name和age字段的值 name=request.name age=request.age #生成Response对象 response=girl_pb2.Response( info=f"name:{name},age:{age}" ) #但Response对象不能直接返回,需要序列化 returnawaitself.finish(response.SerializeToString()) app=web.Application( [("/get_info",GetInfoHandler)] ) app.listen(9000) ioloop.IOLoop.current().start()
整个过程很简单,和 JSON 是一样的。然后我们来访问一下:
importrequests importgirl_pb2 #往localhost:9000发请求 #参数是girl_pb2.Request()序列化后的字节流 payload=girl_pb2.Request( name="古明地觉",age=17 ).SerializeToString() #发送HTTP请求,返回girl_pb2.Response()序列化后的字节流 content=requests.post("http://localhost:9000/get_info", data=payload).content #然后我们反序列化 response=girl_pb2.Response() response.ParseFromString(content) print(response.info) """ name:古明地觉,age:17 """
所以 protobuf 本质上也是一个序列化和反序列化协议,在使用上和 JSON 没有太大区别。只不过 JSON 对应的 Python 对象是字典,而 protobuf 则是单独生成的对象。
protobuf 的基础数据类型
在不涉及 gRPC 的时候,protobuf 文件是非常简单的,你需要返回啥结构,那么直接在 .proto 文件里面使用标识符 message 定义即可。
message 消息名称 {
类型 字段名 = 1; 类型 字段名 = 2; 类型 字段名 = 3;}
但是类型我们需要说一下,之前用到了两个基础类型,分别是 string 和 int32,那么除了这两个还有哪些类型呢?
以上是基础类型,当然还有复合类型,我们一会单独说,先来演示一下基础类型。编写 .proto 文件:
//文件名:basic_type.proto syntax="proto3"; packagebasic_type; messageBasicType{ //字段的名称可以和类型名称一致,这里为了清晰 //我们就直接将类型的名称用作字段名 int32int32=1; sint32sint32=2; uint32uint32=3; fixed32fixed32=4; sfixed32sfixed32=5; int64int64=6; sint64sint64=7; uint64uint64=8; fixed64fixed64=9; sfixed64sfixed64=10; doubledouble=11; floatfloat=12; boolbool=13; stringstring=14; bytesbytes=15; }
然后我们来生成 Python 文件,命令如下:
python3 -m grpc_tools.protoc --python_out=. -I=. basic_type.proto
执行之后,会生成 b开发者_JS培训asic_type_pb2.py 文件,我们测试一下:
importbasic_type_pb2 basic_type=basic_type_pb2.BasicType( int32=123, sint32=234, uint32=345, fixed32=456, sfixed32=789, int64=1230, sint64=2340, uint64=3450, fixed64=4560, sfixed64=7890, double=3.1415926, float=2.71, bool=True, string="古明地觉", bytes=b"satori", ) #定义一个函数,接收序列化之后的字节流 defparse(content:bytes): obj=basic_type_pb2.BasicType() #反序列化 obj.ParseFromString(content) print(obj.int32) print(obj.sfixed64) print(obj.string) print(obj.bytes) print(obj.bool) parse(basic_type.SerializeToString()) """ 123 7890 古明地觉 b'satori' True """
很简单,没有任何问题,以上就是 protobuf 的基础类型。然后再来看看符合类型,以及一些特殊类型。
repeat 和 map
repeat 和 map 是一种复合类型,可以把它们当成 Python 的列表和字典。
//文件名:girl.proto syntax="proto3"; packagegirl; messageUserInfo{ //对于Python而言 //repeated表示hobby字段的类型是列表 //string则表示列表里面的元素必须都是字符串 repeatedstringhobby=1; //map<string,string>表示info字段的类型是字典 //字典的键值对必须都是字符串 map<string,string>info=2; }
我们执行命令,生成 Python 文件,然后导入测试一下。
importgirl_pb2 user_info=girl_pb2.UserInfo( hobby=["唱","跳","rap",""], info={"name":"古明地觉","age":"17"} ) print(user_info.hobby) print(user_info.info) """ ['唱','跳','rap','编程客栈'] {'name':'古明地觉','age':'17'} """
结果正常,没有问题。但需要注意:对于复合类型而言,在使用的时候有一个坑。
importgirl_pb2 #如果我们没有给字段传值,那么会有一个默认的零值 user_info=girl_pb2.UserInfo() print(user_info.hobby)#[] print(user_info.info)#{} #对于复合类型的字段来说,我们不能单独赋值 try: user_info.hobby=["唱","跳","rap",""] exceptAttributeErrorase: print(e) """ Assignmentnotallowedtorepeatedfield"hobby"inprotocolmessageobject. """ #先实例化,然后单独给字段赋值,只适用于基础类型 #因此我们需要这么做 user_info.hobby.extend(["唱","跳","rap",""]) user_info.info.update({"name":"古明地觉","age":"17"}) print(user_info.hobby) print(user_info.info) """ ['唱','跳','rap',''] {'name':'古明地觉','age':'17'} """
所以这算是一个需要注意的点,也不能叫坑吧,总之注意一下即可。
message 的嵌套
通过标识符 message 即可定义一个消息体,大括号里面的则是参数,但参数的类型也可以是另一个 message。换句话说,message 是可以嵌套的。
//文件名:girl.proto syntax="proto3"; packagegirl; messageUserInfo{ repeatedstringhobby=1; //BasicInfo定义在外面也是可以的 messageBasicInfo{ stringname=1; int32age=2; stringaddress=3; } BasicInfobasic_info=2; }
生成 Python 文件,导入测试一下。
importgirl_pb2 #在protobuf文件中,BasicInfo定义在UserInfo里面 #所以BasicInfo在这里对应UserInfo的一个类属性 #如果定义在全局,那么直接通过girl_pb2获取即可 basic_info=girl_pb2.UserInfo.BasicInfo( name="古明地觉",age=17,address="地灵殿") user_info=girl_pb2.UserInfo( hobby=['唱','跳','rap',''], basic_info=basic_info ) print(user_info.hobby) """ ['唱','跳','rap',''] """ print(user_info.basic_info.name) print(user_info.basic_info.age) print(user_info.basic_info.address) """ 古明地觉 17 地灵殿 """
以上是 message 的嵌套,或者说通过 message 定义的消息体,也可以作为字段的类型。
枚举类型
再来聊一聊枚举类型,它通过 enum 标识符定义。
//里面定义了两个成员,分别是MALE和FEMALE enumGender{ MALE=0; FEMALE=1; }
这里需要说明的是,对于枚举来说,等号后面的值表示成员的值。比如一个字段的类型是 Gender,那么在给该字段赋值的时候,要么传 0 要么传 1。因为枚举 Gender 里面只有两个成员,分别代表 0 和 1。
而我们前面使用 message 定义消息体的时候,每个字段后面跟着的值则代表序号,从 1 开始,依次递增。至于为什么要有这个序号,是因为我们在实例化的时候,可以只给指定的部分字段赋值,没有赋值的字段则使用对应类型的零值。那么另一端在拿到字节流的时候,怎么知道哪些字段被赋了值,哪些字段没有被赋值呢?显然要通过序号来进行判断。
下面来编写 .proto 文件。
//文件名:girl.proto syntax="proto3"; packagegirl; //枚举成员的值必须是整数 enumGender{ MALE=0; FEMALE=1; } messageUserInfo{ stringname=1; int32age=2; Gendergender=3; } messageGirls{ //列表里面的类型也可以是message定义的消息体 repeatedUserInfogirls=1; }
输入命令生成 Python 文件,然后导入测试:
importgirl_pb2 user_info1=girl_pb2.UserInfo( name="古明地觉",age=17, gender=gijavascriptrl_pb2.Gender.Value("FEMALE")) user_info2=girl_pb2.UserInfo( name="芙兰朵露",age=400, #传入一个具体的值也是可以的 gender=1) girls=girl_pb2.Girls(girls=[user_info1,user_info2]) print(girls.girls[0].name,girls.girls[1].name) print(girls.girls[0].age,girls.girls[1].age) print(girls.girls[0].gender,girls.girls[1].gender) """ 古明地觉芙兰朵露 17400 11 """
枚举既可以定义在全局,也可以定义在某个 message 里面。
.proto 文件的导入
.proto 文件也可以互相导入,我们举个例子。下面定义两个文件,一个是 people.proto,另一个是 girl.proto,然后在 girl.proto 里面导入 people.proto。
/*文件名:people.proto */ syntax="proto3"; //此时的包名就很重要了,当该文件被其它文件导入时 //需要通过这里的包名,来获取内部的消息体、枚举等数据 packagepeople; messageBasicInfo{ stringname=1; int32age=2; } /*文件名:girl.proto */ syntax="proto3"; //导入people.proto, import"people.proto"; messagePersonalInfo{ stringphone=1; stringaddress=2; } messageGirl{ //这里的BasicInfo是在people.proto里面定义的 //people.proto里面的package指定的包名为people //所以这里需要通过people.的方式获取 people.BasicInfobasic_info=1; PersonalInfopersonal_info=2; }
然后执行命令,基于 proto 文件生成 Python 文件,显然此时会有两个 Python 文件。
python3 -m grpc_tools.protoc --python_out=. -I=. girl.proto
python3 -pythonm grpc_tools.protoc --python_out=. -I=. people.proto
importgirl_pb2 importpeople_pb2 basic_info=people_pb2.BasicInfo(name="古明地觉",age=17) personal_info=girl_pb2.PersonalInfo(phone="18838888888", address="地灵殿") girl=girl_pb2.Girl(basic_info=basic_info, personal_info=personal_info) print(girl.basic_info.name)#古明地觉 print(girl.basic_info.age)#17 print(girl.personal_info.phone)#18838888888 print(girl.personal_info.address)#地灵殿
以上就是 proto 文件的导入,不复杂。
一些常用的方法
.proto 文件在生成 .py 文件之后,里面的一个消息体对应一个类,我们可以对类进行实例化。而这些实例化的对象都有哪些方法呢?我们总结一下常用的。
首先重新编写 girl.proto,然后生成 Python 文件。
syntax="proto3"; messagePeople{ stringname=1; int32age=2; } messageGirl{ Peoplepeople=1; stringaddress=2; int32length=3; }
内容很简单,我们测试一下。
importgirl_pb2 girl=girl_pb2.Girl( people=girl_pb2.People(name="古明地觉",age=17), address="地灵殿", length=152 ) # SerializeToString:将对象序列化成二进制字节串 content=girl.SerializeToString() # ParseFromString:将二进制字节串反序列化成对象 girl2=girl_pb2.Girl() girl2.ParseFromString(content) print( girl2.people.name, girl2.people.age, girl2.address, girl2.length )#古明地觉17地灵殿152 #以上两个是最常用的方法 # MergeFrom:将一个对象合并到另一个对象上面 girl=girl_pb2.Girl(address="红魔馆",length=145) #我们先实例化了Girl,后实例化People #接下来要将它绑定到girl的people字段上 people=girl_pb2.People(name="芙兰朵露",age=400) #但girl.people=people是会报错的,因为只有标量才能这么做 #所以我们可以通过girl.people.xxx=people.xxx进行绑定 #但如果people的字段非常多,那么会很麻烦 #因此这个时候可以使用MergeFrom girl.people.MergeFrom(people) print( girl.people.name,girl.people.age )#芙兰朵露400 #同理还有MergeFromString,接收的是序列化之后的字节串 people.name,people.age="魔理沙",15 girl.people.MergeFromString(people.SerializeToString()) print( girl.people.name,girl.people.age )#魔理沙15
非常简单,但我们发现还少了点什么,就是它和 Python 的字典能不能互相转化呢?答案是可以的,但需要导入专门的函数。
fromgoogle.protobuf.json_formatimport( MessageToJson, MessageToDict ) importgirl_pb2 girl=girl_pb2.Girl( people=girl_pb2.People(name="古明地觉",age=17), address="地灵殿", length=152 ) #转成JSON print(MessageToJson(girl)) """ { "people":{ "name":"\u53e4\u660e\u5730\u89c9", "age":17 }, "address":"\u5730\u7075\u6bbf", "length":152 } """ #转成字典 print(MessageToDict(girl)) """ {'people':{'name':'古明地觉','age':17}, 'address':'地灵殿','length':152} """
同理,如果我们有一个字典,也可以转成相应的对象。
fromgoogle.protobuf.json_formatimport( ParseDict ) importgirl_pb2 data={'people':{'name':'魔理沙','age':16}, 'address':'魔法森林','length':156} girl=girl_pb2.Girl() #基于字典进行解析 ParseDict(data,girl) print(girl.people.name)#魔理沙 print(girl.people.age)#16 print(girl.address)#魔法森林 print(girl.length)#156
以上就是工作中的一些常用的方法。
小结
以上就是 protobuf 相关的内容,核心就是编写 .proto 文件,然后生成 Python 文件。它在业务中发挥的作用,和 json 是类似的,都是将对象转成二进制之后再通过网络进行传输。接收方在收到字节流之后,将其反序列化成内存中的对象,然后获取内部的字段。
但是 protobuf 比 json 的性能要优秀很多,并且通过 .proto 文件定义好结构,约束性也要更强一些。
最后补充一点,.proto 文件里面还可以定义很多和 gRPC 相关的内容,关于 gRPC 我们以后再聊。
到此这篇关于Python中应用protobuf的示例详解的文章就介绍到这了,更多相关Python protobuf内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!
精彩评论