文章目录
一文搞懂Java应用ProtoBuf协议
一、序言
我们日常写代码,服务之间的接口交互基本使用的都是JSON格式的数据,其可读性及易用性都较好,对于其它格式的协议研究过少,最近经手的项目涉及到与其它公司进行数据汇总,数据的访问频率非常高且数据量较大,除引入Kafka等消息中间件外,另计划将通信协议格式调整为谷歌的ProtoBuf协议。
小豪近期也简要研究了一下ProtoBuf,本文将从ProtoBuf的概念开始介绍,逐步带大家搞清楚如何使用Java操作ProtoBuf协议,旨在帮助大家快速上手Protobuf的基本使用。本文大纲如下:
二、基础概念
1、何为ProtoBuf
百度百科对它的介绍的是:Protocol Buffers(简称ProtoBuf)是Google公司开发的一种数据描述语言,类似于XML能够将结构化数据序列化,可用于数据存储、通信协议等方面。它不依赖于语言和平台并且可扩展性极强。现阶段官方支持C++、JAVA、Python、Objective C、C#、Ruby、PHP、JavaScript八种编程语言,还可以找到大量的几乎涵盖所有语言的第三方拓展包。
Protocol Buffers在谷歌被广泛用于各种结构化信息存储和交换。Protocol Buffers作为一个自定义的远程过程调用(RPC)系统,用于在谷歌几乎所有的设备间的通信。
简要理解,ProtoBuf即谷歌自己制定的一种数据格式,类似于Json、XML等
1.1 优劣性
谷歌制定的ProtoBuf最大的优势就是高效性,不同于Json和XML使用文本进行数据编码,ProtoBuf采用二进制进行编码,其传输速度、解析速度都比较快,序列化后的体积也更小。
但相较于Json和XML,其可读性太差了,只能通过反序列化得到可读的数据内容,另外通用性也较差,只在谷歌内部用的比较多。
2、ProtoBuf文件语法
这里我们首先编写一个简要的ProtoBuf文件,内容如下,文件名为userInfo.proto
,文件内容如下:
syntax = "proto3";
// 表示生成的序列化器的Java包
option java_package = "com.example.hgspb.pbmodel";
// 表示生成的Java序列化器的类名
option java_outer_classname = "UserInfoFactory";
message UserInfo {
required int64 id = 1;
string name = 2;
EnumSex sex = 3;
}
enum EnumSex {
SEX_MAN = 0; // 男
SEX_WOMAN = 1; // 女
}
2.1 基本结构
ProtoBuf语法结构如下:
- 语法版本:首行
syntax = "proto3"
代表当前使用的是proto3
规范的语法 - 包名:第二、三行代表输出Java文件之后的对应的包以及类名(注:生成的Java类名不要与
message
声明的消息名称一致) - 消息定义:剩下的其它行代表
message
消息结构与enum
枚举类型。
其中message
关键字,作用为定义一种消息类型,类似于Java中定义Class
实体类一样,message
里面定义所包含的属性,属性由字段规则、字段类型、字段名称和字段编号组成。
// 属性格式
[字段规则] 字段类型 字段名称 = 字段标识符;
2.2 字段规则
字段规则类似于MySQL中的约束,即对数据的限制条件。
ProtoBuf常见的约束有以下几种:
- singular:字段可以出现
0
次或出现1
次(不得超过1
次),proto3
语法默认的字段规则 - repeated:字段可以出现任意多次(包括
0
次),类似于Java中的集合
2.3 字段类型
字段类型包括常用的标量类型与复合类型。
2.3.1 标量类型
标量类型类似于Java中的基本类型,对应关系如下表所示:
ProtoBuf类型 | Java类型 | 默认值 |
---|---|---|
int32 | int | 0 |
int64 | long | 0L |
float | float | 0.0F |
double | double | 0.0D |
bool | boolean | false |
string | String | “” |
2.3.2 复合类型
复合类型可以是其它message
消息,或enum
枚举等,类似于Java中的引用类型。
这里分别列举一下Java中List
、Map
等数据结构在ProtoBuf中的用法:
① 引用其它message
消息、enum
枚举:
message UserInfo {
// 字段类型为UserDetail消息名
UserDetail detail = 1;
// 字段类型为EnumSex枚举
EnumSex sex = 2;
}
message UserDetail {
int64 id = 1;
string name = 2;
}
enum EnumSex {
SEX_MAN = 0; // 男(枚举编号默认从0开始)
SEX_WOMAN = 1; // 女
}
②引用List
集合:
message UserList {
// 声明字段规则为repeated,字段类型为UserInfo,即为List集合
// 等价于 => List<UserDetail>
repeated UserDetail detailList = 1;
}
message UserDetail {
int64 id = 1;
string name = 2;
}
③引用Map
集合:
message UserCache {
// 字段类型为map,即为Map集合
// 等价于 => Map<Long, UserDetail>
map<int64, UserDetail> userCacheMap = 1;
}
message UserDetail {
int64 id = 1;
string name = 2;
}
2.4 字段编号
字段编号即字段的唯一标识,从1
开始编号,后续ProtoBuf传递时,实际上传递是就是该编号而非字段名
三、Java操作ProtoBuf
1、生成proto对应Java文件
1.1 下载编译器
首先我们需要下载ProtoBuf编译器,小豪这里下载的是21.10版本win64位的(github下载地址在这)
下载后直接解压即可。
1.2 编写proto文件
之后编写好对应的proto文件,这里我们直接使用下面的proto文件:
syntax = "proto3";
// 表示生成的序列化器的Java包
option java_package = "com.example.hgspb.pbmodel";
// 表示生成的Java序列化器的类名
option java_outer_classname = "UserFactory";
message UserCache {
// 声明字段规则为repeated,字段类型为UserInfo,即为List集合
// 等价于 => List<UserDetail>
repeated UserInfo userCacheList = 1;
// 字段类型为map,即为Map集合
// 等价于 => Map<Long, UserDetail>
map<int64, UserInfo> userCacheMap = 2;
}
message UserInfo {
int64 id = 1;
string name = 2;
}
将proto文件放置于下载的ProtoBuf编译器的bin
目录下:
1.3 输出Java文件
执行protoc.exe --java_out=./ user.proto
命令
// 编译Java文件语法规范
protoc.exe --java_out=./ proto文件名
如图,会在我们配置的输出包名路径下,生成对应的Java文件:
拷贝到我们项目中即可。
另外,IDEA也内置ProtoBuf相关插件,可通过插件生成对应的Java文件,但配置过程较为繁琐
2、引入第三方依赖库
操作ProtoBuf,我们需要在maven
中引入两个Jar
包:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.21.10</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>3.21.10</version>
</dependency>
这里对应的
Jar
版本必须要与下载的ProtoBuf编译器版本保持一直,否则会报错。如刚才我们下载的编译器版本为protoc-21.10,使用proto3语法,这里对应的
Jar
包版本即为3.21.10版本
3、编写测试类
3.1 序列化
序列化一般分为4步:
①创建对象构造器,使用newBuilder()
方法:
// 创建对象构造器
UserFactory.UserInfo.Builder userInfoBuild = UserFactory.UserInfo.newBuilder();
②为对象属性赋值,使用setter()
方法:
// 属性赋值
userInfoBuild.setId(1L).setName("xiaohao");
③构造对象,使用对象构造器的build()
方法:
// 构造对象
UserFactory.UserInfo userInfo = userInfoBuild.build();
④序列化Java对象,使用对象的toByteArray()
方法:
// 转换为字节数组
byte[] userInfoBytes = userInfo.toByteArray();
3.2 反序列化
序列化一般分为2步:
①反序列为Java对象,使用parseFrom()
方法:
// 反序列化为Java对象
UserFactory.UserInfo userInfoRes = UserFactory.UserInfo.parseFrom(userInfoBytes);
②获取对象属性值,使用getter()
方法:
// 获取属性值
long id = userInfoRes.getId();
String name = userInfoRes.getName();
3.3 完整测试结果
public static void main(String[] args) throws Exception {
// 序列化:
// 创建userInfo对象构造器
UserFactory.UserInfo.Builder userInfoBuild = UserFactory.UserInfo.newBuilder();
// 属性赋值
userInfoBuild.setId(1L).setName("xiaohao");
// 构造userInfo对象
UserFactory.UserInfo userInfo = userInfoBuild.build();
// 创建userCache对象构造器
UserFactory.UserCache.Builder userCacheBuild = UserFactory.UserCache.newBuilder();
// 属性赋值:userCacheList集合
userCacheBuild.addUserCacheList(userInfo);
userCacheBuild.addUserCacheList(userInfo);
// 属性赋值:userCacheMap集合
userCacheBuild.putUserCacheMap(userInfo.getId(), userInfo);
// 构造userCache对象
UserFactory.UserCache userCache = userCacheBuild.build();
// 转换为字节数组
byte[] userCacheBytes = userCache.toByteArray();
System.out.println("序列化结果userCacheBytes:" + Arrays.toString(userCacheBytes));
System.out.println("---------------------------------------");
// 反序列化:
UserFactory.UserCache userCacheRes = UserFactory.UserCache.parseFrom(userCacheBytes);
// 获取userCacheList集合
List<UserFactory.UserInfo> userCacheList = userCacheRes.getUserCacheListList();
// 获取userCacheMap集合
Map<Long, UserFactory.UserInfo> userCacheMap = userCacheRes.getUserCacheMapMap();
System.out.print("反序列化结果userCacheList:");
userCacheList.forEach(u -> System.out.println(u.toString()));
System.out.print("反序列化结果userCacheMap:");
userCacheMap.forEach((k, v) -> {
System.out.println(k + ":" + v);
});
}
控制台输出:
序列化结果userCacheBytes:[10, 11, 8, 1, 18, 7, 120, 105, 97, 111, 104, 97, 111, 10, 11, 8, 1, 18, 7, 120, 105, 97, 111, 104, 97, 111, 18, 15, 8, 1, 18, 11, 8, 1, 18, 7, 120, 105, 97, 111, 104, 97, 111]
---------------------------------------
反序列化结果userCacheList:id: 1
name: "xiaohao"
id: 1
name: "xiaohao"
反序列化结果userCacheMap:1:id: 1
name: "xiaohao"
四、后记
本文从ProtoBuf的基础概念介绍开始,最后引申到Java操作ProtoBuf协议,其最大的特点就是体积小、传输高效,一方面由于其使用二进制方式存储,另一方面其采用TLV(Tag-Length-Value)数据编码格式,进一步压缩了字节占用,从而减小了传输开销。
本文内容作为随笔,也希望能帮助到初学者,如果大家觉得内容对你有帮助,不妨考虑点点赞,关注关注小豪,后续小豪将会继续更新其它Java相关文章~