初始Netty原理(四)— 序列化

在开发一些远程过程调用(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  泛型
     * @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.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>

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();
    }
}

你可能感兴趣的:(后端)