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
(完)