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依赖依赖。

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

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

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("zhangsanfeng@wudang.org");

    // 构建嵌套消息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("zhangsanfeng@wudang.org");

    // 构建引用消息(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

(完)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容