Netty内部提供了两种类型的编解码器:ByteToMessageDecoder、MessageToByteEncoder、MessageToMessageDecoder、MessageToMessageEncoder、ByteToMessageCodec。其中ByteToMessageCodec表示的是message和ByteBuf之间的互相转换,它里面的encoder和decoder分别就是上面讲到的MessageToByteEncoder和ByteToMessageDecoder,用户可以继承ByteToMessageCodec来同时实现encode和decode的功能。
Netty为了解决拆包 & 粘包提供了4种ByteToMessageDecoder类型的解码器,分别为:
- LineBasedFrameDecoder。
- FixedLengthFrameDecoder。
- LengthFieldBasedFrameDecoder。
- DelimiterBasedFrameDecoder。
以上4种均为解码器,并没有与之对应的编码器。
MessageToMessageDecoder、MessageToMessageEncoder这对编解码器并不能解决拆包&粘包问题。
ByteToMessageDecoder
:netty 中的拆包也是如上这个原理,内部会有一个累加器,每次读取到数据都会不断累加,然后尝试对累加到的数据进行拆包,拆成一个完整的业务数据包,这个基类叫做 ByteToMessageDecoder。
ByteToMessageDecoder
中定义了两个累加器:
public static final Cumulator MERGE_CUMULATOR = ...;
public static final Cumulator COMPOSITE_CUMULATOR = ...;
MERGE_CUMULATOR
的原理是每次都将读取到的数据通过内存拷贝的方式,拼接到一个大的字节容器中,这个字节容器在 ByteToMessageDecoder
中叫做 cumulation
。
1.AbstractNioByteChannel
public abstract class AbstractNioByteChannel extends AbstractNioChannel {
protected class NioByteUnsafe extends AbstractNioUnsafe {
@Override
public final void read() {
final ChannelPipeline pipeline = pipeline();
//获取 ByteBuf 分配器之ByteBufAllocator
final ByteBufAllocator allocator = config.getAllocator();
// 获取自适应缓冲区分配器 ~ RecvByteBufAllocator
final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
allocHandle.reset(config);
ByteBuf byteBuf = null;
boolean close = false;
do {
// 通过 ByteBufAllocator 创建对应类型的 ByteBuf
byteBuf = allocHandle.allocate(allocator);
// 从Socket缓冲区读取相应size的客户端写入的字节数据至byteBuf
allocHandle.lastBytesRead(doReadBytes(byteBuf));
...
allocHandle.incMessagesRead(1);
readPending = false;
// 如果存在解码器ByteToMessageDecoder,则回调执行相关解码逻辑
pipeline.fireChannelRead(byteBuf);
byteBuf = null;
} while (allocHandle.continueReading());
allocHandle.readComplete();
pipeline.fireChannelReadComplete();
}
}
}
ByteBuf 分配器之ByteBufAllocator:UnpooledByteBufAllocator、PooledByteBufAllocator。
自适应缓冲区分配器RecvByteBufAllocator之AdaptiveRecvByteBufAllocator:主要用于构建一个最优大小的缓冲区来接收数据。比如,在读事件中就会通过该类来获取一个最优大小的的缓冲区来接收对端发送过来的可读取的数据。
Socket缓冲区读取数据:
- 前提是通过阻塞方法Selector.select返回的存在读事件的channel数包含当前Channel,然后遍历selectedKeys依次处理每个channel存在的read事件。
- 上述doReadBytes方法是从channel缓冲区真正读取数据。在执行该方法之前客户端可能不止一次的发送数据,所以此时ByteBuf读取的数据并非是客户端单次发送的数据,相反可能是客户端多次flush后的数据,即可能导致出现粘包情况。
- 上述doReadBytes方法一旦执行完毕即channel缓冲区读取完毕后,由于workGroup单线程的原因,此时服务端可能正携带ByteBuf流转每个handler处理ByteBuf中数据。如果客户端此时还在不断发送数据则服务端此时只能等待流转结束之后,再次通过事件调度层内部监听IO读事件继续通过NioByteUnsafe#read消费后续数据。
综上得知:数据拆包 & 粘包是发生在TCP协议层,而并非是应用层。即客户端flush数据之后TCP协议可能因为tcp缓冲区尚未完全填充导致不会及时请求服务端,而是等待客户端多次flush后统一推送至服务端导致的拆包或者粘包;也有可能因为并发高doReadBytes方法执行之前,socket缓冲区已经存在客户端多次flush后的数据,导致服务端一次从socket缓冲区读取完毕所有数据至ByteBuf导致的拆包或者粘包。
2.ByteToMessageDecoder
该解码器只是在应用层面处理粘包、拆包问题。而且针对的数据是一次read事件从channel缓冲区读到的全部字节数据。
同一个Socket在其生命周期内对ByteToMessageDecoder只实例化一次,后续不断读写针对的是同一个ByteToMessageDecoder实例。
ByteToMessageDecoder
的工作原理相对简单直观。当有新数据到达时,Netty会将这些数据缓存到一个内部缓冲区(ByteBuf
)中。ByteToMessageDecoder
会检查这个缓冲区,如果缓冲区中的数据足够进行解码(即数据长度大于等于某个特定值),它就会调用decode
方法来对数据进行解码。
在decode
方法中,开发者可以实现自己的解码逻辑,将字节流转换成应用程序可以理解的格式。解码后的消息会被添加到List<Object>
类型的out
参数中,Netty随后会将这个列表中的消息传递给下一个ChannelInboundHandler
进行处理。
如果缓冲区中的数据不足以进行解码,ByteToMessageDecoder
会保留这些数据,并等待更多的数据到达。这有效地解决了半包问题,确保了解码的正确性。
public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {
ByteBuf cumulation;
private boolean first;//判断是否是首次读写
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {//此处证明:4种解码器必须是直接针对缓冲区进行操作
CodecOutputList out = CodecOutputList.newInstance();
try {
first = cumulation == null;
// 初始化累加器 ~ 将Socket缓冲区读取到的数据Object复制到cumulation中。
// cumulation 不为空表明内部存在多余旧字节,将最新缓冲区字节追加在当前缓冲区cumulation中
cumulation = cumulator.cumulate(ctx.alloc(),first ? Unpooled.EMPTY_BUFFER : cumulation, (ByteBuf) msg);
callDecode(ctx, cumulation, out);
} finally {
// 如果ByteBuf循环处理后正好被解码器读取完毕,则下次读取数据需重新创建cumulation实例
// cumulation.isReadable 为true表明存在多余字节,还需下次继续读取
if (cumulation != null && !cumulation.isReadable()) {
numReads = 0;
cumulation.release();
cumulation = null;
} else if (++ numReads >= discardAfterReads) {// 此处说明ByteBuf存在多余字节,保留后续继续参与读取
numReads = 0;
discardSomeReadBytes();
}
int size = out.size();
firedChannelRead |= out.insertSinceRecycled();
fireChannelRead(ctx, out, size);
out.recycle();
}
} else {
ctx.fireChannelRead(msg);
}
}
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
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();
// 调用当前抽象类的具体实现类,例如:FixedLengthFrameDecoder、LengthFieldBasedFrameDecoder等Netty提供的4种解决拆包、粘包默认解码器
decodeRemovalReentryProtection(ctx, in, out);
if (ctx.isRemoved()) {
break;
}
if (outSize == out.size()) {
if (oldInputLength == in.readableBytes()) {
break;
} else {
continue;
}
}
...
}
}
}
所以,此时bytebuf缓冲区的字节数据可能存在粘包、拆包现象,具体的解码器Decoder利用各自规则通过while循环不断从bytebuf缓冲区的读取完整数据。
需要注意的是:解码器Decoder读取的数据来源是Socket缓冲区一次读取的字节数;解码器无法解决TCP协议导致的粘包、拆包问题,只能被动的按照既定规则从bytebuf缓冲区轮询读取完整数据。