Protobuf的介绍和使用

一、介绍

Protocol Buffers(简称Protobuf) ,是Google出品的序列化框架,与开发语言无关,和平台无关,具有良好的可扩展性。Protobuf和所有的序列化框架一样,都可以用于数据存储、通讯协议。

Protobuf支持生成代码的语言包括Java、Python、C++、Go、JavaNano、Ruby、C#,官网地址是https://developers.google.com/protocol-buffers/。

Portobuf的序列化的结果体积要比XML、JSON小很多,XML和JSON的描述信息太多了,导致消息要大;此外Portobuf还使用了Varint 编码,减少数据对空间的占用。

Portobuf序列化和反序列化速度比XML、JSON快很多,是直接把对象和字节数组做转换,而XML和JSON还需要构建成XML或者JSON对象结构。

二、快速开始

2.1 安装Protobuf

本文使用的是Windows版本的Protobuf编译器,版本号是3.1.0。下载地址:https://github.com/google/protobuf/releases,注意选择protoc-3.1.0-win32.zip。

下载完毕后,将其解压到磁盘英文路径下,例如d:/protobuf,里面有bin和include两个目录。

配置环境变量,点击【计算机】-【属性】-【高级系统设置】-【环境变量】,找到并编辑PATH,在尾巴上增加d:/protobuf/bin;

验证,打开cmd窗口,输入protoc --version,会看到版本信息:libprotoc 3.1.0。否则就是环境变量没有配置正确。

  • Protobuf编译器对应序列化理论中的IDL Compiler。*

2.2 编写proto文件

我们需要编写proto文件,定义程序中需要处理的结构化数据,在protobuf中,结构化数据被称为 Message。proto 文件非常类似java的pojo bean。我们创建了一个名为Person.proto的文件(txt文本),并在文件里描述了Person数据结构,一共有两个属性,分别是age和name。这里我们使用的protobuf语法是proto3,需要在第一行声明;否则,默认使用proto2

proto文件对应序列化理论中的IDL(Interface description language,接口描述语言)。

syntax="proto3"; 
option java_package = "org.serialization.protobuf.quickstart";   
option java_outer_classname = "PersonProtobuf";   
message Person  {   
  int32 age = 1;
  string name = 2;
}  

2.3 编译

使用portoc编译器,把proto文件编译成java程序,第一个参数java_out指定了生成程序的目录,第二个参数是Person.proto的位置。也可以使用portoc编译器把proto文件编译成其它语言的程序。把生成的java文件拷贝到自己的工程里就可以使用了。

protoc --java_out=./ Person.proto

2.4 序列化调用

本节描述如何调用portoc编译器生成的java代码实现序列化和反序列化,需要我们在工程中添加protobuf-java依赖依赖。


    com.google.protobuf
    protobuf-java
    3.1.0

调用的示例代码如下,详情参见注释。

package org.serialization.protobuf.quickstart;

import com.google.protobuf.InvalidProtocolBufferException;

/**
 * 演示了使用Protobuf序列化和反序列化Person对象。
 */
public class Main {

    public static void main(String[] args) {

        //  序列化
        // 创建Person的Builder
        PersonProtobuf.Person.Builder personBuilder = 
                            PersonProtobuf.Person.newBuilder();
        // 设置Person的属性
        personBuilder.setAge(18);
        personBuilder.setName("张三丰");
        // 创建Person
        PersonProtobuf.Person zhangsanfeng = personBuilder.build();
        // 序列化,byte[]可以被写到磁盘文件,或者通过网络发送出去。
        byte[] data = zhangsanfeng.toByteArray();
        System.out.println("serialization end.");


        // 反序列化,byte[]可以读文件或者读取网络数据构建。
        System.out.println("deserialization begin.");
        try {
            PersonProtobuf.Person person = PersonProtobuf.Person.parseFrom(data);
            System.out.println(person.getAge());
            System.out.println(person.getName());
        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
        }

    }
}

三、关于Message

前文快速开始的例子非常简单,现实中我们往往需要定义结构更加复杂的Message,接下来我们看一下如何定义嵌套消息和import(引用)消息。

3.1 嵌套消息

下面是一个嵌套消息的示例,并展示了枚举、List、Map等类型,编译之后代码都在一个类里,即Staffbuf。

syntax = "proto3";
option java_package = "org.serialization.protobuf.advanced";
option java_outer_classname = "Staffbuf";
message Staff {
    int32 id = 1;
    string name = 2;
    string email = 3;

    // 枚举示例
    enum PhoneType {
        MOBILE = 0;
        TELEPHONE = 1;
    }
    
    // 嵌套示例
    message PhoneNumber {
        string number = 1;
        PhoneType type = 2;

    }
    
    // list示例
    repeated PhoneNumber phone = 4;
        
    message Map {
        string key = 1;
        int32 value = 2;
    }

    // map示例
    Map map = 5;
}

调用示例

构造Staff的时候,需要先构建嵌套消息PhoneNumber。

