8.3 通信协议的实现
我们把Java对象根据协议封装成二进制数据包的过程称为编码,把从二进制数据包中解析出Java对象的过程称为解码。在学习如何使用Netty进行通信协议的编解码之前,我们先来定义一下客户端与服务端通信的Java对象。
8.3.1 Java对象
如下代码定义通信过程中的Java对象。
1.以上是通信过程中Java对象的抽象类。可以看到,我们定义了一个版本号(默认值为1),以及一个获取指令的抽象方法。所有的指令数据包都必须实现这个方法,这样我们就可以知道某种指令的含义。
2.@Data注解由lombok提供,它会自动帮我们生产getter、setter方法,减少大量重复代码,推荐使用。
接下来,以客户端登录请求为例,定义登录请求数据包。
登录请求数据包继承自Packet定义了3个字段,分别是用户ID、用户名和密码。其中最为重要的就是覆盖了父类的getCommand()方法,值为常量LOGIN_REQUEST。
Java对象定义完成之后,我们需要定义一种规则,如何把一个Java对象转换成二进制数据,这个规则叫作Java对象的序列化。
8.3.2 序列化
如下代码定义序列化接口。
序列化接口有3个方法:getSerializerAlgorithm()方法获取具体的序列化算法标识,serialize()方法将Java对象转换成字节数组,deserialize()方法将字节数组转换成某种类型的Java对象。在本书中,我们使用最简单的JSON序列化方式,将阿里巴巴的Fastjson作为序列化框架。
然后,我们定义一下序列化算法的类型,以及默认序列化算法。
这样,我们就实现了序列化相关的逻辑。如果想要实现其他序列化算法,则只需要继承Serializer,然后定义序列化算法的标识,再覆盖两个方法即可。
序列化定义了Java对象与二进制数据的互转过程。接下来,我们学习如何把这部分数据编码到通信协议的二进制数据包中去。
8.3.3 编码:封装成二进制数据的过程
PacketCodeC.java
编码过程分为3个步骤。
1.我们需要创建一个ByteBuf,这里我们调用Netty的ByteBuf分配器来创建,ioBuffer()方法会返回适配IO读写相关的内存,它会尽可能创建一个直接内存。直接内存可以理解为不受JVM堆管理的内存空间,写到IO缓冲区的效果更高。
2.将Java对象序列化成二进制数据包。
3.我们对照本章开头的协议设计和上一章ByteBuf的API,逐个往ByteBuf写入字段,即实现了编码过程。到此,编码过程结束。
一端实现编码之后,Netty会将此ByteBuf写到另一端。另一端获得的也是一个ByteBuf对象。基于这个ByteBuf对象,就可以反解出在对端创建的Java对象,这个过程被称作解码,下面我们就来分析这个过程。
8.3.4 解码:解析Java对象的过程
PacketCodeC.java
解码的流程如下。
1.我们假定decode方法传递进来的ByteBuf已经是合法的(后面我们再来实现校验),即首个4字节是我们定义的魔数0x12345678,这里我们调用skipBytes跳过这个4字节。
2.我们暂时不关注协议版本,通常在没有遇到协议升级的时候,暂时不处理这个字段。因为在绝大多数情况下,几乎用不着这个字段,但仍然需要暂时保留。
3.我们调用ByteBuf的API分别获得序列化算法标识、指令、数据包的长度。
4.我们根据获得的数据包的长度取出数据,通过指令获得该数据包对应的Java对象的类型,根据序列化算法标识获得序列化对象,将字节数组转换为Java对象。至此,解码过程结束。
由此可以看到,解码过程与编码过程正好是相反的过程。