Protobuf数据编码工具应用分析



一、引言

最近,在接触的蓝牙语音交互项目(AMA、DMA、GMA)中,平台端的数据交互及解析与平常接触的项目不同,以前主要是JSON,个别项目用XML,而这次用的是Proto格式,数据解析用的是Protobuf,对自已来说算是陌生的东西,于是在应用的过程中就顺便把Protobuf的使用方法及经验作下总结。

JSON、XML是目前常用的数据交换格式,它们直接使用字段名称维护序列化后类实例中字段与数据之间的映射关系,一般用字符串的形式保存在序列化后的字节流中。消息和消息的定义相对独立,可读性较好。但序列化后的数据字节很大,序列化和反序列化的时间较长,数据传输效率不高。Protobuf和JSON、XML序列化的方式不同,采用了二进制字节的序列化方式,用字段索引和字段类型通过算法计算得到字段之前的关系映射,从而达到更高的时间效率和空间效率,特别适合对数据大小和传输速率比较敏感的场合使用。

二、Protobuf简介

Protobuf,全称:Protocol Buffer,是Google开发一种数据描述格式,能够将结构化数据序列化,可用于数据存储,通信协议等方面。

Protocol Buffers - Google's data interchange format

Protobuf使用并不复杂,主要流程是:需要自己或者由网络端(平台)写一个.proto文件用来描述序列化的格式,然后用protobuf提供的protoc工具将.proto文件编译成一个Java文件(protobuf官方支持很多语言:Java、C++、C#、Go、Python ,protobuf是一个开源项目,因此有很多大牛也实现了其他语言,但它们的可靠性还有待验证),最后将该Java文件引入到我们的项目中就可以使用了,当然还得引入protobuf的依赖包。



由于Protocol Buffer原生没有对C的支持,可以使用Protobuf-c这个第三方库。

protobuf是以二进制来存储数据的。相对于JSON和XML具有以下优点:

  • 简洁
  • 体积小:消息大小只需要XML的1/10 ~ 1/3
  • 速度快:解析速度比XML快20 ~ 100倍
  • 使用protobuf的编译器,可以生成更容易在编程中使用的数据访问代码
  • 更好的兼容性,protobuf设计的一个原则就是要能够很好的支持向下或向上兼容

GitHub: Protobuf

三、Protobuf应用

接下就将指定格式的信息转换成字节形式数据,然后将字节形式数据恢复成指定格式的信息。

3.1 ProtoBuf的安装

首先,访问Google的Protobuf项目,除了源码外,还提供了不需要编译即可进行使用的版本,地址如下:protobuf编译器下载



对于windows平台,下载protoc-${version}-win32.zip,在此以protoc-3.6.1-win32.zip为例。下载后解压到任意目录,并进入bin文件夹。

3.2 Got *.proto文件

不管是创建还是根据平台端取得proto文件,本质上都没啥区别,以自已创建为例,定义消息的实体结构(详细字段定义可自行百度):

syntax = "proto3";

option java_package = "com.cchip.test.bean";
option java_outer_classname = "PersonFactory";

message Person{

     int32 id = 1;
     string name = 2;
     int32 age = 3;
}

3.3 编译proto文件生成对应的java文件

回到刚才的Bin文件夹,简单点,把.proto文件也拷贝到同一级目录(其实可放任意目录),调出命令工具,执行如下命令
$ protoc --java_out=${OUTPUT_DIR} path/to/your/proto/file

注意目录定位,否则命令执行就会出错。正常情况下执行命令后就会在当前目录下生成一个com的目录,一级一级下去,有一个名为PersonFactory的Java文件。

// Generated by the protocol buffer compiler.  DO NOT EDIT!
// source: test.proto

package com.cchip.test.bean;

public final class PersonFactory {
  private PersonFactory() {}
  public static void registerAllExtensions(
      com.google.protobuf.ExtensionRegistryLite registry) {
  }

  public static void registerAllExtensions(
      com.google.protobuf.ExtensionRegistry registry) {
    registerAllExtensions(
        (com.google.protobuf.ExtensionRegistryLite) registry);
  }
  public interface PersonOrBuilder extends
      // @@protoc_insertion_point(interface_extends:Person)
      com.google.protobuf.MessageOrBuilder {

    /**
     * <code>int32 id = 1;</code>
     */
    int getId();

    /**
     * <code>string name = 2;</code>
     */
    java.lang.String getName();
    /**
     * <code>string name = 2;</code>
     */
    com.google.protobuf.ByteString
        getNameBytes();

    /**
     * <code>int32 age = 3;</code>
     */
    int getAge();
  }
  /**
   * Protobuf type {@code Person}
   */
  public  static final class Person extends
      com.google.protobuf.GeneratedMessageV3 implements
      // @@protoc_insertion_point(message_implements:Person)
      PersonOrBuilder {
  private static final long serialVersionUID = 0L;
    // Use Person.newBuilder() to construct.
    private Person(com.google.protobuf.GeneratedMessageV3.Builder<?> builder) {
      super(builder);
    }
    private Person() {
      id_ = 0;
      name_ = "";
      age_ = 0;
    }

    @java.lang.Override
    public final com.google.protobuf.UnknownFieldSet
    getUnknownFields() {
      return this.unknownFields;
    }
    private Person(
        com.google.protobuf.CodedInputStream input,
        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
        throws com.google.protobuf.InvalidProtocolBufferException {
      this();
      if (extensionRegistry == null) {
        throw new java.lang.NullPointerException();
      }
      int mutable_bitField0_ = 0;
      com.google.protobuf.UnknownFieldSet.Builder unknownFields =
          com.google.protobuf.UnknownFieldSet.newBuilder();
      try {
        boolean done = false;
        ...
        }
      } catch (com.google.protobuf.InvalidProtocolBufferException e) {
        throw e.setUnfinishedMessage(this);
      } catch (java.io.IOException e) {
        throw new com.google.protobuf.InvalidProtocolBufferException(
            e).setUnfinishedMessage(this);
      } finally {
        this.unknownFields = unknownFields.build();
        makeExtensionsImmutable();
      }
    }
    public static final com.google.protobuf.Descriptors.Descriptor
        getDescriptor() {
      return com.cchip.test.bean.PersonFactory.internal_static_Person_descriptor;
    }

    @java.lang.Override
    protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
        internalGetFieldAccessorTable() {
      return com.cchip.test.bean.PersonFactory.internal_static_Person_fieldAccessorTable
          .ensureFieldAccessorsInitialized(
              com.cchip.test.bean.PersonFactory.Person.class, com.cchip.test.bean.PersonFactory.Person.Builder.class);
    }
    ......
    public static com.cchip.test.bean.PersonFactory.Person parseFrom(
        java.nio.ByteBuffer data)
        throws com.google.protobuf.InvalidProtocolBufferException {
      return PARSER.parseFrom(data);
    }

    public static Builder newBuilder(com.cchip.test.bean.PersonFactory.Person prototype) {
      return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype);
    }
    @java.lang.Override
    public Builder toBuilder() {
      return this == DEFAULT_INSTANCE
          ? new Builder() : new Builder().mergeFrom(this);
    }

    /**
     * Protobuf type {@code Person}
     */
    public static final class Builder extends
        com.google.protobuf.GeneratedMessageV3.Builder<Builder> implements
        // @@protoc_insertion_point(builder_implements:Person)
        com.cchip.test.bean.PersonFactory.PersonOrBuilder {
      public static final com.google.protobuf.Descriptors.Descriptor
          getDescriptor() {
        return com.cchip.test.bean.PersonFactory.internal_static_Person_descriptor;
      }

      @java.lang.Override
      protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
          internalGetFieldAccessorTable() {
        return com.cchip.test.bean.PersonFactory.internal_static_Person_fieldAccessorTable
            .ensureFieldAccessorsInitialized(
                com.cchip.test.bean.PersonFactory.Person.class, com.cchip.test.bean.PersonFactory.Person.Builder.class);
      }

      // Construct using com.cchip.test.bean.PersonFactory.Person.newBuilder()
      private Builder() {
        maybeForceBuilderInitialization();
      }

      private Builder(
          com.google.protobuf.GeneratedMessageV3.BuilderParent parent) {
        super(parent);
        maybeForceBuilderInitialization();
      }
      private void maybeForceBuilderInitialization() {
        if (com.google.protobuf.GeneratedMessageV3
                .alwaysUseFieldBuilders) {
        }
      }
      @java.lang.Override
      public Builder clear() {
        super.clear();
        id_ = 0;

        name_ = "";

        age_ = 0;

        return this;
      }

      @java.lang.Override
      public com.google.protobuf.Descriptors.Descriptor
          getDescriptorForType() {
        return com.cchip.test.bean.PersonFactory.internal_static_Person_descriptor;
      }

      @java.lang.Override
      public com.cchip.test.bean.PersonFactory.Person getDefaultInstanceForType() {
        return com.cchip.test.bean.PersonFactory.Person.getDefaultInstance();
      }

      @java.lang.Override
      public com.cchip.test.bean.PersonFactory.Person build() {
        com.cchip.test.bean.PersonFactory.Person result = buildPartial();
        if (!result.isInitialized()) {
          throw newUninitializedMessageException(result);
        }
        return result;
      }

      @java.lang.Override
      public com.cchip.test.bean.PersonFactory.Person buildPartial() {
        com.cchip.test.bean.PersonFactory.Person result = new com.cchip.test.bean.PersonFactory.Person(this);
        result.id_ = id_;
        result.name_ = name_;
        result.age_ = age_;
        onBuilt();
        return result;
      }

      @java.lang.Override
      public Builder clone() {
        return (Builder) super.clone();
      }


    // @@protoc_insertion_point(class_scope:Person)
    private static final com.cchip.test.bean.PersonFactory.Person DEFAULT_INSTANCE;
    static {
      DEFAULT_INSTANCE = new com.cchip.test.bean.PersonFactory.Person();
    }

    public static com.cchip.test.bean.PersonFactory.Person getDefaultInstance() {
      return DEFAULT_INSTANCE;
    }

    private static final com.google.protobuf.Parser<Person>
        PARSER = new com.google.protobuf.AbstractParser<Person>() {
      @java.lang.Override
      public Person parsePartialFrom(
          com.google.protobuf.CodedInputStream input,
          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
          throws com.google.protobuf.InvalidProtocolBufferException {
        return new Person(input, extensionRegistry);
      }
    };

    public static com.google.protobuf.Parser<Person> parser() {
      return PARSER;
    }

    @java.lang.Override
    public com.google.protobuf.Parser<Person> getParserForType() {
      return PARSER;
    }

    @java.lang.Override
    public com.cchip.test.bean.PersonFactory.Person getDefaultInstanceForType() {
      return DEFAULT_INSTANCE;
    }

  }

  private static final com.google.protobuf.Descriptors.Descriptor
    internal_static_Person_descriptor;
  private static final 
    com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
      internal_static_Person_fieldAccessorTable;

  public static com.google.protobuf.Descriptors.FileDescriptor
      getDescriptor() {
    return descriptor;
  }
  private static  com.google.protobuf.Descriptors.FileDescriptor
      descriptor;
  static {
    java.lang.String[] descriptorData = {
      "\n\ntest.proto\"/\n\006Person\022\n\n\002id\030\001 \001(\005\022\014\n\004na" +
      "me\030\002 \001(\t\022\013\n\003age\030\003 \001(\005B$\n\023com.cchip.test." +
      "beanB\rPersonFactoryb\006proto3"
    };
    com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
        new com.google.protobuf.Descriptors.FileDescriptor.    InternalDescriptorAssigner() {
          public com.google.protobuf.ExtensionRegistry assignDescriptors(
              com.google.protobuf.Descriptors.FileDescriptor root) {
            descriptor = root;
            return null;
          }
        };
    com.google.protobuf.Descriptors.FileDescriptor
      .internalBuildGeneratedFileFrom(descriptorData,
        new com.google.protobuf.Descriptors.FileDescriptor[] {
        }, assigner);
    internal_static_Person_descriptor =
      getDescriptor().getMessageTypes().get(0);
    internal_static_Person_fieldAccessorTable = new
      com.google.protobuf.GeneratedMessageV3.FieldAccessorTable(
        internal_static_Person_descriptor,
        new java.lang.String[] { "Id", "Name", "Age", });
  }

  // @@protoc_insertion_point(outer_class_scope)
}

有个插曲,原本以为跟JSON字段转JSON基类那样,生成一个简单的Java类,哪知打开一看,代码很多行,好多方法,下不了手,只好再问百度。

3.4 添加对应版本的protobuf的依赖包

必须一一对应,之前用什么版本的工具,这里也需要导入同一版本的工具,如上面用V3.6.1,这边也需导入3.6.1:

<dependency>
  <groupId>com.google.protobuf</groupId>
  <artifactId>protobuf-java</artifactId>
  <version>3.6.1</version>
</dependency>

3.5 Android中实现对消息结构的序列化/反序列化

模拟将对象转成byte[],方便传输

PersonFactory.Person.Builder builder = PersonFactory.Person.newBuilder();
builder.setId(1);
builder.setName("ant");
Person person = builder.build();

// 转换为Byte
byte[] men = person.toByteArray();

模拟接收Byte[],反序列化成Person类

byte[] byteArray =person.toByteArray();
Person p2 = Person.parseFrom(byteArray);
System.out.println("after :" +p2.toString());

3.6 优缺点

  • 优点:通过以上的时间效率和空间效率,可以看出protobuf的空间效率是JSON的2-5倍,时间效率要高,对于数据大小敏感,传输效率高的模块可以采用protobuf库
  • 缺点:消息结构可读性不高,序列化后的字节序列为二进制序列不能简单的分析有效性

四、总结

这里仅介绍了基本的ProtoBuf的安装和使用过程,跟普通工具差不多,需要了解更多的可以参考google官方文档。


参考资源

  • https://developers.google.com/protocol-buffers/