目录
序列化与反序列化简介:
序列化也叫编码。就是把对象变成二进制(字节数组),序列化主要有两个目的:
- 网络传输
- 对象技久化
反列化也叫编解码,就是把二进制变成对象
JDK的序列化
JDK从1.1版本提供了序列化,无需添加额外的类库,只需要POJO实现Serializable接口即可通过ObjectInputStream、ObjectOutputStream读取或写出,但是jdk自身的序列化性能太低,编流太大不适应一些高并发通信场合网络通信,并且无法跨语言
class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long userId;
private String username;
private String realName;
private Integer age;
public User(Long userId, String username, String realName, Integer age) {
this.userId = userId;
this.username = username;
this.realName = realName;
this.age = age;
}
public static long getSerialVersionUID() {
return serialVersionUID;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getRealName() {
return realName;
}
public void setRealName(String realName) {
this.realName = realName;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
public class SerializableTest {
public static void main(String[] args) throws IOException {
User user = new User(100000L, "admin", "小三", 18);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.putLong(user.getUserId());
byteBuffer.put(user.getUsername().getBytes());
byteBuffer.put(user.getRealName().getBytes());
byteBuffer.putInt(user.getAge());
byteBuffer.flip();
System.out.println("使用ByteBuffer编码后的编流大小:" + byteBuffer.remaining());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(user);
oos.flush();
oos.close();
System.out.println("使用jdk序列化后的编流大小:" + baos.toByteArray().length);
}
}
由测试结果可以看出,JDK序列化机制编码后的码流大小是采用手工二进制编码的13倍
接下来进测试一下序列化性能:
public class SerializableTest {
public static void main(String[] args) throws IOException {
User user = new User(100000L, "admin", "小三", 18);
final int times = 10000;
long startTime = System.currentTimeMillis();
for (int i = 0; i <= times; i++){
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.putLong(user.getUserId());
byteBuffer.put(user.getUsername().getBytes());
byteBuffer.put(user.getRealName().getBytes());
byteBuffer.putInt(user.getAge());
}
long endime = System.currentTimeMillis();
System.out.println("使用ByteBuffer编码后花费的时间:" + (endime - startTime));
startTime = System.currentTimeMillis();
ByteArrayOutputStream baos = null;
ObjectOutputStream oos = null;
for (int i = 0; i <= times; i++){
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(user);
oos.flush();
oos.close();
}
endime = System.currentTimeMillis();
System.out.println("使用jdk序列化后的花费的时间:" + (endime - startTime));
}
}
由测试结果可以看出,JDK序列化机制编码所花费的时间是采用手工二进制编码的5.7倍
以上两点说明编码后字节数组越大,意味着越占空间,存储的硬件成本越高,在网络传输时占用更大带宽,导致系统呑吐量下降,因此现在RPC框架中都不采用JDK的序列化
Protobuf序列化
官方文档:https://developers.google.cn/protocol-buffers
它将数据结构定义成一个*.proto文件,通过代码生成工具 protoc生成对应的POJO对象和Protbuf相关的对象
定义数据结构定义文件:User.proto
syntax = "proto3";
package proto;
option java_package = "com.isaiah.nettydemo.proto";
option java_outer_classname = "User";
message UserMessage{
int64 userId = 1;
string username = 2;
string realName = 3;
int32 age = 4;
}
在java工程中添加protobuf的jar包
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.10.0</version>
</dependency>
对proto文件进行编译生成java类:protoc User.proto --java_out=/Users/isaiah/Workspace/xlb/NettyDemo/src/main/java
测试:
public class ProtobufTest {
public static void main(String[] args) throws IOException {
User.UserMessage userMessage = User.UserMessage.newBuilder()
.setUserId(10000L)
.setUsername("admin")
.setRealName("小三")
.setAge(18).build();
byte[] bytes = userMessage.toByteArray();
System.out.println("使用protobuf序列化后的编流大小:" + bytes.length);
User.UserMessage u = User.UserMessage.parseFrom(bytes);
System.out.println(u);
}
}
优点
- 跨语言,可自定义数据结构。
- 字段被编号,新添加的字段不影响老结构。解决了向后兼容问题。
- 自动化生成代码,简单易用。
- 二进制消息,效率高,性能高。
- Netty等框架集成了该协议,提供了编×××提高开发效率。
缺点
- 二进制格式,可读性差(抓包dump后的数据很难看懂)
- 对象冗余,字段很多,生成的类较大,占用空间。
- 默认不具备动态特性(可以通过动态定义生成消息类型或者动态编译支持)
总结:简单快速上手,高效兼容性强,维护成本较高。
Thrift框架中的序列化
与protobuf类似,Thrift通过IDL描述接口定义数据结构,通过代码生成工具thrift生成对应的POJO代码,它的以下特点:
优点
- 序列化和RPC支持一站式解决,比pb更方便
- 跨语言,IDL接口定义语言,自动生成多语言文件
- 省流量,体积较小
- 包含完整的客户端/服务端堆栈,可快速实现RPC
- 为服务端提供了多种工作模式,如线程池模型、非阻塞模型
缺点
- 早期版本问题较大,0.7以前有兼容性问题
- 不支持双通道
- rpc方法非线程安全,服务器容易被挂死,需要串行化。
- 默认不具备动态特性(可以通过动态定义生成消息类型或者动态编译支持)
- 开发环境、编译较麻烦
总结:跨语言、实现简单,初次使用较麻烦,需要避免使用问题和场景限制。
Protobuf与Thrift框架对比
相比Thrift支持更多语言C++, Java, Python, Ruby, Perl, PHP, C#, Erlang, Haskell
- Thrift的字节码并不紧凑,比如每个字段的id占4个字节,类型占1个字节;而Google Protocol Buffers的字段id和类型占同一个字节,而且对于i32等类型还会使用varint减少数组长度。
- Thrift生成的Java代码很简洁,代码实现也很简洁;Google Protocol Buffers生成的Java代码动不动就几千行……
- Thrift不单单是一个序列化协议,更是一个rpc调用框架;从这方面来说,Google Protocol Buffers是完全做不到的。
MessagePack序列化框架
public class MsgPackTest {
public static void main(String[] args) throws IOException {
User user = new User(100000L, "admin", "小三", 18);
MessagePack messagePack = new MessagePack();
byte[] buff = messagePack.write(user);
System.out.println("使用MessagePack序列化后的码流大量:" + buff.length);
User u = messagePack.read(buff, User.class);
System.out.println(u);
}
}
优点
- 跨语言,多语言支持(超多)
- It’s like JSON.but fast and small.序列化反序列化效率高(比json快一倍),文件体积小,比json小一倍。
- 兼容json数据格式
缺点
- 缺乏复杂模型支持。msgpack对复杂的数据类型(List、Map)支持的不够,序列化没有问题,但是反序列化回来就很麻烦,尤其是对于java开发人员。
- 维护成本较高。msgpack通过value的顺序来定位属性的,需要在不同的语言中都要维护同样的模型以及模型中属性的顺序。
- 不支持模型嵌套。msgpack无法支持在模型中包含和嵌套其他自定义的模型(如weibo模型中包含comment的列表)。
总结:高性能但扩展性较差维护成本较高。
JSON与XML
json、xml本身是字符串,而protobuf、thrift序列化最终是二进制,在性能上无法相提并论,但在使用上json、xml相比非常简单
json解析框架主要有三个:fastjson、jackson、gson,在java11中已jdk内置json解析的API
JSON
优点
- 简单易用开发成本低
- 跨语言
- 轻量级数据交换
- 非冗长性(对比xml标签简单括号闭环)
缺点
- 体积大,影响高并发
- 无版本检查,自己做兼容
- 片段的创建和验证过程比一般的XML复杂
- 缺乏命名空间导致信息混合
总结:最简单最通用的应用协议,使用广泛,开发效率高,性能相对较低,维护成本较高。
fastjson
阿里开发的一个json解析框架,号称速度最快的josn解析库
jackson
spring框架默认使用的序列化框架,特点:功能强大,但是使用上比较复杂
Gson
Gson在安卓开发中应用的比较多,使用上也非常简单
XML
dom4j
sax
jdk内置
编码与反解码也称编组、解编组
应用场景分析
json/xml序列化在传输本质上是字符串,适合在系统业务内部,而jdk序列化/protobuf/Thrif是二进制更适合RPC系统间通信传输