Netty 编码和解码
1. 前言
本节内容,主要是讲解 Netty 的编码和解码,前面我们讲解了 ByteBuf,Netty 是面向 ByteBuf 来编程的,发送的内容会被编码成 ByteBuf,从 Channel 接受的数据流则被封装成了 ByteBuf,需要把它解码成我们所熟悉的格式。
2. 编码和解码的作用
首先,我们先通过一个实例来进行说明。
实例:
ch.pipeline().addLast(new CodecHandler());
public class CodecHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("接受:"+msg.toString());
}
}
客户端发送数据:
执行结果:
接受:PooledUnsafeDirectByteBuf(ridx: 0, widx: 5, cap: 1024)
通过以上测试,发现客户端往服务端发送普通的字符串,服务端接受的时候并不是正常字符串,而是把 ByteBuf 类型打印出来。
主要原因是,Netty 的数据类型是 ByteBuf,无法直接强转,需要通过解码的方式去转换才能得到正常的数据,编码也是同样道理。
因此,本节学编码和解码的知识可以了解 Netty 如何去接受和发送参数。
3. 解码示例
实例:
public class ServerLoginHandler extends ChannelInboundHandlerAdapter {
//1.读取客户端发送过来的数据
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//1.转换ByteBuf
ByteBuf buffer=(ByteBuf)msg;
//2.定义一个byte数组,长度是ByteBuf的可读字节数
byte[] bytes=new byte[buffer.readableBytes()];
//3.往自定义的byte[]读取数据
buffer.readBytes(bytes);
//4.字节流->字符串
String str=new String(bytes);
}
}
通过以上代码,我们发现能正常接收并且打印客户端发送过来的字符串数据。但是如果是其它的类型数据(比如:Map,实体,List 等)那么还得手工写另外的转换方法,相对比较麻烦。
4. 编码解码流程
4.1 整体流程
无论是使用 Netty 还是原始的 Socket 编程,基于 TCP 通信的数据包格式均为二进制,但是我们平时开发不可能基于二进制去开发,而是封装一个一个的实体。这样的话,我们就需要实现实体和二进制之间的编码和解码了。
- 客户端往服务端发送消息,手写需要把实体转换成 byte [],并且把 byte [] 写入到 ByteBuf 容器里面,最终转换二进制。其实,整个过程就是一个编码的过程;
- 服务端接受到消息,二进制是给机器去识别的,人眼无法快速去识别它,然而实体是我们所熟悉并且一看就能看出有哪些属性,因此需要把二进制转换我们所熟悉的实体,整个过程就是一个解码的过程。
4.2 编码流程
实例:
//封装编码方法
public ByteBuf encode(Object obj) {
// 1. 创建 ByteBuf 对象
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.ioBuffer();
// 2. 序列化 Java 对象
byte[] bytes = SerializeUtils.serialize(obj);
// 3. 实际编码过程
byteBuf.writeBytes(bytes);
return byteBuf;
}
//序列化工具类
public class SerializeUtils{
//序列化方法
public static byte[] serialize(Object obj){
//省略序列化过程
return null;
}
}
代码说明:
- 创建一个 ByteBuf(前面章节详细讲解过);
- 把内容序列化成字节数组;
- 把字节数组写入到 ByteBuf。
4.3 解码流程
实例:
//解码
public <T> T decode(ByteBuf byteBuf,Class clazz) {
// 数据包长度
int length = byteBuf.readableBytes();
byte[] bytes = new byte[length];
byteBuf.readBytes(bytes);
return SerializeUtils.desrialize(bytes,clazz);
}
//序列化工具类
public class SerializeUtils{
//序列化方法
public static <T> T desrialize(byte[] bytes,Class clazz){
//省略反序列化过程
return null;
}
}
代码说明:
- 根据 ByteBuf 获取可读的数据长度;
- 根据数据长度创建相应的字节数组;
- 把 ByteBuf 里面的内容读取到自定义的字节数组里面;
- 通过反序列化的手段,把字节数组反序列化成对象。
5. 序列化和反序列化
上面讲编码和解码的时候,涉及两个空方法没有实现,分别是 serialize()
序列化和 desrialize()
反序列化,其实序列化和反序列化技术选择很多,常见的解决方案大概如下:
- 通过对象流来手工实现序列化,但是实体必须实现
Serializeable
序列化接口,否则无法被正常序列化和反序列化; - 对象 -> 转换 json 格式的字符串,Java 里面 String 类型字符串可以自动转换字节数组,常见的开源框架分别有 Fastjson、Jackjson 等;
- 对象 - 转存 xml 格式的字符串,常见框架有 XStream 等;
- 其他技术,如:Hessian 序列化、Kryo 序列化等。
这里就不详细展开展示序列化和反序列化的说明,如果有兴趣,可以参考我写的另外一篇文章:
接下来,主要说明的是,为了灵活扩展,我们最好不要写死某种序列化技术,为了方便后期更改技术框架,因为每种序列化技术的差距比较大,主要体现两点:
- 消耗时间: 序列化和反序列化的消耗时间长度;
- 数据长度: 序列化过后的字节数组长度,这个是会影响网络传输性能的。
一般情况下,通过面向接口 + 策略模式的方式去解耦,底层可以灵活的切换序列化技术。
实例:
//定义一个序列化接口
public interface SerializeService<T>{
//序列化方法
public byte[] serialize(T t);
//反序列化方法
public T deserialize(byte[] bytes,Class<T> clazz);
}
//具体序列化实现列
public class JsonSerializeService<T> implements SerializeService<T>{
//序列化方法
public byte[] serialize(T t){
return null;
}
//反序列化方法
public T deserialize(byte[] bytes,Class<T> clazz){
return null;
}
}
//序列化使用
@Component
public class Test{
@Autowired
private SerializeService serializeService;
public ByteBuf encode(Object obj) {
// 1. 创建 ByteBuf 对象
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.ioBuffer();
// 2. 序列化 Java 对象
byte[] bytes = serializeService.serialize(obj);
// 3. 实际编码过程
byteBuf.writeBytes(bytes);
return byteBuf;
}
}
6. 小结
本节内容大家掌握好以下内容:
- 编码和解码的概念是什么?为什么需要编码和解码?
- Netty 如何去进行编码和解码,以及大体流程是什么?
- 编码和解码需要依赖序列化和反序列化技术,要了解序列化方面的技术有哪些。
思考题:能否把我们的编码和解码封装成独立的 Handler 呢?那么应该如何去封装呢?