通信协议之Protocol buffer(Java篇)

之前一直习惯用json进行数据的传输,觉得很方便。来到新公司后发现同事们用的更多的的协议都不是json,而是Protocol buffer。这个东西之前没有听说过,不明白同事们为什么放弃好好的json不用,用这个。后来了解到经常是设备与设备之间进行通信,而不是设备与服务器做通信。很多设备是linux下c语言做核心服务,c来解析json比较麻烦。于是决定花些时间来学习这个陌生的协议。

简介

Protocol Buffers(也称protobuf)是Google公司出口的一种独立于开发语言,独立于平台的可扩展的结构化数据序列机制。通俗点来讲它跟xml和json是一类。是一种数据交互格式协议。

网上有很多它的介绍,主要优点是它是基于二进制的,所以比起结构化的xml协议来说,它的体积很少,数据在传输过程中会更快。另外它也支持c++、java、python、php、javascript等主流开发语言。

更多信息,你可以在这个它的官方网站查阅相关的资料

Protocol编译安装

protocol的编译器是C语言编写的,如果你正在使用c++开发,请根据c++安装引导进行安装。

对于不是c++语言的开发者而言,最简单的方式就是从下面的网站下载一个预编译的二进制文件。
https://github.com/google/protobuf/releases

在上面的链接中,你可以按照对应的平台下载zip包:protoc- VERSION PLATFORM.zip. 如protobuf-java-3.1.0.zip

如果你要寻找之前的版本,通过下面的maven库:
http://repo1.maven.org/maven2/com/google/protobuf/protoc/

因为我主要用java开发应用,所以这篇文章重点讲解protocol的java实现。 还有这是在windows上操作。

Protocol buffer的安装

1 通过Maven方式安装

如果你机器上还没有安装maven.请到下面的网站安装
http://maven.apache.org/
选择相应的包。我这里选择的是apache-maven-3.3.9-bin.zip.

添加maven到path

将下载后的bin目录添加到path中。

然后用mvn -version测试一下

C:\Users\xx-xxx>mvn -version
Apache Maven 3.3.9 (bb52d8502b132ec0a5a3f4c09453c07478323dc5; 2015-11-11T00:41:4
7+08:00)
Maven home: E:\xxxxxx\apache-maven-3.3.9-bin\apache-maven-3.3.9\bin\..

Java version: 1.7.0_09, vendor: Oracle Corporation
Java home: C:\Program Files\Java\jdk1.7.0_09\jre
Default locale: zh_CN, platform encoding: GBK
OS name: "windows 7", version: "6.1", arch: "amd64", family: "windows"

说明安装成功

安装protoc

从https://github.com/google/protobuf/releases下载protoc,如protoc-3.1.0-win32.zip,然后解压然后添加到path.
在命令行中测试

protoc --version

正常情况应该打印版本信息,说明添加成功。

将protoc复制到protocol buffer解压的目录。

这一步很重要。
例如之前下载的protobuf-java-3.1.0.zip我将它解压在E盘
E:\xxxx\protobuf-java-3.1.0,那么E:\xxxx\protobuf-java-3.1.0\protobuf-3.1.0这个目录就当它是根目录。我用$ROOT表示。

将protoc.exe文件复制到$ROOT/src/目录下。

然后在命令行中定位到$ROOT/java/
然后运行命令mvn package,
如果一切成功的话,就会在$ROOT/java/core/target/目录下生成protobuf-java-3.1.0.jar文件。

编译代码

在下载的protobuf-java-3.1.0.zip解压后,我们定义它的根目录为$ROOT。它下面有一个examples文件夹,里面有个adressbook.proto文件,.proto就是protocol buffer的文件格式后缀。
我们将之前生成的protobuf-java-3.1.0.jar复制到该目录下。然后执行下面的命令

protoc --java_out=. addressbook.proto

它就会自动生成com/example/tutorial/AddressBookProtos.java文件。

自此,protoc buffer就完全编译成功,并且能正常运行了。

接下来我讲解它的基本语法

语法

在Android开发中,json运用的很广泛,gson类可以直接将一个java类对象直接序列成json格式的字符串。
在protocol buffer中同样有类似功能的结构。
比如我要定义一个学生类,它包含学号、姓名、性别必要信息,也有兴趣爱好、特长等必要信息。
如果用java的话。大概如下面

class Student{
    int number;
    String name;
    int sex;
    String hobby;
    String skill;
}

那么如果用Protocol buffer来定义呢?

这里有一个关键字message。message是消息的意思,等同于java中的class关键字和c中的struct,代表一段数据的封装。

简单示例

首先我们得创建.proto文件。这里为Student.proto

syntax = "proto3";

message Student
{
    int32 number = 1;
    string name = 2;
    int32 sex = 3;
    string hobby = 4;
    string skill = 5;
}

