Netty提供的解码器
Netty提供的解码器的基类是ByteToMessageDecoder
, netty默认提供的几个非常有用的解码器都是它的子类
- FixedLengthFrameDecoder: 适用于业务包长度固定的情况, 比如TS流, 构造器传入每个业务包的固定长度值, 解码器接收到数据后, 会按照定长来划分业务包并包业务交给后续的处理器(如自定义的handler), 如果当前接收的数据在划分后还有剩余字节, 则会跟后续接收的数据拼接在一起继续划分
- LengthFieldBasedFrameDecoder: 适用于有协议头(并含有数据长度字段)的情况, 比如PS流, 构造器分别传入如下四个参数:
- lengthFieldOffset: 长度字段在数据包的位置
- lengthFieldLength: 长度字段的字节数
- lengthAdjustment: 长度调整值, 用来确定最终返回的业务包的数据, 计算方式: 从长度字段后, 取
长度+该值
作为负载数据长度, 如果只需要返回负载数据, 比如数据包长16字节, 长度字段是2字节, 偏移是1字节, 负载数据紧随长度字段之后, 长度是13字节, 那么: 情形1)长度字段是数据包总长16, 调整值就是13-16=-3; 情形2) 长度是负载数据长度13, 调整值就是13-13=0, - initialBytesToStrip: 表示被抛弃的头部字节数, 该值=长度字段值+长度条件值
- LineBasedFrameDecoder: 把数据包按照换行符划分为业务包, 换行符是
\n
或\r\n
- DelimiterBasedFrameDecoder: 用自定义的分隔符来划分业务包
- HttpRequestDecoder/HttpResponseDecoder: 支持HTTP协议的解码器
- RtspDecoder: 继承自HttpObjectDecoder, 支持RTSP协议的解码器
此外, 也可以通过继承ByteToMessageDecoder
来实现自定义解码器
Netty实现粘包和拆包
- 粘包: 每次接收到的数据包中有若干个业务包
- 分包: 一个业务包被分隔为了若干个数据包
二者都是数据接收过程中产生的问题, 所以需要在解码的过程中做处理. 具体解决办法有两种:
- 使用Netty提供的解码器, 优点是编写的代码更少, 因为解码器会根据解码器类型和应用设置自动去处理业务部的划分, 所以可以自动处理粘包和分包
- 自定义解码器, 优点是灵活度更高, 适合自定义协议, 因为粘包和分包可以同时处理, 所以下面的例子放在一起实现:
public class MyProtocolHandler extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
byte[] msg = new byte[16];
while (in.readableBytes() > 16) {
in.readBytes(msg);
out.add(new String(msg));
}
}
}
说明:
- 首先自定义解码器继承了
ByteToMessageDecoder
并重载了decode
方法 - 为了示例简便, 假定每个业务包长度是16字节
- 粘包处理: 每次读取16字节后把业务包加入到
out
列表中 - 分包处理: 如果剩余字节数不足16字节则返回, Netty会在收到后续数据后, 和上次处理剩余数据一起传入
decode
中
Netty实现拆包处理的原理
核心代码是如下方法, 已添加关键注释说明流程:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
// out封装了业务包, 一般是某个类对象的列表
CodecOutputList out = CodecOutputList.newInstance();
try {
// 判断是否第一次处理数据
first = cumulation == null;
// 根据first来决定是否合并遗留数据
cumulation = cumulator.cumulate(ctx.alloc(),
first ? Unpooled.EMPTY_BUFFER : cumulation, (ByteBuf) msg);
// 内部用一个循环来调用decode方法, out返回处理出来的业务对象
callDecode(ctx, cumulation, out);
} finally {
// 如果遗留数据被处理完, 则清空遗留数据缓冲
if (cumulation != null && !cumulation.isReadable()) {
numReads = 0;
cumulation.release();
cumulation = null;
} else if (++ numReads >= discardAfterReads) {
// 如果遗留数据太多超过了缓冲区大小, 则丢弃最早收到的部分字节
numReads = 0;
discardSomeReadBytes();
}
// 到这里就是cumulation不为空, 且有可读数据, 即遗留数据
//------------
int size = out.size();
firedChannelRead |= out.insertSinceRecycled();
// 把本解码器解码出来的业务包投递给下一个解码器
fireChannelRead(ctx, out, size);
out.recycle();
}
} else {
ctx.fireChannelRead(msg);
}
}
cumulation
: 保存了遗留数据, 类型是ByteBuf
cumulator
:Cumulator
接口对象, 接口cumulate可以把遗留数据和新收到的数据拼接在一起- 在
ByteToMessageDecoder
中有两个Cumulator
的实现类:MERGE_CUMULATOR
和COMPOSITE_CUMULATOR
MERGE_CUMULATOR
: 通过合并缓冲区的方式把遗留数据和新数据拼接起来, 需要数据拷贝, 默认采用这种方式COMPOSITE_CUMULATOR
: 通过指针计算的方式把遗留数据和新数据拼接起来, 需要计算索引位置, 实现复杂, 但是没有数据拷贝
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
try {
while (in.isReadable()) {
int outSize = out.size();
if (outSize > 0) {
fireChannelRead(ctx, out, outSize);
out.clear();
// 每次继续循环前先判断当前处理器是否被移除了
if (ctx.isRemoved()) {
break;
}
outSize = 0;
}
int oldInputLength = in.readableBytes();
// 内部调用decode方法
decodeRemovalReentryProtection(ctx, in, out);
// 同上
if (ctx.isRemoved()) {
break;
}
// 如果经过一次decode方法后, 业务对象数量没有变化
if (outSize == out.size()) {
if (oldInputLength == in.readableBytes()) {
// 并且in内部的数据没有被读取, 即调用decode前后没有变化, 说明没有新的业务对象可以读取, 所以退出循环
break;
} else {
continue;
}
}
if (oldInputLength == in.readableBytes()) {
// 如果in的数据没有被读取, 但是业务对象数量变化了, 则抛出异常
throw new DecoderException(
StringUtil.simpleClassName(getClass()) +
".decode() did not read anything but decoded a message.");
}
// 如果循环只处理一次decode方法, 则退出循环
if (isSingleDecode()) {
break;
}
}
}
- 所以在自定义实现时,
decode()
中一定要注意如果没有获取到业务对象, 千万不要out.add()
- in是
ByteBuf
类型, 内部包含一个读索引一个写索引, 在decode中用in.read()
读取数据后会移动读索引 ByteBuf
的isReadable()
返回是否还有可读数据, 一般就是判断写索引是否大于读索引