Python中应用protobuf的示例详解
古明地觉 人气:0楔子
本次我们来聊一聊 protobuf,它是一个数据序列化和反序列化协议,因此它和 json 的定位是一样的。当客户端需要传递数据给服务端时,会将内存中的对象序列化成一个可以在网络中传输的二进制流,服务端收到之后再反序列化,得到内存中的对象。
不过既然都有 json 了,还会出现 protobuf,那就说明 protobuf 相较于 json 有着很大的优势。来看一下优缺点:
总结一下,protobuf 全称为 Protocol Buffer,它是 Google 开发的一种轻量并且高效的结构化数据存储格式,性能要远远优于 json 和 xml。另外 protobuf 经历了两个版本,分别是 protobuf2 和 protobuf3,目前主流的版本是 3,因为更加易用。
下面就来开始学习 protobuf 吧。
但是别忘记安装,直接 pip3 install grpcio grpcio-tools protobuf 即可
编写一个简单的 protobuf 文件
protobuf 文件有自己的语法格式,所以相比 json 它的门槛要高一些。我们创建一个文件,文件名为 girl.proto。
protobuf 文件的后缀是 .proto
// syntax 负责指定使用哪一种 protobuf 服务 // 注意:syntax 必须写在非注释的第一行 syntax = "proto3"; // 包名, 这个目前不是很重要, 你删掉也是无所谓的 package girl; // 把 UserInfo 当成 Python 中的类 // name 和 age 当成绑定在实例上的两个属性 message UserInfo { string name = 1; // = 1表示第1个参数 int32 age = 2; }
protobuf 文件编写完成,然后我们要用它生成相应的 Python 文件,命令如下:
我们要用 protobuf 文件生成 Python 文件,所以 --python_out 负责指定 Python 文件的输出路径,这里是当前目录;-I 表示从哪里寻找 protobuf 文件,这里也是当前目录;最后的 girl.proto 就是指定的 protobuf 文件了。
我们执行该命令,会发现执行完之后多了一个 girl_pb2.py,我们直接用即可。注意:这是基于 protobuf 自动生成的 Python 文件,我们不要修改它。如果参数或返回值需要改变,那么应该修改 protobuf 文件,然后重新生成 Python 文件。
然后我们来看看采用 protobuf 协议序列化之后的结果是什么,不是说它比较高效吗?那么怎能不看看它序列化之后的结果呢,以及它和 json 又有什么不一样呢?
import orjson import girl_pb2 # 在 protobuf 文件中定义了 message UserInfo # 那么我们可以直接实例化它,而参数则是 name 和 age # 因为在 message UserInfo 里面指定的字段是 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 短,平均能得到一倍的压缩。序列化我们知道了,那么如何反序列化呢?
import orjson import girl_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"; package girl; message Request { string name = 1; int32 age = 2; } message Response { string info = 1; }
一个 protobuf 文件中可以定义任意个 message,在生成 Python 文件之后每个 message 会对应一个同名的类。然后我们执行之前的命令,生成 Python 文件。
接下来使用 Tornado 编写一个服务:
from abc import ABC from tornado import web, ioloop import girl_pb2 class GetInfoHandler(web.RequestHandler, ABC): async def post(self): # 拿到客户端传递的字节流 # 这个字节流应该是由 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 对象不能直接返回,需要序列化 return await self.finish(response.SerializeToString()) app = web.Application( [("/get_info", GetInfoHandler)] ) app.listen(9000) ioloop.IOLoop.current().start()
整个过程很简单,和 JSON 是一样的。然后我们来访问一下:
import requests import girl_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"; package basic_type; message BasicType { // 字段的名称可以和类型名称一致,这里为了清晰 // 我们就直接将类型的名称用作字段名 int32 int32 = 1; sint32 sint32 = 2; uint32 uint32 = 3; fixed32 fixed32 = 4; sfixed32 sfixed32 = 5; int64 int64 = 6; sint64 sint64 = 7; uint64 uint64 = 8; fixed64 fixed64 = 9; sfixed64 sfixed64 = 10; double double = 11; float float = 12; bool bool = 13; string string = 14; bytes bytes = 15; }
然后我们来生成 Python 文件,命令如下:
python3 -m grpc_tools.protoc --python_out=. -I=. basic_type.proto
执行之后,会生成 basic_type_pb2.py 文件,我们测试一下:
import basic_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", ) # 定义一个函数,接收序列化之后的字节流 def parse(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"; package girl; message UserInfo { // 对于 Python 而言 // repeated 表示 hobby 字段的类型是列表 // string 则表示列表里面的元素必须都是字符串 repeated string hobby = 1; // map<string, string> 表示 info 字段的类型是字典 // 字典的键值对必须都是字符串 map<string, string> info = 2; }
我们执行命令,生成 Python 文件,然后导入测试一下。
import girl_pb2 user_info = girl_pb2.UserInfo( hobby=["唱", "跳", "rap", "
加载全部内容
- 猜你喜欢
- 用户评论