Netty作为网络应用框架,在网络上的各个应用之间不断进行数据交互。而网络数据交换的基本单位是字节,而java应用传输又是POJO,这就要序列化成字节再网络上传输。Netty是使用TCP/UDP在互联网上传输数据,由于Netty异步的特性,当使用TCP时,不可避免就会出现TCP粘包/拆包现象。对于TCP粘包/拆包,业界公开的有一下几种方法:
Netty使用ByteToMessageDecoder进行读半包处理,ByteToMessageDecoder是一个抽象类,继承于ChannelInboundHandlerAdapter,首先看下ByteToMessageDecoder的属性:
cumulation-用来保存累计读取到的字节
cumulator-累计器,把从channel获取到的字节累计起来有两种实现,首先看下定义
Cumulator只有一个方法 cumulate(),用于累计字节,该接口有两个实现,默认使用的是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()) {
// Expand cumulation (by replace it) when either there is not more room in the buffer
// or if the refCnt is greater then 1 which may happen when the user use slice().retain() or
// duplicate().retain() or if its read-only.
//
// See:
// - https://github.com/netty/netty/issues/2327
// - https://github.com/netty/netty/issues/1764
buffer = expandCumulation(alloc, cumulation, in.readableBytes());
} else {
buffer = cumulation;
}
buffer.writeBytes(in);
in.release();
return buffer;
}
};
in 参数表示新读取到的数据,该方法只是简单的判断了下是否要扩容,是的话就扩容,然后把新读取到的数据写入进来,释放in的内存,在返回新ByteBuf。另外一个实现是COMPOSITE_CUMULATOR
public static final Cumulator COMPOSITE_CUMULATOR = new Cumulator() {
@Override
public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in){
ByteBuf buffer;
if (cumulation.refCnt() > 1) {
// Expand cumulation (by replace it) when the refCnt is greater then 1 which may happen when the user
// use slice().retain() or duplicate().retain().
//
// See:
// - https://github.com/netty/netty/issues/2327
// - https://github.com/netty/netty/issues/1764
buffer = expandCumulation(alloc, cumulation, in.readableBytes());
buffer.writeBytes(in);
in.release();
} else {
CompositeByteBuf composite;
if (cumulation instanceof CompositeByteBuf) {
composite = (CompositeByteBuf) cumulation;
} else {
composite = alloc.compositeBuffer(Integer.MAX_VALUE);
composite.addComponent(true, cumulation);
}
composite.addComponent(true, in);
buffer = composite;
}
return buffer;
}
};
该 累计器在用于调用slice().retain()
或duplicate().retain()时扩容,扩容的方式和
MERGE_CUMULATOR一致,只不过返回的是CompositeByteBuf。
singleDecode-设置为true后,单个解码器只会解码出一个结果
decodeWasNull-解码结果为空
first-是否是第一次读取数据
discardAfterReads-多少次读取后,丢弃数据 默认16次
numReads-已经累加了多少次数据了
ByteToMessageDecoder的属性就是这些了。下面看下ByteToMessageDecoder的事件处理方法channelRead(ChannelHandlerContext ctx, Object msg)。
if (msg instanceof ByteBuf) {//只针对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 (Exception e) {
throw new DecoderException(e);
} finally {
//当存在数据且不可读,说明已经读完了,就重置状态,释放空间
if (cumulation != null && !cumulation.isReadable()) {
numReads = 0;
cumulation.release();
cumulation = null;
//判断反复累加次数是不是大于设置的discardAfterReads,当达到了discardAfterReads,就要去丢弃一些数据,防止OOM
} else if (++ numReads >= discardAfterReads) {
numReads = 0;
discardSomeReadBytes();
}
int size = out.size();
decodeWasNull = !out.insertSinceRecycled();
//触发渠道的channelRead事件,size是触发多少次事件
fireChannelRead(ctx, out, size);
//回收解码结果
out.recycle();
}
} else {
ctx.fireChannelRead(msg);
}
CodecOutputList是Netty定制的一个列表,实现了RandomAccess。该列表循环使用缓存解码结果,减少不必要的实例创建,提升性能,且与线程绑定。
再看discardSomeReadBytes()
protected final void discardSomeReadBytes() {
if (cumulation != null && !first && cumulation.refCnt() == 1) {
cumulation.discardSomeReadBytes();
}
}
大家可以看到,只有cumulation.refCnt() == 1的时候才会丢弃数据,那是因为,在用户显示的调用slice().retain()
或duplicate().retain()时,
会使refCnt>1,这时说明缓存的数据用户另有他用,所有就不丢弃数据。
fireChannelRead(ctx, out, size)
static void fireChannelRead(ChannelHandlerContext ctx, CodecOutputList msgs, int numElements) {
for (int i = 0; i < numElements; i ++) {
ctx.fireChannelRead(msgs.getUnsafe(i));
}
}
循环调用out.size次,发布channelRead事件。
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List
下面看看decodeRemovalReentryProtection(ctx, in, out)方法
final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List
该方法处理调用抽象方法decode(ctx, in, out)外,就是做了下并发处理。
到此,ByteToMessageDecoder就基本解析完了,在看一看一些细节方法
handlerRemoved(ChannelHandlerContext ctx)
@Override
public final void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
//是否正在解码中,
if (decodeState == STATE_CALLING_CHILD_DECODE) {
//正在解码中就把状态标志为STATE_HANDLER_REMOVED_PENDING,表示要销毁数据,等解码线程解完码后销毁数据,自己直接返回
decodeState = STATE_HANDLER_REMOVED_PENDING;
return;
}
ByteBuf buf = cumulation;
//累加区有数据,
if (buf != null) {
//GC回收
cumulation = null;
//缓冲区还有数据
int readable = buf.readableBytes();
if (readable > 0) {
//把缓冲区的数据全部读取出来,发布channelread事件,释放资源
ByteBuf bytes = buf.readBytes(readable);
buf.release();
ctx.fireChannelRead(bytes);
} else {
buf.release();
}
//读取次数置为0,发布channelreadComplete事件
numReads = 0;
ctx.fireChannelReadComplete();
}
//空方法,子类可以覆盖
handlerRemoved0(ctx);
}