Netty 入站处理器的工作是IO处理操作环节的数据包解码、业务处理两个环节。在入站处理过程中,Netty底层首先读到ByteBuf二进制数据,最终需要转换成java POJO 对象,这个过程是需要Decoder(解码器)去完成的。
Netty的出站处理器的工作是IO处理操作环节的目标数据编码、把数据包写到通道。在出站处理过程中,需要将 Java POJO 对象 转换成 为最终的 ByteBuf 二进制数据,然后 才能 通过底层 Java 通道发送到对端 这个转换过程, 需要通过 Encoder (编码器)去完成。
所以Decoder 和Encoder 是Netty的核心组件。
解码器必须保证接收到的ByteBuf二进制数据包,一定是一个完整的POJO对象的二进制数据包。在 Decoder (解码器)进行解码(或反序列化)操作之前首先要确定:自己所收到的二进制数据包 ,必须 是一个完整的包,而不能是一个半包或者粘包。
所以,在解码/编码的过程中,需要解决半包和粘包问题。
什么是半包问题
Netty 发送和读取数据的地方是ByteBuf缓冲区,对于发送端,每次发送就是向通道写入一个ByteBuf,发送数据时先填好ByteBuf,然后通过通道发送出去。 对于接收端,每一次读取就是通过Handler业务业务处理器的入站方法,从通道读到一个ByteBuf。
粘包,指 Receiver (接收端)收到一个 ByteBuf ,包含了 Sender (发送端)的多个 ByteBuf ,发送端的多个 ByteBuf 在接收端“粘”在了一起。
半包,就是 Receiver 将 Sender 的一个 ByteBuf “拆”开了收,收到多个破碎的包。 换句话说, Receiver 收到了 Sender 的一个 ByteBuf 的一小部分。
可以将“粘包”的情况看成特殊的“半包”。“粘包”和“半包”可以统称 为传输的“半包问题”。具体如下图所示:
底层网络是以二进制字节报文的形式来传输数据的,并且数据在进入传输阶段之前,还会发生CPU数据复制和DMA数据复制。无论在数据传输阶段,还是在数据复制阶段,都可能存在二进制字节数据的二次分隔。
写数据的过程大致为:编码器将一个java类型的数据转换成底层能够传输的二进制ByteBuf 缓冲数据。发送端的应用层Netty程序以ByteBuf为单位来发送数据,这些数据首先会通过CPU复制的方式,复制到了底层操作系统内核缓冲区; 然后通过DMA复制的方式,DMA设备会把内核缓冲区中的数据复制到网卡设备中,当TCP内核缓冲区的单个数据包,可能比较小,一次DMA复制可能不止一个内核缓冲区的小包,会将多个小数据包一块复制,以便提升效率。
数据被复制到网卡设备之后,由于一个TCP协议报文的有效数据大小是有限制的,具体的MSS值会在三次握手阶段进行协商,最大不会超过1460字节。所以网卡设备协议栈处理程序会按照TCP/IP协议规范对数据包进行二次封装,封装成传输层TCP的协议报文之后再进行发送,在这个数据包封装的过程中,也会发送二进制数据的二次分隔。
所以,无论数据传输阶段的二进制数据分隔,还是在数据复制阶段的二进制数据分隔,都可能导致最终粘包现象或者半包现象。
netty解决半包问题的基本思路是在接收端,Netty程序需要根据自定义协议,将读取到的进程缓冲区ByteBuf,在应用层进行二次组装,重新组装应用层的数据包,。这个过程叫做分包/拆包,
接收端的这个过程通常也称为分包,或者叫做拆包。在Netty 中分包的方法,主要有两种方法:
( 1 )可以自定义解码器分包器:基于 ByteToMessageDecoder 或者 ReplayingDecoder ,定 义自己的用户缓冲区分包器。
( 2 )使 用 Netty 内置的解码器。如可以使用 Netty 内置的 LengthFieldBasedFrameDecoder 自定义长度数据包解码器,对用户缓冲区 ByteBuf 进行正确的分包。
Netty解码器
Netty的解码器是一个InBound入站处理器,负责处理“入站数据",它还能将上一站Inbound入站处理器传过来的输入数据,进行数据的解码或者格式转换,然后发送到下一个Inbound入站处理器。
解码器的职责是将输入类型为ByteBuf缓冲区的数据进行解码,输出java Pojo对象。 Netty 内置的解码器是ByteToMessageDecoder。Netty中的解码器都是 Inbound 入站处理器类型,几乎都直接或者间接地实现了入站处理的超级接口 ChannelInboundHandler 。
ByteToMessageDecoder是一个非常重要的解码器基类,它是一个抽象类,实现了解码处理的基础逻辑和流程。 ByteToMessageDecoder 继承自 ChannelInboundHandlerAdapter 适配器, 是一个入站处理器,用于完成从 ByteBuf 到 Java POJO 对象的解码功能。
ByteToMessageDecoder 解码的流程如下:
step1:它将上一站传过来的输入 到 Bytebuf 中的数据进行解码,解码出一个 List<Object> 对象列表;
step2:迭代 List<Object>列表,逐个将 Java POJO 对象传入下一站 Inbound 入站处理器。
由于ByteToMessageDecoder 是一个抽象类:
public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {
......
}
直接使用ByteToMessageDecoder 类,并不能完成ByteBuf字节码具体java类型的解码,还需要依赖其子类的具体实现。
ByteToMessageDecoder抽象类的解码方法为decode,这个方法也是一个抽象方法:
protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
作为解码器的父类, ByteToMessageDecoder 仅仅提供了一个整体框架:它会调用子类的decode 方法,完成具体的二进制字节解码,然后会获取子类解码之后的 Object 结果,放入自 己内部的结果列表 List<Object> 中,最终,父类会负责将 List<Object> 中的元素,一个一个地传递给下一个站。
从设计模式的角度来说,ByteToMessageDecoder采用的是模板方法设计模式。ByteToMessageDecoder抽象类定义了算法骨架,ByteToMessageDecoder的子类完成算法的具体实现,即需要从入站 Bytebuf 解码出来的所有 Object 实例, 加入到父类的 List<Object> 列表中。
ByteToMessageDecoder 子类实现流程如下:
step1: 继承ByteTOMessageDecoder抽象类。
step2:实现其基类的 decode 抽象方法,将ByteBuf到目标 POJO 解码逻辑写入此方法, 负责将 Bytebuf 中的二进制数据解码成Java POJO 对象。
step3:解码完成后,需要将解码后的 Java POJO 对象,放入 decode 方法的 List<Object> 实参中,此实参是父类所传入的解码结果收集容器。
余下的工作,都有父类ByteToMessageDecoder 去自动完成。在流水线的处理过程中,父 类在执行完子类的 decode 解码后,会将 List<Object> 收集到的结果,一个一个地、逐个传递到下一个 Inbound 入站处理器。
将ByteBuf 缓冲区中的字节,解码成 Integer 整数类型的代码示例如下:
public class Byte2IntegerDecoder extends ByteToMessageDecoder {
//钩子实现
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
while (in.readableBytes() >= 4) {
Integer anInt = in.readInt();
Logger.info("解码出一个整数: " + anInt);
out.add(anInt);
}
}
}
由于自定的解码器还存在各种问题,比如需要对ByteBuf的长度进行检查。 因此Netty也提供了不少开箱即可用的Decoder解码器:
梳理几个比较常见的解码器:
(1 )固定长度数据包解码器 FixedLengthFrameDecoder
适用场景:每个接收到的数据包的长度,都是固定的,例如 100 个字节。在这种场景下, 只需要把这个解码器加到流水线中,它会把入站 ByteBuf 数据包拆分成一个个长度为 100 的数 据包,然后发往下一个 channelHandler 入站处理器。
(2 )行分割数据包解码器 LineBasedFrameDecoder
适用场景:每个 ByteBuf 数据包,使用换行符(或者回车换行符)作为数据包的边界分割符。在这种场景下,只需要把这个 LineBasedFrameDecoder 解码器加到流水线中, Netty 会使用换行分隔符,把 ByteBuf 数据包分割成一个一个完整的应用层 ByteBuf 数据包,再发送到 下一站。
(3 )自定义分隔符数据包解码器 DelimiterBasedFrameDecoder
DelimiterBasedFrameDecoder 是 LineBasedFrameDecoder 按照行分割的通用版本。不同之 处在于,这个解码器更加灵活,可以自定义分隔符,而不是局限于换行符。如果使用这个解 码器,那么所接收到的数据包,末尾必须带上对应的分隔符。
( 4 )自定义长度数据包解码器 LengthFieldBasedFrameDecoder
这是一种基于灵活长度的解码器。在 ByteBuf 数据包中,加了一个长度域字段,保存了 原始数据包的长度。解码的时候,会按照这个长度进行原始数据包的提取。
Netty 编码器
在Netty 的业务处理完成后,业务处理的结果往往是某个 Java POJO 对象,需要编码成最 终的 ByteBuf 二进制类型,通过流水线写入到底层的 Java 通道。这个过程就需要用到Encoder(编码器)。
在Netty中,编码器是一个OutBound出站处理器,负责处理出站数据, 编码器还可以将上一站 Outbound 出站处理器传过来的输入( Input )数据进行编 码或者格式转换,然后传递到下一站 ChannelOutboundHandler 出站处理器。
Netty 中的编码器负责将“出站”的某种 Java POJO 对象编码成二进制 ByteBuf ,或者转换成另一种 Java POJO 对象。
编码器是ChannelOutboundHandler 的具体实现类。一个编码器将出站对象编码之后,编码后数据将被传递到下一个 ChannelOutboundHandler 出站处理器,进行后面出站处理。MessageToByteEncoder 是Netty内置的编码器,是一个抽象类,其功能是将一个 Java POJO 对象编码成一个 ByteBuf 数据包。
MessageToByteEncoder数据包。它是一个抽象类,仅仅实现了编码的基础流程,在编码过程中,通过调用encode 抽象方法来完成。
public abstract class MessageToByteEncoder<I> extends ChannelOutboundHandlerAdapter {
protected abstract void encode(ChannelHandlerContext ctx, I msg, ByteBuf out) throws Exception;
}
encode()方法没有具体的 encode 编码逻辑实现,实现 encode 抽象方法的工作需要子类去完成。
MessageToByteEncoder子类实现流程如下:
step1: 继承MessageToByteEncoder抽象类。
step2:实现其基类的 encode抽象方法,Java POJO 对象编码成二进制 ByteBuf ,或者转换成另一种 Java POJO 对象。
step3:编码完成后,基类 MessageToByteEncoder 会将输出的 ByteBuf 数据包发送到下一站。
将 Java 整数编码成二进制ByteBuf 数据包的示例代码如下:
public class Integer2ByteEncoder extends MessageToByteEncoder<Integer> {
@Override
public void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out)
throws Exception {
// 目标 bytebuf
out.writeInt(msg);
Logger.info("encoder Integer = " + msg);
}
}
使用MessageToMessageEncoder 编码器还可以将的某一种 POJO 对象编码成另外一种的 POJO对象。其子类继承MessageToMessageEncoder抽象类,并实现encode()抽象方法即可。
编码器和解码器结合
在实际的开发中,由于数据的入站和出站关系紧密,因此编码器和解码器的关系很紧密。编码和解码更是一种紧密的、相互配套的关系。在流水线处理时,数据的流动往往一进一出, 进来时解码,出去时编码。所以,在同一个流水线上,加了某种编码逻辑,常常需要加上一 个相对应的解码逻辑。
相互配套的分开的解码器和编码器在加入到通道的流水线时需要分两次添加比较麻烦,Netty提供了Codec(编解码器)类型,ByteToMessageCodec I>一个 抽象类。从功能上说,继承它就等同于继承了 ByteToMessageDecoder 解码器和MessageToByteEncoder 编码器这两个基类。
编解码器 ByteToMessageCodec 同时包含了编码 encode 和解码 decode 两个抽象方法,这两个方法都需要我们自己实现:
( 1 )编码方法 encode(ChannelHandlerContext, I, ByteBuf)
( 2 )解码方法 decode(ChannelHandlerContext, ByteBuf, List<Object>)
整数到字节、字节到整数的编解码实例如下:
public class Byte2IntegerCodec extends ByteToMessageCodec<Integer> {
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() >= 4) {
int i = in.readInt();
System.out.println("Decoder i= " + i);
out.add(i);
}
}
@Override
public void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out)
throws Exception {
out.writeInt(msg);
System.out.println("write Integer = " + msg);
}
}
编码器和解码器的结合,简单通过继承的方式,将前面的编码器的encode 方法和解码器的 decode 方法放在了同一个自定义的类中,这样在逻辑上更加紧密。当然在使用时,加 入到流水线中,也只需要加入一次。