本次我们来聊一聊protobuf,它是一个数据序列化和反序列化协议,因此它和json的定位是一样的。当客户端需要传递数据给服务端时,会将内存中的对象序列化成一个可以在网络中传输的二进制流,服务端收到之后再反序列化,得到内存中的对象。
不过既然都有json了,还会出现protobuf,那就说明protobuf相较于json有着很大的优势。来看一下优缺点:
总结一下,protobuf全称为ProtocolBuffer,它是Google开发的一种轻量并且高效的结构化数据存储格式,性能要远远优于json和xml。另外protobuf经历了两个版本,分别是protobuf2和protobuf3,目前主流的版本是3,因为更加易用。
下面就来开始学习protobuf吧。
但是别忘记安装,直接pip3installgrpciogrpcio-toolsprotobuf即可
编写一个简单的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又有什么不一样呢?
然后还有一个关键地方的就是,json这种数据结构比较松散。你在返回json的时候,需要告诉调用你接口的人,返回的json里面都包含哪些字段,以及类型是什么。但protobuf则不需要,因为字段有哪些、以及相应的类型,都必须在文件里面定义好。别人只要拿到.proto文件,就知道你要返回什么样的数据了,一目了然。
在服务端之间传输protobuf
如果两个服务需要彼此访问,那么最简单的方式就是暴露一个HTTP接口,服务之间发送HTTP请求即可彼此访问,至于请求数据和响应数据,则使用JSON。
所以通过HTTP+JSON是最简单的方式,也是业界使用最多的方式。但这种方式的性能不够好,如果是同一个内网的多个服务,那么更推荐使用gRPC+protobuf。关于gRPC以后再聊,我们来看看protobuf数据在HTTP请求中是如何传递的。
首先还是编写.proto文件。
//文件名:girl.protosyntax="proto3";packagegirl;messageRequest{stringname=1;int32age=2;}messageResponse{stringinfo=1;}一个protobuf文件中可以定义任意个message,在生成Python文件之后每个message会对应一个同名的类。然后我们执行之前的命令,生成Python文件。
接下来使用Tornado编写一个服务:
fromabcimportABCfromtornadoimportweb,ioloopimportgirl_pb2classGetInfoHandler(web.RequestHandler,ABC):asyncdefpost(self):#拿到客户端传递的字节流#这个字节流应该是由girl_pb2.Request()序列化得到的content=self.request.body#下面进行反序列化request=girl_pb2.Request()request.ParseFromString(content)#获取里面的name和age字段的值name=request.nameage=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是一样的。然后我们来访问一下:
protobuf的基础数据类型
在不涉及gRPC的时候,protobuf文件是非常简单的,你需要返回啥结构,那么直接在.proto文件里面使用标识符message定义即可。
message消息名称{类型字段名=1;类型字段名=2;类型字段名=3;}但是类型我们需要说一下,之前用到了两个基础类型,分别是string和int32,那么除了这两个还有哪些类型呢?
以上是基础类型,当然还有复合类型,我们一会单独说,先来演示一下基础类型。编写.proto文件:
//文件名:basic_type.protosyntax="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-mgrpc_tools.protoc--python_out=.-I=.basic_type.proto
执行之后,会生成basic_type_pb2.py文件,我们测试一下:
repeat和map
repeat和map是一种复合类型,可以把它们当成Python的列表和字典。
//文件名:girl.protosyntax="proto3";packagegirl;messageUserInfo{//对于Python而言//repeated表示hobby字段的类型是列表//string则表示列表里面的元素必须都是字符串repeatedstringhobby=1;//map
message的嵌套
通过标识符message即可定义一个消息体,大括号里面的则是参数,但参数的类型也可以是另一个message。换句话说,message是可以嵌套的。
//文件名:girl.protosyntax="proto3";packagegirl;messageUserInfo{repeatedstringhobby=1;//BasicInfo定义在外面也是可以的messageBasicInfo{stringname=1;int32age=2;stringaddress=3;}BasicInfobasic_info=2;}生成Python文件,导入测试一下。
枚举类型
再来聊一聊枚举类型,它通过enum标识符定义。
//里面定义了两个成员,分别是MALE和FEMALEenumGender{MALE=0;FEMALE=1;}这里需要说明的是,对于枚举来说,等号后面的值表示成员的值。比如一个字段的类型是Gender,那么在给该字段赋值的时候,要么传0要么传1。因为枚举Gender里面只有两个成员,分别代表0和1。
而我们前面使用message定义消息体的时候,每个字段后面跟着的值则代表序号,从1开始,依次递增。至于为什么要有这个序号,是因为我们在实例化的时候,可以只给指定的部分字段赋值,没有赋值的字段则使用对应类型的零值。那么另一端在拿到字节流的时候,怎么知道哪些字段被赋了值,哪些字段没有被赋值呢?显然要通过序号来进行判断。
下面来编写.proto文件。
//文件名:girl.protosyntax="proto3";packagegirl;//枚举成员的值必须是整数enumGender{MALE=0;FEMALE=1;}messageUserInfo{stringname=1;int32age=2;Gendergender=3;}messageGirls{//列表里面的类型也可以是message定义的消息体repeatedUserInfogirls=1;}输入命令生成Python文件,然后导入测试:
importgirl_pb2user_info1=girl_pb2.UserInfo(name="古明地觉",age=17,gender=girl_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)"""古明地觉芙兰朵露1740011"""枚举既可以定义在全局,也可以定义在某个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-mgrpc_tools.protoc--python_out=.-I=.girl.proto
python3-mgrpc_tools.protoc--python_out=.-I=.people.proto
importgirl_pb2importpeople_pb2basic_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)#17print(girl.personal_info.phone)#18838888888print(girl.personal_info.address)#地灵殿以上就是proto文件的导入,不复杂。
一些常用的方法
.proto文件在生成.py文件之后,里面的一个消息体对应一个类,我们可以对类进行实例化。而这些实例化的对象都有哪些方法呢?我们总结一下常用的。
首先重新编写girl.proto,然后生成Python文件。
syntax="proto3";messagePeople{stringname=1;int32age=2;}messageGirl{Peoplepeople=1;stringaddress=2;int32length=3;}内容很简单,我们测试一下。
小结
但是protobuf比json的性能要优秀很多,并且通过.proto文件定义好结构,约束性也要更强一些。