在开发一些远程过程调用(RPC)的程序时,一般都会涉及到对象的序列化和反序列化的问题(因为TCP或UDP这些低层协议只能传输字节流,所以应用层需要将Java POJO对象序列化为字节流才能传输)。
对象的序列化方式有以下几种方式:
- JSON:将Java POJO对象转换成JSON结构化的字符串。一般用于Web应用和移动开发,可读性较强,性能较差。
- XML:与JSON一样,也是序列化为字符串,只是格式不同,可读性强,一般用于异构系统。
- JDK内置序列化:将Java POJO对象转换成二进制字节数组,可移植性强,性能较差,可读性差。
- Protobuf:类似JDK内置序列化,Google开源的高性能、易扩展框架,一般用于高性能通信。
一般常用的序列化方式就是JSON(性能要求不太高的Web开发等)和protobuf(高性能应用,比如和Netty一起实现高性能通信)。
JSON
JSON序列化框架
使用的比较多的两个开源的处理JSON的类库:
- FastJson:这是阿里开源的一个高性能的JSON库,采用独创的算法,将JSON转为Java POJO对象的速度非常快,但将复杂的POJO转换成JSON时可能会出错。
- Gson:这是Google开源的一个非常完善的JSON解析库,可以完成复杂类型的POJO和JSON之间的相互转换。
结合两者的优势,一般策略是:将POJO转为JSON时使用Gson(序列化),将JSON转成POJO时使用FastJson(反序列化)。 下面是一个FastJson和Gson结合使用的JSON工具类。
public class JsonUtil {
// 构造一个Gson的构建器
private static GsonBuilder builder = new GsonBuilder();
static {
// 禁用Html的序列化
builder.disableHtmlEscaping();
}
/**
* POJO的序列化
* @return 使用Google的Gson框架
*/
public static String convertJson(){
// 使用构建者模式创建一个Gson对象
Gson gson = builder.create();
MsgProto.Person person = ProtobufDemo.buildPerson();
// 使用Gson将POJO转换成JSON字符串
return gson.toJson(person);
}
/**
* 将json反序列化为POJO
* 使用阿里的FastJson
* @param json 要反序列化的json
* @param clazz 要反序列化的原型
* @param <T> 泛型
* @return 反序列化后的POJO
*/
public static <T>T parseFromJson(String json, Class<T> clazz){
// 使用FastJson将JSON转换成对应得POJO
return JSONObject.parseObject(json, clazz);
}
}
JSON序列化和反序列化实践
首先定义一个POJO类Person,里面调用JsonUtil工具类的方法来实现序列化和反序列化。
public class Person {
private int id;
private String name;
private String phone;
private String address;
public Person(int id, String name, String phone, String address) {
this.id = id;
this.name = name;
this.phone = phone;
this.address = address;
}
/**
* 序列化成JSON
* @return JSON字符串
*/
public String convertToJson(){
return JsonUtil.convertJson(this);
}
/**
* 反序列化为对象,这是个静态方法
* @param json JSON字符串
* @return 对象
*/
public static Person parseFromJson(String json){
return JsonUtil.parseFromJson(json, Person.class);
}
@Override
public String toString() {
return super.toString() + "name = " + name + " phone = " + phone + " address = " + address;
}
}
再写一个测试方法(使用了Junit测试框架),测试pojo的序列化和反序列化。
@Test
public void testJson(){
Person person = new Person(1, "monkJay", "13330114338", "江西九江");
// 将对象序列化为JSON字符串
String json = person.convertToJson();
LogUtil.info("序列化为JSON后的数据: [{}]",json);
// 将JSON字符串反序列化为对象
Person person1 = Person.parseFromJson(json);
LogUtil.info("反序列化后的对象:[{}]", person1);
}
测试结果:
序列化为JSON后的数据: [{"id":1,"name":"monkJay","phone":"13330114338","address":"江西九江"}]
反序列化后的对象:[pojo.Person@553f17c name = monkJay phone = 13330114338 address = 江西九江]
因为JSON就是一个字符串,所以传输JSON与传输字符串使用的都是Head-Content协议。所以在Netty中传输JSON和前面传输字符串是类似的,唯一不同的就是解码得到字符串后的处理不一样(JSON字符串还要反序列化为POJO对象)。
Protobuf
简介
protobuf是Google提出的一种数据交换的格式,它的编码过程为:使用预先定义的Message数据结构将实际传输的数据打包,编码成二进制的码流进行传输或者存储,解码过程相反。Protobuf独立于语言和平台,官方提供了多种语言的实现。
protobuf数据包是个二进制的数据,本身不具备可读性,但由于是二进制数据,所以占用空间很小,相对来说传输速度很快,所以性能高,适用于高性能、快速想用的数据传输场景(微信就是用的protobuf做消息传输)。
示例文件
Protobuf的语法可参考官方参考文档
下面是一个简单的示例(msg.proto, 文件以.proto结尾)。
// [头部声明]
syntax = "proto3"; // 声明使用的protobuf协议的版本,如果不声明,默认是proto2
// [Java选项配置]
option java_package = "protobuf"; // 生成的Java代码所在的包名
option java_outer_classname = "MsgProto"; // 生成的Java代码的类名
// [消息定义,每个消息结构体就会生成一个对应的Java POJO类]
// [生成的POJO类都会作为内部类,然后封装到外部类中,外部类就是上面声明的那个类]
message Msg {
uint32 id = 1; // 消息ID
string content = 2; // 消息内容
}
message Person {
uint32 id = 1; // ID
string name = 2; // 姓名
string phone = 3; // 电话
string address = 4; // 地址
}
通过Maven插件生成Java代码
先在Github下载protobuf的发行版的安装包。下载链接 我这里下载的是最新版win64的,解压后可以在bin目录下看到一个protoc.exe程序,这个程序就是用来编译proto文件生成对应的POJO和builder代码的。可以只用在控制台用运行命令来生成,但是因为路径的问题,显得有点麻烦,官方推荐使用Maven插件来搞定它。
有一个第三方的插件protobuf-maven-plugin,通过它可以方便的利用Maven来编译proto文件。在Maven的pom文件中增加以下插件配置项,就ok啦。
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<!-- protoc.exe可执行程序的路径,我把它放到了项目资源路径下 -->
<protocExecutable>${project.basedir}/src/main/resources/protobuf/protoc.exe</protocExecutable>
<!-- 设置是否在生成Java文件之前清空目标目录的文件 -->
<clearOutputDirectory>false</clearOutputDirectory>
<!-- 源文件路径,也就是proto文件的目录路径 -->
<protoSourceRoot>${project.basedir}/src/main/resources/protobuf</protoSourceRoot>
<!-- 输出目录路径,生成的Java代码的目录路径 -->
<outputDirectory>${project.basedir}/src/main/java/proto</outputDirectory>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
配置好之后,直接执行插件的compile命令就可以生成Java代码啦,但此时生成的代码还不能使用,因为我们还没有引入protobuf的依赖,无法使用protobuf相关的Java方法。那么下面就引用对应的依赖。这个依赖的版本需要和刚刚下载的可执行程序的版本一致。
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.11.1</version>
</dependency>
protobuf序列化实践
通过proto文件生成的POJO类都是内部类,都是没有构造函数和Setter方法的,需要使用对应生成的Builder构建器来构建POJO对象。下面新建一个类用来构建proto的POJO对象。
public class ProtobufDemo {
// 通过传入的参数来构建对象
public static MsgProto.Msg buildMsg(int id, String content){
// 通过静态方法获取一个Msg对象的构建器
MsgProto.Msg.Builder builder = MsgProto.Msg.newBuilder();
return builder.setId(id).setContent(content).build();
}
public static MsgProto.Person buildPerson(int id, String name, String phone, String address){
// 通过静态方法获取一个Person对象的构建器
MsgProto.Person.Builder builder = MsgProto.Person.newBuilder();
return builder.setId(id).setName(name).setPhone(phone).setAddress(address).build();
}
}
下面是protobuf序列化与反序列化的三种方式(还是使用的Junit测试框架)
/**
* 第一种序列化和反序列化方式
* 类似JDK的序列化方式,一般用于POJO对象的存储
*/
@Test
public void serAndDesr1() throws IOException {
MsgProto.Msg msg = ProtobufDemo.buildMsg(1, "这是第一个proto测试");
// 将protobuf对象序列化成二进制字节数组
byte[] data = msg.toByteArray();
// 构造一个二进制输出字节流
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// 将字节数组写入到输出流中
outputStream.write(data);
// 从输出流中获取字节数组
data = outputStream.toByteArray();
// 通过Protobuf反序列化
MsgProto.Msg inputMsg = MsgProto.Msg.parseFrom(data);
LogUtil.info("第一种反序列化后的数据内容: [{}]", inputMsg.getContent());
}
/**
* 第二种序列化和反序列化方式
* 直接将POJO对象的二进制字节写出到输出流完成序列化
* 从输入流中读取二进制码流完成反序列化得到POJO对象
* 这种方法一般用于阻塞式的传输中,在NIO中会出现半包、粘包问题
*/
@Test
public void serAndDesr2() throws IOException {
MsgProto.Msg msg = ProtobufDemo.buildMsg(2, "这是第二个proto测试");
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// 序列化到二进制流中
msg.writeTo(outputStream);
ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
// 直接从二进制流反序列化为POJO对象
MsgProto.Msg inputMsg = MsgProto.Msg.parseFrom(inputStream);
LogUtil.info("第二种反序列化后的数据内容: [{}]", inputMsg.getContent());
}
/**
* 第三种序列化和反序列化的方式
* 带字节长度,解决半包/粘包问题
*/
@Test
public void serAndDesr3() throws IOException {
MsgProto.Msg msg = ProtobufDemo.buildMsg(3, "这是第三个proto测试");
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// 在序列化的字节码前加了字节数组的长度,类似Head-Content协议
// protobuf在长度做了优化,使用变长类型varint32,可以节省空间
msg.writeDelimitedTo(outputStream);
ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
MsgProto.Msg inputMsg = MsgProto.Msg.parseDelimitedFrom(inputStream);
LogUtil.info("第三种反序列化后的数据内容: [{}]", inputMsg.getContent());
}
运行结果:
20:38:26.804 [main] INFO TestProtobufDemo - 第一种反序列化后的数据内容: [这是第一个proto测试]
20:38:26.818 [main] INFO TestProtobufDemo - 第二种反序列化后的数据内容: [这是第二个proto测试]
20:38:26.818 [main] INFO TestProtobufDemo - 第三种反序列化后的数据内容: [这是第三个proto测试]
protobuf在Netty中解码编码实践
Netty内置了Protobuf专用的基础解码编码器。
- ProtobufEncoder编码器:直接使用msg.toByteArray()将POJO对象编码成二进制字节数组,然后放入ByteBuf数据包中,再交给下一站处理。
- ProtobufVarint32LengthFieldPrepender长度编码器:读取数据包中的字节数,并计算字节数长度的位数(varint32的位数),然后先把长度写到输出数据包中,再把原先的数据写进去(类似Head-Content协议的字符串的写入)。
- ProtobufVarint32FrameDecoder长度解码器:根据数据包中varint32中的长度值,来解码一个足够的字节数组,然后根据长度返回缓冲区的保留切片(其实就是除去长度字段,把数据部分切片)
- ProtobufDecoder解码器:解码器在构造的时候需要指定一个原型POJO的实例,根据原型找到对应的解析器,将二进制的字节数据解析为proto中定义的POJO对象。
下面看一个客户端和服务器端的实践。
服务器端:
public class EchoServer {
private ServerBootstrap sb = new ServerBootstrap();
private int port;
public EchoServer(int port){
this.port = port;
}
public void runServer(){
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
sb.group(boss, worker)
.channel(NioServerSocketChannel.class)
.localAddress("127.0.0.1", port)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new ProtobufVarint32FrameDecoder())
.addLast(new ProtobufDecoder(MsgProto.Person.getDefaultInstance()))
.addLast(new MyDecoder());
}
});
// 阻塞直到服务器启动成功
ChannelFuture future = sb.bind().sync();
// 阻塞直到服务器关闭
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
private static class MyDecoder extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 经过Netty内置解码器的反序列化,这里得到的就是Person类型
MsgProto.Person person = (MsgProto.Person)msg;
LogUtil.info("服务器收到的数据: [{}]", person);
}
}
public static void main(String[] args){
new EchoServer(88).runServer();
}
}
客户端:
public class EchoClient {
private Bootstrap bootstrap = new Bootstrap();
private String ip;
private int port;
public EchoClient(String ip,int port){
this.ip = ip;
this.port = port;
}
public void runClient(){
NioEventLoopGroup group = new NioEventLoopGroup();
try {
bootstrap.group(group)
.channel(NioSocketChannel.class)
.remoteAddress(ip, port)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new ProtobufVarint32LengthFieldPrepender())
.addLast(new ProtobufEncoder());
}
});
// 阻塞直到客户端连接成功
ChannelFuture future = bootstrap.connect().sync();
Channel channel = future.channel();
// 构建一个Person对象
MsgProto.Person person = ProtobufDemo.buildPerson(1, "monkJay", "13330114338", "江西九江");
// 将对象写入通道
channel.writeAndFlush(person);
// 阻塞直到客户端关闭
channel.closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
public static void main(String[] args){
new EchoClient("127.0.0.1", 88).runClient();
}
}