syntax=”proto3”表示运用proto3的语法。而网上的教程大多还是proto2。
proto3提示required限制符不能起作用,默认的就是optional,表示任何域都是可选的。

那好,我们参加教程前面部分将.proto文件转换成.java文件。

protoc --java_out=.  Student.proto

结果在当前目录生成了StudentOuterClass.java文件。至于为什么是StudentOuterClass.java这是因为我们在Student.proto没有指定它编译后生成的文件名称,protoc程序默认生成了StudentOuterClass.java,其实这个名字我们可以自定义,文章后面的内容我会介绍。

序列化

在IDE中新建项目,然后添加StudentOuterClass.java文件,并且添加protobuf-java-3.1.0.jar。

然后编写如下代码:

Student.Builder buidler = Student.newBuilder();
        buidler.setName("Frank");
        buidler.setNumber(123456);
        buidler.setHobby("music");
        Student student = buidler.build();
        System.out.println(student.toString());

最终代码会在运行时打印如下信息:

I/System.out: number: 123456
I/System.out: name: "Frank"
I/System.out: hobby: "music"

说明从.proto中的message到java中的class转换已经成功。

最后

student.toByteArray();

这个方法会得到byte[],我们可以将它送到文件流中进行传输,这也是它的终极目的。也就是我们将protoc对象序列化成了字节流数据。
大家注意这个toByteArray()产生的byte[]数组,它代表要全部传输的二进制数据,大家可以打印它的大小,有心人可以用json实现两样的信息,然后打印json序列化后的数据长度做比较。看看,protocol buffre是不是更节省内存空间,但在这里,我不做过多探究。

那么如何将字节流数据反序列化成protoc对象呢?

反序列化

我们可以这样

byte[] array = student.toByteArray();

try {
    Student student1 = Student.parseFrom(array);
    System.out.println(student1.toString());
} catch (InvalidProtocolBufferException e) {
    e.printStackTrace();
}   

array是之前序列化后产生的byte数据,现在通过Student的静态方法parseFrom()可以数据反序列成Student对象。

现在想来这个是不是很简单???

除了开始的阶段编写.proto文件,然后再把.proto文件编译成java文件麻烦点,其余的步骤甚至比json转换的更便利。

message

上一节已经见识过了message,它等同于java中的class关键字和c中的struct关键字。

message Student
{
}

限定符

在proto2.0版本中有三个限定符

- required  必要的域
- optional  可选的
- repeated  可重复的(0~N)

其中被required修饰的变量是必不可少的。
optional可有可无。
repeated修饰的就要是数组字段。

而在proto3.0中required被删掉了。optional字符也不能使用,因为默认的域都是optional属性。

3.0新语法

syntax = "proto3";
或者
syntax="proto2";

这个写在.proto文件中,用来指定使用proto3语法还是使用proto2语法。目前protobuffer继续支持proto2

数据类型

我们在前面的内容中见到了int32这样的字眼,它其实是数据类型。

protoc类型 java类型 c++类型
double double double
float float float
int32 int int32
int64 long int64
uint32 int uint32
uint64 long uint64
sint32 int int32
sint64 long int64
fixed32 int uint32
fixed64 long uint64
sfixed32 int uint32
sfixed64 long uint64
bool boolean bool
string String string
bytes ByteString string

最常用的就是float、int32、bool string bytes。

枚举

protocol buffer除了支持上面的基本类型外还支持枚举。
关键字是enum

比如我们可以将前文提到的Student对象中的性别用一个枚举代替。
那么将Student.proto文件修改如下:

syntax = "proto3";

enum Sex
{
    MALE = 0;
    FEMALE = 1;
}

message Student
{
    int32 number = 1;
    string name = 2;
    Sex sex = 3;
    string hobby = 4;
    string skill = 5;
}

然后再用protoc.exe编译产生新的java文件,并添加到工程当中。再进行代码测试。

Student.Builder buidler = Student.newBuilder();
        buidler.setName("Frank");
        buidler.setNumber(123456);
        buidler.setHobby("music");
        //已经可以设置Sex属性了
        buidler.setSex(Sex.MALE);
        Student student = buidler.build();
        System.out.println(student.toString());

        byte[] array = student.toByteArray();

        try {
            Student student1 = Student.parseFrom(array);
            System.out.println(student1.toString());
            //在这里打印性别的值
            System.out.println(student1.getSex().toString());
        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
        }

在上面代码中有这两句

//设置性别
 buidler.setSex(Sex.MALE);

//打印性别
 System.out.println(student1.getSex().toString());

然后打印的结果如下:

com.frank.protocdemo I/System.out: number: 123456
com.frank.protocdemo I/System.out: name: "Frank"
com.frank.protocdemo I/System.out: hobby: "music"
com.frank.protocdemo I/System.out: number: 123456
com.frank.protocdemo I/System.out: name: "Frank"
com.frank.protocdemo I/System.out: hobby: "music"
com.frank.protocdemo I/System.out: MALE

