美文网首页
ProtocolBuffer浅析

ProtocolBuffer浅析

作者: 艾鱼先生 | 来源:发表于2019-12-19 20:24 被阅读0次

    ProtocolBuffer浅析

    背景

    ProtocolBuffer是google 定义的一种数据交换的格式,它独立于语言,独立于平台。google 提供了多种语言的实现:java、c#、c++、go 和 python,每一种实现都包含了相应语言的编译器以及库文件。ProtocolBuffer类似于xml、json,不过它更小、更快、也更简单。

    与Json对比

    目前使用最广泛的数据传输协议为JSON,JSON是一种轻量级的数据交换格式而且层次和结构比较简单和清晰,这里主要对比一下Protocol Buffer和JSON的对比,给出优势和劣势:

    优势

    • 传输数据更小

    • 序列化和反序列化更快

    • 由于传输的过程中使用的是二进制,没有结构描述文件,无法解析内容,安全性更高

    劣势

    • 由于传输过程使用的是二进制,自解释性较差,需要原有的结构描述文件才能解析

    实际数据对比

    • 序列化速度:比JSON快20-100倍

    • 数据大小:序列化后体积小3倍

    使用流程


    Protocol Buffer的使用流程总体可以分为三步,如下图所示:

    image
    1. 根据业务创建并定义proto文件

    2. 使用Google Protocol Buffer 提供的工具生成对应语言的源文件

    3. 将源文件拷贝到工程中,使用Protocol Buffer提供的库序列化或反序列化数据

    Android中使用

    1. 在项目根目录的build.gradle中添加依赖:

          dependencies {
              classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8'
          }
      
    2. 在app的gradle中添加插件

      apply plugin: 'com.google.protobuf'//添加插件
      
    3. 在app的build.gradle添加如下代码

      protobuf {
          protoc {
              artifact = 'com.google.protobuf:protoc:3.7.1' // 也可以配置本地编译器路径
          }
      
          generateProtoTasks {
              all().each { task ->
                  task.builtins {
                      java {}// 生产java源码
                  }
              }
          }
      }
      
    4. 添加proto文件的路径

      sourceSets {
          main {
              proto {
                  srcDir 'src/main/proto'
              }
              java {
                  srcDir 'src/main/java'
              }
          }
      }
      
    5. 添加protobuf-java和protoc的依赖,其中protoc的依赖很重要,lite版中不需要添加

      implementation 'com.google.protobuf:protobuf-java:3.7.1'
      implementation 'com.google.protobuf:protoc:3.7.1'
      

    google推荐在Android项目中使用lite版,lite版本生成的java文件更加轻量,其配置如下:

    protobuf {
        //配置protoc编译器
        protoc {
            artifact = 'com.google.protobuf:protoc:3.8.0'
        }
        //这里配置生成目录,编译后会在build的目录下生成对应的java文件
        generateProtoTasks {
            all().each { task ->
                task.builtins {
                    java {
                        option "lite"
                    }
                }
            }
        }
    }
    dependencies {
      implementation 'com.google.protobuf:protobuf-javalite:3.8.0'
    }
    

    语法解析


    简单示例

    首先创建一个.proto文件,并且在文件中声明如下内容:

    syntax = "proto3";      //标明当前proto使用的版本为proto3
    
    option java_package = "com.jon.protocol.protobuf";  //输出的java文件包名
    option java_outer_classname = "Test";               //输出的java文件名称
    
    message TestRequest{
        string name = 1;    //名称
        int32 count = 2;
    }
    

    字段解析

    在整个proto文件中数据类型分为基本类型和结构类型,其中结构类型主要为:

    • message
    • enum
    • map

    下面分别介绍一下不同结构的作用及规定:

    message

    message表示一个结构,类似于java中类,一个proto文件中可以声明多个message结构:

    message SearchRequest {
      string query = 1;
      int32 page_number = 2;
      int32 result_per_page = 3;
    }
    
    message SearchResponse {
     ...
    }
    
    import

    message可以引用不同proto文件中的message,只要在proto文件中的最上面声明import即可,如下所示:

    import "test.proto";
    

    enum

    enum使用很简单,直接在message中声明enum结构体并且将属性声明为对应的enum即可:

    message EnumRequest {
        Corpus corpus = 1;
        enum Corpus {
            UNIVERSAL = 0;
            WEB = 1;
            IMAGES = 2;
            LOCAL = 3;
            NEWS = 4;
            PRODUCTS = 5;
            VIDEO = 6;
        }
    }
    

    在proto3中,enum第一个值必须为0,主要是为了和基础类型的默认值保持一致

    map

    map是proto3新加的,使用也很简单:

    map<key_type, value_type> map_field = N;
    //示例如下
    message MapRequest {
        map<string, TestRequest> map = 1;
    }
    

    基础类型

    如下

    .proto Type Notes C++ Type Java Type Python Type[2] Go Type Ruby Type C# Type PHP Type Dart Type
    double double double float float64 Float double float double
    float float float float float32 Float float float double
    int32 Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. int32 int int int32 Fixnum or Bignum (as required) int integer int
    int64 Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. int64 long int/long[3] int64 Bignum long integer/string[5] Int64
    uint32 Uses variable-length encoding. uint32 int[1] int/long[3] uint32 Fixnum or Bignum (as required) uint integer int
    uint64 Uses variable-length encoding. uint64 long[1] int/long[3] uint64 Bignum ulong integer/string[5] Int64
    sint32 Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. int32 int int int32 Fixnum or Bignum (as required) int integer int
    sint64 Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. int64 long int/long[3] int64 Bignum long integer/string[5] Int64
    fixed32 Always four bytes. More efficient than uint32 if values are often greater than 228. uint32 int[1] int/long[3] uint32 Fixnum or Bignum (as required) uint integer int
    fixed64 Always eight bytes. More efficient than uint64 if values are often greater than 256. uint64 long[1] int/long[3] uint64 Bignum ulong integer/string[5] Int64
    sfixed32 Always four bytes. int32 int int int32 Fixnum or Bignum (as required) int integer int
    sfixed64 Always eight bytes. int64 long int/long[3] int64 Bignum long integer/string[5] Int64
    bool bool boolean bool bool TrueClass/FalseClass bool boolean bool
    string A string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232. string String str/unicode[4] string String (UTF-8) string string String
    bytes May contain any arbitrary sequence of bytes no longer than 232. string ByteString str []byte String (ASCII-8BIT) ByteString strin

    默认值

    • string:空串
    • bytes:空字节
    • bool:false
    • 数字类型:0
    • enum:默认值是第一个元素,且值必须为0

    repeated

    repeated修饰的属性类似于jsonArray,也类似于java中的List,该修饰符在格式正确的消息中可以重复任意次(包括0次)

    message RepeatRequest {
        repeated TestRequest requests = 1;
    }
    

    字段编号

    1. 字段编号从1开始,不可重复定义
    2. 字段编号1-15尽量保持经常访问的字段使用,因为1-15编号在传输的过程中只占用1个字节

    字段扩充

    日常开发过程中,由于需求的变更,往往需要增加字段,这就涉及到字段的扩充,字段扩充需要达到一个目的:兼容

    所以Protocol Buffer在字段扩充中定义了如下规则:

    1. 不要修改已经存在的字段标号

    2. 不用的字段,可以删除,但是编号一定不可以再次使用,建议将字段标为废弃,如加前缀:"OBSOLETE_", 或者标记该字段为reserved

      reserved 2;
      

    只要记住上述规则,就能完成字段扩充且老版本也能兼容

    原理简介

    Protocol Buffer 更快更小的主要原因如下:

    1. 数据在序列化的时候不会传输字段名,只会传输字段标号,并且没有被设置值的字段是不会序列化和传输
    2. 采用可变长度编码,优化数据占用
    message TestRequest{
        string name = 1;    //名称
        int32 count = 2;
    }
    

    上面这个例子中,在序列化时,"name" 、"count"的key值不会参与,由编号1、2代替,这样在反序列化的时候直接通过编号找到对应的key就可以。需要注意的是编号一旦确定就不可以更改,服务端和客户端通过proto通信的时候需要提前定义号数据格式。

    • 没有赋值的key,不参与序列化
      序列化时只会对赋值的key进行序列化,没有赋值的不参与,在反序列化的时候直接给默认值即可
    • 可变长度编码
      可变长度编码,主要缩减整数占用字节实现,例如java中int占用4个字节,但是大多数情况下,我们使用的数字都比较小,使用1个字节就够了,这就是可变长度编码完成的事
    • TLV
      TLV全称为Tag-Length-Value,其中Tag表示后面数据的类型,Length不一定有,根据Tag的值确定,Value就是数据了,TLV表示数据时,减少分隔符的使用,更加紧凑

    T-L-V的数据存储方式

    image

    其中Length不一定有,依据Tag确定,例如int类型的数据就只有Tag-Value,string类型的数据就必须是Tag-Length-Value。

    数据类型

    Protocol Buffer定义了如下的数据类型,其中部分数据类型已经不再使用:

    类型 释义 备注
    0 可变长度编码 int32 int64 uint32 uint64 sint32 sint64 bool enum
    1 64位长度 fixed64 sfixed64 double
    2 value 的长度 string bytes message packed repeated fiels
    3 Start Group 废弃
    4 End Group 废弃
    5 32位长度 fixed32 sfixed32 float

    Tag

    上面已经介绍了Protocol Buffer的数据结构及Tag的类型,但是Tag块并不是只表示数据类型,其中数据编号也在Tag块中,Tag的生成规则如下:

    (field_number << 3) | wire_type
    

    其中Tag块的后3位表示数据类型,其他位表示数据编号

    可变长度编码

    Java中整数类型的长度都是确定的,如int类型的长度为4个字节,可表示的整数范围为-231——231-1,但是实际开发中用到的数字均比较小,会造成字节浪费,可变长度编码就能很好的解决这个问题,可变长度编码规则如下:

    • 字节最高位表示数据是否结束,如果最高位为1,则表示后面的字节也是该数据的一部分

    举个例子:


    image

    其中第一个字节由于最高位为1,则后面的字节也是前面的数据的一部分,第二个字节最高位为0,则表示数据计算终止,由于Protocol Buffer是低位在前,整体的转换过程如下:

    image

    10000001 00000011 ——> 00000110000001 表示的10进制数为:2^0 + 2^7 + 2^8 = 385 通过上面的例子可以知道一个字节表示的数的范围0-128,上面介绍的Tag生成算法中由于后3位表示数据类型,所以Tag中1-15编号只占用1个字节,所以确保编号中1-15为常用的,减少数据大小。

    可变长度编码唯一的缺点就是当数很大的时候int32需要占用5个字节,但是从统计学角度来说,一般不会有这么大的数.

    案例分析

    上面介绍了Protocol Buffer的原理,现在通过实例来展示分析过程,我们定义的proto文件如下:

    message TestRequest{
        string name = 1;    //名称
        int32 count = 2;
    }
    

    其序列化后的字节数据如下:

    testproto.png

    前面介绍过Protocol Buffer的数据结构为TLV,其中L不是必须的,根据T的类型来确定 先看下第一个字节:

    image1

    这里字节最高位为0,所以该Tag就用这一个字节表示,其中后3位表示类型,前面表示字段编号,所以:

    这里字节最高位为0,所以该Tag就用这一个字节表示,其中后3位表示类型,前面表示字段编号,所以: file_num = 0001 = 1 type = 010 = 2 上面介绍过type=2,则后面有Length,按照可变长度编码规则,知道表示长度的字节为:

    image2

    所以Length=4,则value的长度是4个字节,直接取出后面4个字节:

    image3

    这4个字节对应的就是test 再看下一组:

    image4

    由上面的Tag知道: file_num=2 type=0 前面介绍过type=0,后面没有Length,直接就是value,

    image5

    value=1,通过上面的解析可以知道

    1. file_num=1 value=test

    2. file_num=2 value=1 这样解析就结束了

    上面介绍了Protocol Buffer的原理,解释了为什么Protocol Buffer更快,更小,这里再总结一下:

    1. 序列化的时候,不序列化key的name,只序列化key的编号

    2. 序列化的时候,没有赋值的key,不参与序列化,反序列化的时候直接使用默认值填充

    3. 可变长度编码,减小字节占用

    4. TLV编码,去除没有的符号,使数据更加紧凑

    参考资料:

    proto3官网指南:https://developers.google.com/protocol-buffers/docs/proto3

    protobuf-gradle-plugin:https://github.com/google/protobuf-gradle-plugin
    博客:https://juejin.im/post/5dcbf630e51d451bfe5bb21b

    相关文章

      网友评论

          本文标题:ProtocolBuffer浅析

          本文链接:https://www.haomeiwen.com/subject/ewevnctx.html