在Python中使用Protobuf

前言

之前做游戏开发时,游戏服务端与前端采用Protobuf来进行数据传输,为了避免被人恶意破解,还对Protobuf产生的数据做了简单的偏移处理。最近又要用到Protobuf了,所以简单记录一下相关内容。

背景知识

我们日常使用最多的数据通信格式应该是JSON,但在一些请求很大的应用上,JSON有2个问题:

  • 1.JSON中有很多业务无关数据,如大括号、中括号等,传输时,这些数据浪费带宽。

  • 2.JSON解码速度慢,量大时,解码对服务器资源占用大了一些。

Google遇到了这些问题,然后提出了Protobuf,其核心目的就是解决上述2个问题。

Protobuf选择二进制编码的形式,将业务数据编码到其中,不会有无关数据,甚至连字段名都不会编码进去,这让带宽压力减小,此外Google自己设计了编码与解码算法,以保证资源占用合理且尽量快速的特点。

此外,Protobuf还有一个福作用:人类对编码后的数据比较难读。这个副作用从一定程度实现数据保护的效果,一些网站利用这个特性实现了反爬。

因为发展原因,Protobuf分为proto2和proto3两个版本,两者是不相兼容的,即使用proto2应用无法与使用proto3的应用通信,本文主要讨论proto3,且不会涉及proto2与proto3的比较。

proto文件语法

使用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 typenotesC ++ typeJava typePython type [2]TypeRuby typeC# typePHP type
double
doubledoublefloatfloat64floatdoublefloat
float
floatfloatfloatFLOAT32floatfloatfloat
INT32使用可变长度编码。编码负数的效率低 – 如果您的字段可能有负值,请改用sint32。INT32INTINTINT32Fixnum or Bignum (as needed)INTInteger
Int64使用可变长度编码。编码负数的效率低 – 如果您的字段可能有负值,请改用sint64。Int64longint / long [3]Int64TWINSlongInteger/string[5]
UINT32使用可变长度编码。UINT32int [1]int / long [3]UINT32Fixnum or Bignum (as needed)UINTInteger
UINT64使用可变长度编码。UINT64Long [1]int / long [3]UINT64TWINSULONGInteger/string[5]
SINT32使用可变长度编码。签名的int值。这些比常规int32更有效地编码负数。INT32INTINTINT32Fixnum or Bignum (as needed)INTInteger
sint64使用可变长度编码。签名的int值。这些比常规int64更有效地编码负数。Int64longint / long [3]Int64TWINSlongInteger/string[5]
fixed32总是四个字节。如果值通常大于2 28,则比uint32更有效。UINT32int [1]int / long [3]UINT32Fixnum or Bignum (as needed)UINTInteger
fixed64总是八个字节。如果值通常大于2 56,则比uint64更有效。UINT64Long [1]int / long [3]UINT64TWINSULONGInteger/string[5]
sfixed32总是四个字节。INT32INTINTINT32Fixnum or Bignum (as needed)INTInteger
sfixed64总是八个字节。Int64longint / long [3]Int64TWINSlongInteger/string[5]
Boolean
BooleanBooleanBooleanBooleanTrueClass / FalseClassBooleanBoolean
string字符串必须始终包含UTF-8编码或7位ASCII文本。stringstringstr / unicode[4]stringString (UTF-8)stringstring
byte可以包含任意字节序列。stringByte stringStrait[]byteString (ASCII-8BIT)Byte stringstring

Protobuf在Python中的基本使用

当下,微服务架构比较流行,Python中在这块常用的通信技术便是Protobuf+gRPC,本文先简单介绍Protobuf,后面再以本文为基础,写一篇Protobuf+gRPC的使用例子。

首先,我们需要下载protoc编译器,通过protoc,我们可以将定义好的proto文件转成相应编程语言的形式。

下载地址:https://github.com/protocolbuffers/protobuf/releases,如果你跟我一样,使用的windows 64位的系统,那么下载win64版本的protoc则可。

13393d276ec7828c23c80fc71b48dc49.png

随后,我们定义一个简单的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的文件。

d6bc0fb4eaa81e243d635cb469e722fe.png

然后我们导入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 = "xxx@xx.com"
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折售卖中哟~

037c7676e3c37984d1e2b8f919fcee84.png

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
心中带点小风骚的头像心中带点小风骚普通用户
上一篇 2023年3月8日 下午9:23
下一篇 2023年3月8日 下午9:24

相关推荐