一、引言
TCP是一个基于流的协议,TCP作为传输层协议并不知道应用层协议的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在应用层上认为是一个完整的包,可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和半包问题。
Netty提供了多个进站处理器来处理这个问题:
LineBasedFrameDecoder
:通过换行符来区分每个包DelimiterBasedFrameDecoder
:通过特殊分隔符来区分每个包FixedLengthFrameDecoder
:通过定长的报文来分包LengthFieldBasedFrameDecoder
:跟据包头部定义的长度来区分包
这几个类都拥有一个共同的父类:ByteToMessageDecoder
需要注意的是,ByteToMessageDecoder
的子类不允许使用@Sharable
注解(因为每个Decoder
都应当有各自的缓冲区,从逻辑上也不能够被共享),否则在构造阶段会抛出IllegalStateException
异常。
二、ByteToMessageDecoder
ByteToMessageDecoder
提供了最基本的字节转换为可识别消息的功能,也就是将多个直接从套接字读取的ByteBuf
转换为一个方便识别的ByteBuf
。一般放在ChannelPipeline
管道的头部。
ByteToMessageDecoder
持有以下成员变量:
//每次和其它ByeBuf消息碎片合并后的缓冲区
ByteBuf cumulation;
//合并策略,这里默认为通过一次内存复制操作来完成cumulation和读入的ByteBuf的合并
private Cumulator cumulator = MERGE_CUMULATOR;
//是否仅解码一条消息
private boolean singleDecode;
//是否在没有字节可读时尝试进行获取更多的字节进行解码
private boolean decodeWasNull;
//cumulation是否为null
private boolean first;
//本次解码的状态
private byte decodeState = STATE_INIT;
//当读到多少个零碎的ByteBuf时就将当前cumulation作为坏包丢弃
private int discardAfterReads = 16;
//本次解码读到的ByteBuf的数量
private int numReads;
1、channelRead方法
要了解ByteToMessageDecoder
解码机制,我们可以从它的channelRead
方法开始分析:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
//接收Channel读到的数据,此时的ByteBuf数据可能是不可读的
//构造一个List,用于存放每个ByteBuf解码的结果
CodecOutputList out = CodecOutputList.newInstance();
try {
ByteBuf data = (ByteBuf) msg;
first = cumulation == null;
if (first) //如果cumulation为null,就无需进行ByteBuf的合并
cumulation = data;
else //否则调用Cumulator的cumulate方法将当前ByteBuf和cumulation合并
cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
callDecode(ctx, cumulation, out);
} catch (DecoderException e) {
throw e;
} catch (Exception e) {
throw new DecoderException(e);
} finally {
//如果cumulation中的字节已经全部解码成功,那么将当前ByteToMessageDecoder复位
if (cumulation != null && !cumulation.isReadable()) {
numReads = 0;
cumulation.release();
cumulation = null;
//否则cumulation还仍有零散的消息碎片
} else if (++ numReads >= discardAfterReads) {
numReads = 0;
discardSomeReadBytes(); //丢弃已经读取到的字节
}
int size = out.size();
decodeWasNull = !out.insertSinceRecycled();
fireChannelRead(ctx, out, size);
out.recycle();
}
} else {
//如果不是ByteBuf,就忽略这个消息传递给下一个管道
ctx.fireChannelRead(msg);
}
}
channelRead
方法的执行可以分为以下几个步骤:
-
获取一个
CodecOutputList
,用于存放每次channelRead
方法调用完成后的解码结果
CodecOutputList
实现了java.util.List
接口,并通过FastThreadLocal
存放在InternalThreadLocalMap
中,每个线程都默认持有16个CodecOutputList
实例,通过CodecOutputList
池CodecOutputLists
来维护,如果所需的CodecOutputList
超出16个,那么会默认实例化一个新的CodecOutputList
实例。 -
将当前
ByteBuf
与之前读到的ByteBuf
(成员变量cumulation
)进行合并
如果cumulation
为空,那么直接将当前ByteBuf
引用赋给cumulation
如果cumulation
不为空,那么将根据成员变量cumulator
定义的合并策略进行ByteBuf
的合并。
Cumulator是ByteToMessageDecoder的内部接口,定义了一个方法:ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in);
参数
alloc
为ByteBuf
分配器,用于分配一个新的ByteBuf
,可以通过调用ChannelHandlerContext
的alloc
方法获得。cumulation
为原ByteBuf
缓冲区,in
为需要被合并的ByteBuf
缓冲区,其返回值为合并后的ByteBuf
缓冲区。
ByteToMessageDecoder
默认定义了2种Cumulator
实现类:ByteToMessageDecoder.MERGE_CUMULATOR
和ByteToMessageDecoder.COMPOSITE_CUMULATOR
。
MERGE_CUMULATOR
的合并策略是通过ByteBufAllocator
分配一个大小为cumulation
加上in
的可读字节数,然后将cumulation
和in
的数据复制到缓冲区中,所以MERGE_CUMULATOR
需要一次内存复制操作。ByteToMessageDecoder
默认采用这种策略合并缓冲区。
COMPOSITE_CUMULATOR
的合并策略是通过CompositeByteBuf
来完成ByteBuf
的合并,它可以持有多个ByteBuf
实例,所以不需要进行内存复制操作。但是CompositeByteBuf
的索引算法实现较为复杂,可能会比MERGE_CUMULATOR
要慢。
- 合并完成后,对合并后的
ByteBuf
缓冲区(cumulation
)的数据进行解码
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
try {
while (in.isReadable()) {
int outSize = out.size(); //注:在第一次循环中outSize为0
if (outSize > 0) {
//如果List的长度大于0,说明已经有解码好的消息
//产生一个ChannelRead事件,并将集合out的每个元素传播到管道
fireChannelRead(ctx, out, outSize);
out.clear(); //清空这个List
if (ctx.isRemoved()) //如果已经从管道中移除,那么退出循环结束
break;
outSize = 0;
}
//获取可读字节数量
int oldInputLength = in.readableBytes();
decodeRemovalReentryProtection(ctx, in, out);
if (ctx.isRemoved()) //如果this已经从管道移除,那么退出循环
break;
if (outSize == out.size()) {
//如果集合out元素数量在本次循环中没有改变
//如果在decodeRemovalReentryProtection没有处理任何数据
if (oldInputLength == in.readableBytes())
break;
else
continue;
}
if (oldInputLength == in.readableBytes()) //一般不会发生
throw new DecoderException(StringUtil.simpleClassName(getClass()) +
".decode() did not read anything but decoded a message.");
if (isSingleDecode()) //如果仅解码一条消息,那么退出循环
break;
}
} catch (DecoderException e