最后一行,把MALE打印出来了。

数组

通过关键字repeated实现

看下面代码:

syntax = "proto3";

enum Sex
{
    MALE = 0;
    FEMALE = 1;
}

message Student
{
    int32 number = 1;
    string name = 2;
    Sex sex = 3;
    string hobby = 4;
    string skill = 5;
    repeated int32 array = 6 [packed=true];
}

我们通过repeated int32 array =6;定义了一个int[]类型的数组。而后面[packed=true]是因为要优化之前的版本对一些int32数据进行字节对齐。

然后我们在java工程中测试

Student.Builder buidler = Student.newBuilder();
        buidler.setName("Frank");
        buidler.setNumber(123456);
        buidler.setHobby("music");
        buidler.setSex(Sex.MALE);
        buidler.addArray(1);
        buidler.addArray(2);
        buidler.addArray(3);

        Student student = buidler.build();
        System.out.println(student.toString());

        byte[] array = student.toByteArray();

        try {
            Student student1 = Student.parseFrom(array);
            System.out.println(student1.toString());
            System.out.println(student1.getSex().toString());
        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
        }

通过builder.addArray()添加数组元素。
打印结果如下:

com.frank.protocdemo I/System: FinalizerDaemon: finalize objects = 1
com.frank.protocdemo I/System.out: number: 123456
com.frank.protocdemo I/System.out: name: "Frank"
com.frank.protocdemo I/System.out: hobby: "music"
com.frank.protocdemo I/System.out: array: 1
com.frank.protocdemo I/System.out: array: 2
com.frank.protocdemo I/System.out: array: 3
com.frank.protocdemo I/System.out: number: 123456
com.frank.protocdemo I/System.out: name: "Frank"
com.frank.protocdemo I/System.out: hobby: "music"
com.frank.protocdemo I/System.out: array: 1
com.frank.protocdemo I/System.out: array: 2
com.frank.protocdemo I/System.out: array: 3
com.frank.protocdemo I/System.out: MALE

引用另一个message

在java中经常有一个对象包含另一个对象的情况。而在protocol buffer中这个也能实现。
一个message中能够嵌套另外一个message。

比如在Student.proto中添加一个Info对象。

message Info
{
    int32 qq = 1;
    int32 weixin = 2;
}

message Student
{
    int32 number = 1;
    string name = 2;
    Sex sex = 3;
    string hobby = 4;
    string skill = 5;
    repeated int32 array = 6;
    Info info = 7;
}

然后测试代码为:

Student.Builder buidler = Student.newBuilder();
        buidler.setName("Frank");
        Info.Builder infoBuilder = Info.newBuilder();
        infoBuilder.setQq(1111111);
        infoBuilder.setWeixin(222222);
        buidler.setInfo(infoBuilder);
        Student student = buidler.build();
        System.out.println(student.toString());

        byte[] array = student.toByteArray();

        try {
            Student student1 = Student.parseFrom(array);
            System.out.println(student1.toString());
        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
        }

打印结果如下:

com.frank.protocdemo I/System.out: name: "Frank"
com.frank.protocdemo I/System.out: info {
com.frank.protocdemo I/System.out:   qq: 1111111
com.frank.protocdemo I/System.out:   weixin: 222222
com.frank.protocdemo I/System.out: }
com.frank.protocdemo I/System.out: name: "Frank"
com.frank.protocdemo I/System.out: info {
com.frank.protocdemo I/System.out:   qq: 1111111
com.frank.protocdemo I/System.out:   weixin: 222222
com.frank.protocdemo I/System.out: }

嵌套

java中有内部类的概念,如

class Student{

    class Score{
    }
}

而在protocol buffer中同样支持

message Student
{
    message Score
    {
        int32 chinese = 1;
        int32 history = 2;
    }

    int32 number = 1;
    string name = 2;
    Sex sex = 3;
    string hobby = 4;
    string skill = 5;
    repeated int32 array = 6;
    Info info = 7;
    Score score = 8;
}

上面在Message Student定义了message Score,里面域代表两门成绩。中文和历史。

那好,测试代码如下:

Student.Builder buidler = Student.newBuilder();
        buidler.setName("Frank");
        Student.Score.Builder scoreBuilder = Student.Score.newBuilder();
        scoreBuilder.setChinese(99);
        scoreBuilder.setHistory(88);
        buidler.setScore(scoreBuilder);
        Student student = buidler.build();
        System.out.println(student.toString());

        byte[] array = student.toByteArray();

        try {
            Student student1 = Student.parseFrom(array);
            System.out.println(student1.toString());
        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
        }

打印结果:

