我们在开发一些远程过程调用(RPC)的程序时,通常会涉及对象的序列化/反序列化
的问题,例如一个“Person”对象从客户端通过TCP方式发送到服务器端。由于TCP协议
(或者UDP等类似低层协议)只能发送字节流,所以需要应用层将Java POJO对象“序列
化”成字节流,发送过去之后,数据接收端再将字节流“反序列化”化成Java POJO对象即
可。
“序列化”和“反序列化”一定会涉及POJO的编码和格式化(Encoding & Format),
目前我们可选择的编码方式有:
⚫ 使用JSON。将Java POJO对象转换成JSON结构化字符串。基于HTTP协议,在Web
应用、移动开发方面等,这是常用的编码方式,因为JSON的可读性较强。但是它
的性能稍差。
⚫ 基于XML。和JSON一样,数据在序列化成字节流之前都转换成字符串。可读性
强,性能差,异构系统、Open API类型的应用中常用。
⚫ 使用Java内置的编码和序列化机制,可移植性强,性能稍差,无法跨平台(语
言)。
⚫ 开源的二进制的序列化/反序列化框架,例如Apache Avro,Apache Thrift、
Protobuf等。前面的两个框架和Protobuf相比,性能非常接近,而且设计原理如出
一辙;其中Avro在大数据存储(RPC数据交换、本地存储)时比较常用;Thrift的
亮点在于内置了RPC机制,所以在开发一些RPC交互式应用时,客户端和服务器
端的开发与部署都非常简单。
如何选择序列化/反序列化框架呢?
评价一个序列化框架的优缺点,大概从两个方面着手:
(1)结果数据大小,原则上说,序列化后的数据尺寸越小,传输效率越高。
(2)结构复杂度,这会影响序列化/反序列化的效率,结构越复杂,越耗时。
理论上来说,对于对性能要求不是太高的服务器程序,可以选择JSON文本格式的序列
化框架;对于性能要求比较高的服务器程序,则应该选择传输效率更高的二进制序列化框
架,建议是Protobuf。
Protobuf是一个高性能、易扩展的序列化框架,性能比较高,其性能的有关的数据可以
参看官方文档。Protobuf本身非常简单,易于开发,而且结合Netty框架,可以非常便捷地
实现一个通信应用程序。反过来,Netty也提供了相应的编解码器,为Protobuf解决了有关
Socket通信中“半包、粘包”等问题。
当然,无论是使用JSON、Protobuf还是其他的传输协议,我们必须保证在数据包的反序列化之前,接收端的ByteBuf二进制数据包一定是一个完整的应用层二进制包,不能是一
个半包或者粘包,这就涉及到通信过程中的拆包技术。
JSON(JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式。它基于
ECMAScript (欧洲计算机协会制定的JS规范)的一个子集,采用完全独立于编程语言的文本
格式来存储和表示数据。简洁和清晰的层次结构使得JSON成为理想的数据交换语言。
JSON协议是一种文本协议,易于人阅读和编写,同时也易于机器解析和生成,并能有
效地提升网络传输效率。
JSON的核心优势
XML也是一种常用的文本协议,XML和JSON都使用结构化方法来标记数据。和XML
相比,JSON作为数据包格式传输的时候具有更高的效率,这是因为JSON不像XML那样需
要有严格的闭合标签,这就让有效数据量与总数据包比大大提升,从而同等数据流量的情
况下,JSON减少网络的传输压力。
下面来做一个简单的比较。用XML表示中国部分省市数据如下:
JSON的核心优势
XML也是一种常用的文本协议,XML和JSON都使用结构化方法来标记数据。和XML
相比,JSON作为数据包格式传输的时候具有更高的效率,这是因为JSON不像XML那样需
要有严格的闭合标签,这就让有效数据量与总数据包比大大提升,从而同等数据流量的情
况下,JSON减少网络的传输压力。
下面来做一个简单的比较。用XML表示中国部分省市数据如下:
中国
广东
广州
深圳
新疆
乌鲁木齐
以上中国部分省市数据用JSON表示如下:
{
"name": "中国",
"province": [
{
"name": "广东",
"cities": {
"city": ["广州", "深圳"]
}
}, {
"name": "新疆",
"cities": {
"city": ["乌鲁木齐"]
}
可以看到,JSON的语法格式和清晰的层次结构非常简单,明显要比 XML 容易阅
读,并且在数据交换方面,由于JSON所使用的字符要比XML少得多,可以大大得节约传
输数据所占用的带宽。
Java处理JSON数据有三个比较流行的开源类库有:阿里的FastJson、谷歌的Gson和开
源社区的Jackson。
Jackson是一个简单的、基于Java的JSON开源库。使用Jackson开源库,可以轻松地将
Java POJO对象转换成JSON、XML格式字符串;同样也可以方便地将JSON、XML字符串
转换成Java POJO对象。Jackson开源库的优点是:所依赖的Jar包较少、简单易用、性能也
还不错,另外Jackson社区相对比较活跃。Jackson开源库的缺点是:对于复杂POJO类型、
复杂的集合Map、List的转换结果,不是标准的JSON格式,或者会出现一些问题。
Google的Gson开源库是一个功能齐全的JSON解析库,起源于Google公司内部需求而由
Google自行研发而来,在2008年5月公开发布第一版之后已被许多公司或用户应用。Gson
可以完成复杂类型的POJO和JSON字符串的相互转换,转换的能力非常强。
阿里巴巴的FastJson是一个高性能的JSON库。顾名思义,FastJson库采用独创的快速算
法,将JSON转成POJO的速度提升到极致,从性能上说,其反序列化速度超过其他JSON开
源库。传闻说FastJson在复杂类型的POJO转换JSON(序列化)时,可能会出现一些引用类型问题而导致JSON转换出错,需要进行引用的定制(亲自实验过,对于嵌套的java的对象是无法直接反序列化的)。
在实际开发中,目前主流的策略是:Google的Gson库和阿里的FastJson库两者结合使
用。在POJO序列化成JSON字符串的应用场景(序列化场景),使用Google的Gson库;在
JSON字符串反序列化成POJO的应用场景(反序列化场景),使用阿里的FastJson库。
Protobuf全称是Google Protocol Buffer,Google提出的一种数据交换的格式,是一套类
似JSON或者XML的数据传输格式和规范,用于不同应用或进程之间进行通信。Protobuf具
有以下特点:
(1)语言无关,平台无关
Protobuf支持Java、 C++,、Python、JavaScript等多种语言,支持跨多个平台。
(2)高效
比XML更小(3~10倍),更快(20 ~ 100倍),更为简单。
(3)扩展性,兼容性好
可以更新数据结构,而不影响和破坏原有的旧程序。
Protobuf既独立于语言,又独立于平台。Google官方提供了多种语言的实现:Java、
C#、C++、GO、JavaScript和Python。Protobuf的编码过程为:使用预先定义的Message数据
结构将实际的传输数据进行打包,然后编码成二进制的码流进行传输或者存储。Protobuf的
解码过程则刚好与编码过程相反:将二进制码流解码成Protobuf自己定义的Message结构的
POJO实例。
与JSON、XML相比,Protobuf算是后起之秀,只是Protobuf更加适合于高性能、快速
响应的数据传输应用场景。Protobuf数据包是一种二进制的格式,相对于文本格式的数据交
换(JSON、XML)来说,速度要快很多。由于Protobuf优异的性能,使得它更加适用于分
布式应用场景下的数据通信或者异构环境下的数据交换。
另外,JSON、XML是文本格式,数据具有可读性;而Protobuf是二进制数据格式,数
据本身不具有可读性,只有反序列化之后才能得到真正可读的数据。正因为Protobuf是二进
制数据格式,数据序列化之后,体积相比JSON和XML要小,更加适合网络传输。
总体来说,在一个需要大量数据传输的应用场景中,因为数据量很大,那么选择
Protobuf可以明显地减少传输的数据量和提升网络IO的速度。对于打造一款高性能的通信
服务器来说,Protobuf传输协议是最高性能的传输协议之一。微信的消息传输就采用了
Protobuf协议。
在Protobuf中,通信协议的格式是通过“.proto”文件定义的。一个“.proto”文件有两
大组成部分:头部声明、消息结构体的定义。头部声明的部分,主要包含了协议的版本、
包名、特定语言的选项设置等;消息结构体部分,可以定义一个或者多个消息结构体。
在Java中,当用Protobuf编译器(如“protoc3.6.1.exe”)来编译“.proto”文件时,编
译器将生成Java语言的POJO消息类和Builder构造者类。通过POJO消息类和Builder构造
者,Java程序可以很容易地操作在.proto文件中定义的消息和字段:包括获取、设置字段
值,将消息序列化到一个输出流中(序列化),以及从一个输入流中解析消息(反序列
化)。
官方文档:
https://developers.google.com/protocol-buffers/docs/proto3
proto描述文件的编写
syntax = "proto3";
message gps_data {
int64 id = 1;
string terminalId = 2;
string dataTime = 3;
double lon = 4;
double lat = 5;
float speed = 6;
int32 altitude = 7;
int32 locType = 8;
int32 gpsStatus = 9;
float direction = 10;
int32 satellite = 11;
}
注意:
* 顶部必须申明句法的版本号,如果不申明,则默认是2.0的语法。
* 下面Message节里面的字段就是业务需要的各个字段了,等号后面的数字是序号,必须指定。在3.5版本中不用指定required、等关键字了。
* gps_data 的名称格式与为生成的java文件的名称是有关系的,如果加入了下划线,则默认生成的是GpsData 这个驼峰格式的名称。当然你也可以在文件里自定义java文件的名称
如下:
option java_outer_classname = "BatteryData";
但是,生成的java代码中的的builder还是按照默认格式来生成的,所以,建议大家就直接按照默认规则来设计即可。
* 此外,还可以指定改java代码的包路径,命令如下:
option java_package = "com.yjgis.test";
但是这里不建议使用这个功能,因为包路径会写入到生成的代码中,一旦,代码进行重构的时候,修改起来会很麻烦,还不如直接把包路径这种功能交给ide来完成
JAVA代码的生成
生成JAVA模型的命令
在命令行工具中敲入下面的命令
protoc -I=src/main/resource/proto
--java_out=src/main/java src/main/resource/proto/protobuf.proto
说明:
-I 后面是proto文件所在的目录,
–java_out 后面是生成java文件存放地址
最后一行是proto文件的名称,可以写绝对地址,也可以直接写proto文件名称
生成的代码如下所示
绿色即是上面文件生成的代码。
在JAVA中如何使用生成的数据模型
把生成的这个java类拷贝到工程,如下图:
主要需要使用的对象就是绿色方框内的,下面,我们来写一些代码
首先在测试工程pom里增加依赖
pom:
com.google.protobuf
protobuf-java
3.5.0
JAVA代码:
import com.google.protobuf.InvalidProtocolBufferException;
public class TestGpsProtobuf {
public static void main(String[] args) {
System.out.println("===== 构建一个GPS模型开始 =====");
GpsData.gps_data.Builder gps_builder = GpsData.gps_data.newBuilder();
gps_builder.setAltitude(1);
gps_builder.setDataTime("2017-12-17 16:21:44");
gps_builder.setGpsStatus(1);
gps_builder.setLat(39.123);
gps_builder.setLon(120.112);
gps_builder.setDirection(30.2F);
gps_builder.setId(100L);
GpsData.gps_data gps_data = gps_builder.build();
System.out.println(gps_data.toString());
System.out.println("===== 构建GPS模型结束 =====");
System.out.println("===== gps Byte 开始=====");
for(byte b : gps_data.toByteArray()){
System.out.print(b);
}
System.out.println("\n" + "bytes长度" + gps_data.toByteString().size());
System.out.println("===== gps Byte 结束 =====");
System.out.println("===== 使用gps 反序列化生成对象开始 =====");
GpsData.gps_data gd = null;
try {
gd = GpsData.gps_data.parseFrom(gps_data.toByteArray());
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
System.out.print(gd.toString());
System.out.println("===== 使用gps 反序列化生成对象结束 =====");
}
}
运行一下:
Connected to the target VM, address: '127.0.0.1:59012', transport: 'socket'
===== 构建一个GPS模型开始 =====
id: 100
dataTime: "2017-12-17 16:21:44"
lon: 120.112
lat: 39.123
altitude: 1
gpsStatus: 1
direction: 30.2
===== 构建GPS模型结束 =====
===== gps Byte 开始=====
810026195048495545495045495532495458504958525233-707312243794644157-76-56118-66-113676456172185-102-103-1565
bytes长度50
===== gps Byte 结束 =====
===== 使用gps 反序列化生成对象开始 =====
id: 100
dataTime: "2017-12-17 16:21:44"
lon: 120.112
lat: 39.123
altitude: 1
gpsStatus: 1
direction: 30.2
===== 使用gps 反序列化生成对象结束 =====
如何快速的进行json格式化
我们在使用protobuf的时候,有些场景是需要快速把protobuf生成的对象转成json的,protobuf提供了很方便的方法来实现这个功能。但是这个功能并不包含在核心包里,需要依赖protobuf的工具包。
代码如下:
pom:
com.google.protobuf
protobuf-java-util
3.5.0
System.out.println("===== 使用gps 转成json对象开始 =====");
String jsonFormatM = "";
try {
jsonFormatM = JsonFormat.printer().print(gd);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(jsonFormatM.toString());
System.out.println("json数据大小:" + jsonFormatM.getBytes().length);
System.out.println("===== 使用gps 转成json对象结束 =====");
执行结果:
===== 使用gps 转成json对象开始 =====
{
"id": "100",
"dataTime": "2017-12-17 16:21:44",
"lon": 120.112,
"lat": 39.123,
"altitude": 1,
"gpsStatus": 1,
"direction": 30.2
}
json数据大小:145
===== 使用gps 转成json对象结束 =====
可以看到protobuf数据大小是json的1/3,如果数据模型更复杂或者数据量更大,优势还会更加明显。
这里只是为了展示基础用法,实际上通过Maven插件生成POJO和Builder是更好的选择
具体使用参考下文:
几种序列化方式的总结(Serializable,json,Fastjson,ProtoBuff,Hessian和Kyro)_HX_2022的博客-CSDN博客_serializable与jackson
https://www.csdn.net/tags/Mtzakg5sNTAwMTgtYmxvZwO0O0OO0O0O.html
Spring Session基于Redis存储的序列化问题 - 溪水静幽 - 博客园
spring-session 面对redis配置序列化无效后的解决方式_迷茫路人的博客-CSDN博客
Spring Boot2.X 自定义Redis的cacheManager,保存Json格式到Redis_caojidasabi的博客-CSDN博客_自定义rediscachemanager
Redis读取数据将复杂对象的List读取为Map_Y.R.J的博客-CSDN博客