一文搞懂Java应用ProtoBuf协议


一文搞懂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语法结构如下:

在这里插入图片描述

  1. 语法版本:首行syntax = "proto3"代表当前使用的是proto3规范的语法
  2. 包名:第二、三行代表输出Java文件之后的对应的包以及类名(注:生成的Java类名不要与message声明的消息名称一致)
  3. 消息定义:剩下的其它行代表message消息结构与enum枚举类型。

其中message关键字,作用为定义一种消息类型,类似于Java中定义Class实体类一样,message里面定义所包含的属性,属性由字段规则、字段类型、字段名称和字段编号组成。

// 属性格式
[字段规则] 字段类型 字段名称 = 字段标识符;
2.2 字段规则

字段规则类似于MySQL中的约束,即对数据的限制条件。

ProtoBuf常见的约束有以下几种:

  1. singular:字段可以出现0次或出现1次(不得超过1次),proto3语法默认的字段规则
  2. repeated:字段可以出现任意多次(包括0次),类似于Java中的集合
2.3 字段类型

字段类型包括常用的标量类型与复合类型。

2.3.1 标量类型

标量类型类似于Java中的基本类型,对应关系如下表所示:

ProtoBuf类型Java类型默认值
int32int0
int64long0L
floatfloat0.0F
doubledouble0.0D
boolbooleanfalse
stringString“”
2.3.2 复合类型

复合类型可以是其它message消息,或enum枚举等,类似于Java中的引用类型。

这里分别列举一下Java中ListMap等数据结构在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相关文章~

  • 15
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值