public static void main(String[] args) {

    //  序列化
    // 创建Staff的Builder
    Staffbuf.Staff.Builder staffBuilder = Staffbuf.Staff.newBuilder();
    staffBuilder.setId(1);
    staffBuilder.setName("张三丰");
    //staffBuilder.setEmail("[email protected]");

    // 构建嵌套消息PhoneNumber
    List list = new ArrayList();
    Staffbuf.Staff.PhoneNumber.Builder phoneBuilder = 
                        Staffbuf.Staff.PhoneNumber.newBuilder();
    phoneBuilder.setType(Staffbuf.Staff.PhoneType.TELEPHONE);
    phoneBuilder.setNumber("010-12345678");
    Staffbuf.Staff.PhoneNumber phoneNumber = phoneBuilder.build();
    list.add(phoneNumber);
    phoneBuilder.clear();
    //phoneBuilder.setType(Staffbuf.Staff.PhoneType.MOBILE);
    phoneBuilder.setNumber("13912345678");
    list.add(phoneBuilder.build());

    // 构建Map
    Staffbuf.Staff.Map.Builder mapBuilder = Staffbuf.Staff.Map.newBuilder();
    mapBuilder.setKey("a");
    mapBuilder.setValue(100);

    staffBuilder.addAllPhone(list);
    staffBuilder.setMap(mapBuilder);
    Staffbuf.Staff zhangsanfeng = staffBuilder.build();
    // 序列化,byte[]可以被写到磁盘文件,或者通过网络发送出去。
    byte[] data = zhangsanfeng.toByteArray();
    System.out.println("serialization end.");


    // 反序列化,byte[]可以读文件或者读取网络数据构建。
    System.out.println("deserialization begin.");
    try {
        Staffbuf.Staff staff = Staffbuf.Staff.parseFrom(data);
        System.out.println(staff.getName());
        staff.getPhoneList().forEach( x -> System.out.println(x.toString()));
    } catch (InvalidProtocolBufferException e) {
        e.printStackTrace();
    }

}

3.2 引入消息 Import Message

Porto引入消息,和java import类是很相似的。一些公共的消息内容,可能会单独定义在一个porto文件中,之后被其它的porto文件引用。我们首先定义一个PhoneNumberBuf.proto文件,然后定义一个Staff.porto文件来引用它。两个porto文件都需要编译,每个文件会生成java代码。

PhoneNumberBuf.proto

syntax="proto3"; 
option java_package = "org.serialization.protobuf.imported";   
option java_outer_classname = "PhoneNumberBuf";   
message PhoneNumber  {   
   string number = 1; 
  
  // 枚举示例
  enum PhoneType { 
    MOBILE = 0; 
    TELEPHONE = 1; 
  }
   PhoneType type = 2;
 
}

Staff.porto

syntax="proto3"; 
import "PhoneNumberbuf.proto"; // 引入消息
option java_package = "org.serialization.protobuf.imported";   
option java_outer_classname = "Staffbuf";   
message Staff  {   
  int32 id = 1;
  string name = 2;
  string email = 3;
    
  // 引入的消息类型
  repeated PhoneNumber phone = 4;

}

调用示例

public static void main(String[] args) {

    //  序列化
    // 创建Staff的Builder
    Staffbuf.Staff.Builder staffBuilder = Staffbuf.Staff.newBuilder();
    staffBuilder.setId(1);
    staffBuilder.setName("张三丰");
    //staffBuilder.setEmail("[email protected]");

    // 构建引用消息(import message)PhoneNumber
    List list = new ArrayList();
    PhoneNumberBuf.PhoneNumber.Builder phoneBuilder =
                      PhoneNumberBuf.PhoneNumber.newBuilder();
    phoneBuilder.setType(PhoneNumberBuf.PhoneNumber.PhoneType.TELEPHONE);
    phoneBuilder.setNumber("010-12345678");
    PhoneNumberBuf.PhoneNumber phoneNumber = phoneBuilder.build();
    list.add(phoneNumber);
    phoneBuilder.clear();
    phoneBuilder.setType(PhoneNumberBuf.PhoneNumber.PhoneType.MOBILE);
    phoneBuilder.setNumber("13912345678");
    list.add(phoneBuilder.build());
    staffBuilder.addAllPhone(list);
    // 完成staff的构建
    Staffbuf.Staff zhangsanfeng = staffBuilder.build();
    // 序列化,byte[]可以被写到磁盘文件,或者通过网络发送出去。
    byte[] data = zhangsanfeng.toByteArray();
    System.out.println("serialization end.");



    // 反序列化,byte[]可以读文件或者读取网络数据构建。
    System.out.println("deserialization begin.");
    try {
        Staffbuf.Staff staff = Staffbuf.Staff.parseFrom(data);
        System.out.println(staff.getId());
        System.out.println(staff.getName());
        staff.getPhoneList().forEach(x -> System.out.println(x.toString()));
    } catch (InvalidProtocolBufferException e) {
        e.printStackTrace();
    }

}

3.3 DynamicMessage

ProtoBuf中还支持动态消息,我们可以使用DynamicMessage和Descriptor把数据从byte[]或者InputStream中反序列化出来。

// 服务端可以维护一个Map,从Map中获取Staff这个Message,然后再获取Descriptor。
Descriptors.Descriptor descriptor = Staffbuf.Staff.getDescriptor();
DynamicMessage msg = DynamicMessage.parseFrom(descriptor,data);
// 之后再通过反射的方式把DynamicMessage转换成Staff

(完)

你可能感兴趣的:(Protobuf的介绍和使用)