之前做游戏开发时,游戏服务端与前端采用Protobuf来进行数据传输,为了避免被人恶意破解,还对Protobuf产生的数据做了简单的偏移处理。最近又要用到Protobuf了,所以简单记录一下相关内容。
我们日常使用最多的数据通信格式应该是JSON,但在一些请求很大的应用上,JSON有2个问题:
1.JSON中有很多业务无关数据,如大括号、中括号等,传输时,这些数据浪费带宽。
2.JSON解码速度慢,量大时,解码对服务器资源占用大了一些。
Google遇到了这些问题,然后提出了Protobuf,其核心目的就是解决上述2个问题。
Protobuf选择二进制编码的形式,将业务数据编码到其中,不会有无关数据,甚至连字段名都不会编码进去,这让带宽压力减小,此外Google自己设计了编码与解码算法,以保证资源占用合理且尽量快速的特点。
此外,Protobuf还有一个福作用:人类对编码后的数据比较难读。这个副作用从一定程度实现数据保护的效果,一些网站利用这个特性实现了反爬。
因为发展原因,Protobuf分为proto2和proto3两个版本,两者是不相兼容的,即使用proto2应用无法与使用proto3的应用通信,本文主要讨论proto3,且不会涉及proto2与proto3的比较。
使用Protobuf通信的第一步,便是定义出proto文件,我们先展示一个简单的proto文件,如下:
message Person{
required string name = 1;
message Info{
required int32 id = 1;
repeated string phonenumber = 2;
}
repeated Info info = 2;
}
先思考一下,为啥需要proto文件?为何不能像JSON那样直接使用呢?
回顾一下Protobuf其中一个优点:只编码业务相关的数据到待传输的二进制流中,以节省带宽。
Protobuf实现这个特点的原理是:通信双方手里都拿着定义好的proto文件。
发送方通过这个proto文件定义发送的内容,这些内容不会需要像JSON那样,有字段名、有括号这些,接收方收到数据后,再利用手里的proto文件,按算法直接解析里面的数据。
我一直强调,JSON传输时,会有字段名、有括号,可能有人会比较懵逼,还是举个例子,如下JSON:
{"name": "ayuliao", "age": 30}
这段JSON在传输时,name和age这两个字段名也会被传输,但这两个字段名没啥业务意义,主要就是用来获取数据的,假设这段数据使用Protobuf传输,Protobuf就不会将name和age编码到数据流里,只会将ayuliao与30编码进去,接收方获得数据后,手里有一份与发送方一样的proto文件,然后按proto文件中的格式进行解码。
思索一下,要在没有字段名的情况下,合理的解码Protobuf数据,就必须要求传输数据是按一定规则组织的,比如ayuliao必须在30前,这样我会先解码ayuliao,再解码30,至此,我们引入proto文件的一个语法要求,message结构里的分配标识号不可重复。
看到上面的proto文件,有两个messge结构,外部的message结构是Person,Person中有有name与info两个属性,其中name的分配标识号为1,info的分配标识号为2,在同一个message中,分配标识号不可重复,否则protobuf无法正常使用。
此外,从上面proto文件中也可以发现,message中不同属性可以有不同的限定修饰符,有3种:
required:发送方发送的数据中必须包含这个字段的值,接收方接收的数据也必须要能识别该字段,大白话,加上required修饰符,这个字段双方必须使用,否则报错。
optional:可选字段,发送方可选择性地发送该字段,接收方如果能够识别该字段就进行相应解码处理,如果不能识别,则直接忽略。
repeated:可重复字段,发送方每次发送都可以包含多个值,类似于传递一个数组。
在日常使用protobuf时,有两个常见的tips:
1.分配标识号一般会按业务划分,不同业务间字段不按大小顺序紧密排序,如:
message data {
optional string name = 10001;
optional int32 age = 10002;
optional string job = 20001;
optional string hobby = 20002;
}
上述proto,基础信息(name、age)以1000开头,其他信息(job、hobby)以2000开头,这样后续要添加时,更加清晰,比如要添加性别这个基本信息 :optional string sex = 10003;。
2.很多使用protobuf通信的系统会将字段的限定修饰符设置为optional,这样系统在升级时,旧版程序无需升级也可以与新程序进行通信,只是对于新字段无法识别而已,这样可以做到平滑升级。
这里从网上摘抄了proto文件可以使用的数据类型以及生成到不同编程语言时,在该编程语言中映射的类型,不需记忆,需要时,到此翻一下就好了。
.proto type | notes | C ++ type | Java type | Python type [2] | Type | Ruby type | C# type | PHP type |
---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | float | double | float | |
float | float | float | float | FLOAT32 | float | float | float | |
INT32 | 使用可变长度编码。编码负数的效率低 - 如果您的字段可能有负值,请改用sint32。 | INT32 | INT | INT | INT32 | Fixnum or Bignum (as needed) | INT | Integer |
Int64 | 使用可变长度编码。编码负数的效率低 - 如果您的字段可能有负值,请改用sint64。 | Int64 | long | int / long [3] | Int64 | TWINS | long | Integer/string[5] |
UINT32 | 使用可变长度编码。 | UINT32 | int [1] | int / long [3] | UINT32 | Fixnum or Bignum (as needed) | UINT | Integer |
UINT64 | 使用可变长度编码。 | UINT64 | Long [1] | int / long [3] | UINT64 | TWINS | ULONG | Integer/string[5] |
SINT32 | 使用可变长度编码。签名的int值。这些比常规int32更有效地编码负数。 | INT32 | INT | INT | INT32 | Fixnum or Bignum (as needed) | INT | Integer |
sint64 | 使用可变长度编码。签名的int值。这些比常规int64更有效地编码负数。 | Int64 | long | int / long [3] | Int64 | TWINS | long | Integer/string[5] |
fixed32 | 总是四个字节。如果值通常大于2 28,则比uint32更有效。 | UINT32 | int [1] | int / long [3] | UINT32 | Fixnum or Bignum (as needed) | UINT | Integer |
fixed64 | 总是八个字节。如果值通常大于2 56,则比uint64更有效。 | UINT64 | Long [1] | int / long [3] | UINT64 | TWINS | ULONG | Integer/string[5] |
sfixed32 | 总是四个字节。 | INT32 | INT | INT | INT32 | Fixnum or Bignum (as needed) | INT | Integer |
sfixed64 | 总是八个字节。 | Int64 | long | int / long [3] | Int64 | TWINS | long | Integer/string[5] |
Boolean | Boolean | Boolean | Boolean | Boolean | TrueClass / FalseClass | Boolean | Boolean | |
string | 字符串必须始终包含UTF-8编码或7位ASCII文本。 | string | string | str / unicode[4] | string | String (UTF-8) | string | string |
byte | 可以包含任意字节序列。 | string | Byte string | Strait | []byte | String (ASCII-8BIT) | Byte string | string |
当下,微服务架构比较流行,Python中在这块常用的通信技术便是Protobuf+gRPC,本文先简单介绍Protobuf,后面再以本文为基础,写一篇Protobuf+gRPC的使用例子。
首先,我们需要下载protoc编译器,通过protoc,我们可以将定义好的proto文件转成相应编程语言的形式。
下载地址:https://github.com/protocolbuffers/protobuf/releases,如果你跟我一样,使用的windows 64位的系统,那么下载win64版本的protoc则可。
随后,我们定义一个简单的proto文件,名为demo.proto,内容如下:
syntax = "proto3";
message Person {
string name = 1;
int32 id = 2;
string email = 3;
string phone = 4;
}
通过protoc将demo.proto编译成Python文件,命令如下(protoc路径替换成自己的路径则可):
& "C:\Users\admin\Downloads\protoc-21.1-win64\bin\protoc.exe" --python_out=. demo.proto
--python_out用于指定生成Python文件要存放的路径,随后紧接demo.proto文件路径(注意有空格做间隔)。
运行命令后,会生成名为demo_pd2.py的文件。
然后我们导入demo_pd2文件,使用其中的Person类便可以实现Protobuf的编码与解码。
通过一段简单的代码演示一下:
from asyncore import read
import demo_pb2
R = 'read'
W = 'write'
def fwrb(filepath, opt, data=None):
if opt == R:
with open(filepath, 'rb') as f:
return f.read()
elif opt == W:
with open(filepath, 'wb') as f:
f.write(data)
filepath = './person_protobuf_data'
person = demo_pb2.Person()
person.name = "ayuliao"
person.id = 6
person.email = "[email protected]"
person.phone = "13229483229"
person_protobuf_data = person.SerializeToString()
print(f'person_protobuf_data: {person_protobuf_data}')
fwrb(filepath, W, person_protobuf_data)
data_from_file = fwrb(filepath, R)
parse_person = demo_pb2.Person()
# 没有数据
print(f'parse_person1 : {parse_person}')
# ParseFromString函数返回解析数据的长度
parse_data_len = parse_person.ParseFromString(data_from_file)
# 解析后,parse_person对象被填充了数据
print(f'parse_person2 : {parse_person}')
print(f'parse_data_len: {parse_data_len}')
print(f'data_from_file lenght: {len(data_from_file)}')
上述代码中,需要注意的是,ParseFromString函数不会直接返回解析后的结果,而是将结果直接填充到调用它的parse_person对象中。
本文讨论了protobuf基础知识,protobuf在通信质量要求比较高的应用中会比较常见,但我个人的观点依旧是:如无必要勿增实体,如果应用没有到达需要使用protobuf的地步,还是优先使用HTTP吧。
最后,对于Python入门、Python自动化感兴趣的同学,可以入手《Python自动化办公》这本书籍,目前5折售卖中哟~