com.frank.protocdemo I/System.out: name: "Frank"
com.frank.protocdemo I/System.out: score {
com.frank.protocdemo I/System.out:   chinese: 99
com.frank.protocdemo I/System.out:   history: 88
com.frank.protocdemo I/System.out: }
com.frank.protocdemo I/System.out: name: "Frank"
com.frank.protocdemo I/System.out: score {
com.frank.protocdemo I/System.out:   chinese: 99
com.frank.protocdemo I/System.out:   history: 88
com.frank.protocdemo I/System.out: }

可以看到结果也是准确无误的。

import关键字

我们之前的示例中

message Info
{
    int32 qq = 1;
    int32 weixin = 2;
}

message Student
{
    int32 number = 1;
    string name = 2;
    Sex sex = 3;
    string hobby = 4;
    string skill = 5;
    repeated int32 array = 6;
    Info info = 7;
}

message Info和message Student定义在同一个.proto文件当中。而在protocol buffer中可以通过import关键字引入其它.proto文件当中的message。

[Info.proto]

syntax="proto3";

option java_package="com.frank.protocdemo";

message Info
{
    int32 qq = 1;
    int32 weixin = 2;
}

[Student.proto]

syntax = "proto3";

import "Info.proto";

option java_package="com.frank.protocdemo";
option java_outer_classname="StudentDemo";

enum Sex
{
    MALE = 0;
    FEMALE = 1;
}


message Student
{
    message Score
    {
        int32 chinese = 1;
        int32 history = 2;

    }

    int32 number = 1;
    string name = 2;
    Sex sex = 3;
    string hobby = 4;
    string skill = 5;
    repeated int32 array = 6 [packed=true];
    Score score = 7;
    Info info = 8;
}

我们通过import "Info.proto";就算导入了message Info.

然后我们对两个proto文件进行编译

protoc -I=.  --java_out=.  Info.proto  Student.proto

将会产生InfoOuterClass.java与StudentDemo.java。我们可以将这两个文件添加到工程中,就能如此前那样正常使用。自此,import关键字导入其它proto文件中的message就完成了。

option之java_package

文章一开始我们编写Student.proto时没有指定这个选项,所以它编译后生成的java文件就在当前目录下。现在我们这样编写代码

syntax = "proto3";

option java_package="com.frank.protocdemo";

enum Sex
{
    MALE = 0;
    FEMALE = 1;
}


message Student
{
    message Score
    {
        int32 chinese = 1;
        int32 history = 2;

    }

    int32 number = 1;
    string name = 2;
    Sex sex = 3;
    string hobby = 4;
    string skill = 5;
    repeated int32 array = 6 [packed=true];
    Score score = 7;
}

option java_package=”com.frank.protocdemo”;
添加了这段代码,它的作用是最终会将编译产生的java文件放在com/frank/protocdemo这个目录下。

所以java_package是用来定义编译后产生的文件所在包结构的

package

package与java_package有些不同,java_package是定义编写生成后的java文件所在的目录,而package是对应的java类的包名。

option之java_outer_classname

我们之前的代码中都没有指定Student.proto编译生成的java文件名称,所以它默认的就是StudentOuterClass.java。
现在我们试试这样

syntax = "proto3";

option java_package="com.frank.protocdemo";
option java_outer_classname="StudentDemo";

enum Sex
{
    MALE = 0;
    FEMALE = 1;
}


message Student
{
    message Score
    {
        int32 chinese = 1;
        int32 history = 2;

    }

    int32 number = 1;
    string name = 2;
    Sex sex = 3;
    string hobby = 4;
    string skill = 5;
    repeated int32 array = 6 [packed=true];
    Score score = 7;
}

我们添加了这行option java_outer_classname="StudentDemo";,而最终它产生的java文件也不再是StudentOuterClass.java而是StudentDemo.java。

所以java_outer_classname是用来定义编译后的java文件名字的。

编译命令

还记得本文开始的地方吗?用

protoc --java_out =.  addressbook.proto

将proto文件编译成java文件。
其实它的完整命令如下:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto
  • proto_path=IMPORT_PATH 默认当前目录
  • cpp_out 生成的c++文件目录
  • java_out 生成的java文件目录
  • pytho_out 生成的python文件目录

–proto_path等同于-I选项,它的意思是等待编译的.proto文件所在的目录,可以指定多个,如果不指定的话默认是当前目录。

path/to/file.proto 等待编译的proto数据文件。

所以

protoc --java_out =.  addressbook.proto

就是将addressbook.proto文件编译产生java文件。

总结

protocol buffer的使用还是相对简单点,唯一麻烦的就是多了一个预编译的过程,将.proto文件转换成.java文件。但有的时候,这些过程是必须的。

你可能感兴趣的:(Android笔记,Android,中级进阶相关的知识点)