Netty的底层是基于TCP实现的,TCP协议在传输数据的过程中一个完整的业务可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,因此我们需要考虑Netty的粘包拆包问题
Netty提供了拆包的基类ByteToMessageDecoder,如果我们为引用程序添加了解码器每次从TCP缓冲区读到数据都会调用到ByteToMessageDecoder的channelRead方法,它是Netty解码的入口
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
CodecOutputList out = CodecOutputList.newInstance();
try {
ByteBuf data = (ByteBuf) msg;
first = cumulation == null;
if (first) {
cumulation = data;
} else {
cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
}
callDecode(ctx, cumulation, out);
} catch (DecoderException e) {
throw e;
} catch (Throwable t) {
throw new DecoderException(t);
} finally {
if (cumulation != null && !cumulation.isReadable()) {
numReads = 0;
cumulation.release();
cumulation = null;
} else if (++ numReads >= discardAfterReads) {
numReads = 0;
discardSomeReadBytes();
}
int size = out.size();
decodeWasNull = !out.insertSinceRecycled();
fireChannelRead(ctx, out, size);
out.recycle();
}
} else {
ctx.fireChannelRead(msg);
}
}
从上诉代码可以看出Netty拆包的过程主要分为一下四个流程
1.累加数据
2.将累加到的数据传递给业务进行业务拆包
3.清理字节容器
4.传递业务数据包给业务解码器处理
ByteBuf data = (ByteBuf) msg;
first = cumulation == null;
if (first) {
cumulation = data;
} else {
cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
}
累加数据是通过如上代码实现的,主要功能就是将读取到的数据塞到ButeBuf中去。上面涉及到一个累加器cumulator(实现的功能就是往ByteBuf追加数据),在该类中定义了如下两个累加器(默认情况下会使用MERGE_CUMULATOR)
public static final Cumulator MERGE_CUMULATOR
public static final Cumulator COMPOSITE_CUMULATOR
下面我们看一下MERGE_CUMULATOR是如何将新读取到的数据累加到字节容器里的
public static final Cumulator MERGE_CUMULATOR = new Cumulator() {
@Override
public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
final ByteBuf buffer;
if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes() || cumulation.refCnt() > 1 || cumulation.isReadOnly()) {
buffer = expandCumulation(alloc, cumulation, in.readableBytes());
} else {
buffer = cumulation;
}
buffer.writeBytes(in);
in.release();
return buffer;
}
};
Netty中ByteBuf的抽象使得累加非常简单,通过一个简单的API调用buffer.writeBytes(in)便将新数据累加到字节容器中,为了防止字节容器大小不够在累加之前还进行了扩容处理,扩容也是一个内存拷贝操作新增的大小即是新读取数据的大小
static ByteBuf expandCumulation(ByteBufAllocator alloc, ByteBuf cumulation, int readable) {
ByteBuf oldCumulation = cumulation;
cumulation = alloc.buffer(oldCumulation.readableBytes() + readable);
cumulation.writeBytes(oldCumulation);
oldCumulation.release();
return cumulation;
}
到这一步字节容器里的数据已是目前未拆包部分的所有的数据了
CodecOutputList out = CodecOutputList.newInstance();
callDecode(ctx, cumulation, out);
callDecode将尝试将字节容器的数据拆分成业务数据包塞到业务数据容器out中
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List
在解码之前先记录一下字节容器中有多少字节待拆,然后调用抽象函数decode进行拆包
protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List
Netty中对各种用户协议的支持就体现在这个抽象函数中,所有的拆包器最终都实现了该抽象方法
业务拆包完成之后如果发现并没有拆到一个完整的数据包,这个时候又分两种情况
1.拆包器什么数据也没读取,可能数据还不够业务拆包器处理,直接break等待新的数据
2.拆包器已读取部分数据,说明解码器仍然在工作,继续解码
业务拆包完成之后如果发现已经解析到数据包但是并没有读取任何数据,这个时候就会抛出一个Runtime异常,告诉你什么数据都没读取却解析出一个业务数据包这是有问题的
业务拆包完成之后只是从字节容器中取走了数据,但是这部分空间对于字节容器来说依然保留着,而字节容器每次累加字节数据的时候都是将字节数据追加到尾部,如果不对字节容器做清理那么时间一长就会OOM
正常情况下其实每次读取完数据,Netty都会在下面这个方法中将字节容器清理
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
numReads = 0;
discardSomeReadBytes();
if (decodeWasNull) {
decodeWasNull = false;
if (!ctx.channel().config().isAutoRead()) {
ctx.read();
}
}
ctx.fireChannelReadComplete();
}
但是当发送端发送数据过快channelReadComplete没有办法及时清理可能会引发OOM,所以为防止发送端发送数据过快Netty会在每次读取到一次数据,业务拆包之后对字节字节容器做清理,清理部分的代码如下
if (cumulation != null && !cumulation.isReadable()) {
numReads = 0;
cumulation.release();
cumulation = null;
} else if (++ numReads >= discardAfterReads) {
numReads = 0;
discardSomeReadBytes();
}
如果字节容器当前已无数据可读取,直接释放该容器并且将cumulation置为null减少下次拆包时计数器累加的工作,如果连续16次(discardAfterReads的默认值)字节容器中仍然有未被业务拆包器读取的数据那就做一次压缩,将有效数据段移到容器首部
discardSomeReadBytes之前,字节累加器中的数据分布
+--------------+----------+----------+
| readed | unreaded | writable |
+--------------+----------+----------+
discardSomeReadBytes之后,字节容器中的数据分布
+----------+-------------------------+
| unreaded | writable |
+----------+-------------------------+
这样字节容器又可以承载更多的数据了
以上三个步骤完成之后,就可以将拆成的包丢到业务解码器处理了,代码如下
int size = out.size();
decodeWasNull = !out.insertSinceRecycled();
fireChannelRead(ctx, out, size);
out.recycle();
最后调用fireChannelRead将拆到的业务数据包都传递到后续的handler,如果未解析到有效的数据包此处的msgs长度为0,即如果在拆包过程中未解析到有效的数据包,读事件不会往下传递
static void fireChannelRead(ChannelHandlerContext ctx, CodecOutputList msgs, int numElements) {
for (int i = 0; i < numElements; i ++) {
ctx.fireChannelRead(msgs.getUnsafe(i));
}
}
这样就可以把一个个完整的业务数据包传递到后续的业务解码器进行解码,随后处理业务逻辑
关于消息编码器原理与消息解码器类似,不同的是消息编码器的抽象是MessageToByteDecoder。所以此处不展开分析