在开发一些远程过程调用(RPC)的程序时,一般都会涉及到对象的序列化和反序列化的问题(因为TCP或UDP这些低层协议只能传输字节流,所以应用层需要将Java POJO对象序列化为字节流才能传输)。
对象的序列化方式有以下几种方式:
一般常用的序列化方式就是JSON(性能要求不太高的Web开发等)和protobuf(高性能应用,比如和Netty一起实现高性能通信)。
使用的比较多的两个开源的处理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 泛型
* @return 反序列化后的POJO
*/
public static <T>T parseFromJson(String json, Class<T> clazz){
// 使用FastJson将JSON转换成对应得POJO
return JSONObject.parseObject(json, clazz);
}
}
首先定义一个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是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; // 地址
}
先在Github下载protobuf的发行版的安装包。下载链接 我这里下载的是最新版win64的,解压后可以在bin目录下看到一个protoc.exe程序,这个程序就是用来编译proto文件生成对应的POJO和builder代码的。可以只用在控制台用运行命令来生成,但是因为路径的问题,显得有点麻烦,官方推荐使用Maven插件来搞定它。
有一个第三方的插件protobuf-maven-plugin,通过它可以方便的利用Maven来编译proto文件。在Maven的pom文件中增加以下插件配置项,就ok啦。
<plugin>
<groupId>org.xolstice.maven.pluginsgroupId>
<artifactId>protobuf-maven-pluginartifactId>
<version>0.6.1version>
<configuration>
<protocExecutable>${project.basedir}/src/main/resources/protobuf/protoc.exeprotocExecutable>
<clearOutputDirectory>falseclearOutputDirectory>
<protoSourceRoot>${project.basedir}/src/main/resources/protobufprotoSourceRoot>
<outputDirectory>${project.basedir}/src/main/java/protooutputDirectory>
configuration>
<executions>
<execution>
<goals>
<goal>compilegoal>
<goal>test-compilegoal>
goals>
execution>
executions>
plugin>
配置好之后,直接执行插件的compile命令就可以生成Java代码啦,但此时生成的代码还不能使用,因为我们还没有引入protobuf的依赖,无法使用protobuf相关的Java方法。那么下面就引用对应的依赖。这个依赖的版本需要和刚刚下载的可执行程序的版本一致。
<dependency>
<groupId>com.google.protobufgroupId>
<artifactId>protobuf-javaartifactId>
<version>3.11.1version>
dependency>
通过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测试]
Netty内置了Protobuf专用的基础解码编码器。
下面看一个客户端和服务器端的实践。
服务器端:
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();
}
}