前言

之前做游戏开发时,游戏服务端与前端采用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 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

    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

    物联沃分享整理
    物联沃-IOTWORD物联网 » 在Python中使用Protobuf

    发表评论