数据接入与解码过程
我们接着上面分析的服务端接收到数据后的处理,其中有一步是执行pipelinefireChannelRead(byteBuf)方法,将数据传递给后面的处理流程,我们看看后面的解码过程。我们继续跟踪这个方法会走到
io.netty.channel.DefaultChannelPipeline.HeadContext#channelRead
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
}
public ChannelHandlerContext fireChannelRead(final Object msg) {
invokeChannelRead(findContextInbound(), msg);
return this;
}
private AbstractChannelHandlerContext findContextInbound() {
//查找inbound的channelHandlerContext
AbstractChannelHandlerContext ctx = this;
do {
ctx = ctx.next;
} while (!ctx.inbound);
return ctx;
}
在pipeline组着的双向链表中查找,没有注解@Skip的channelHandlerContext,然后执行与其绑定的 channelHandler,这里就会走到io.nettyhandler.codecByteToMessageDecoder#channelRead方法
public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter
可以看到这个ByteToMessageDecoder本质上也是一个channelHandler,我们看一下这个抽象类的channelRead方法
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
//每个线程创建一个CodecOutputList,相当于list
CodecOutputList out = CodecOutputList.newInstance();
try {
ByteBuf data = (ByteBuf) msg;
first = cumulation == null;
if (first) {
//如果第一次写入累加器值未data
cumulation = data;
} else {
//如果不是第一次写入累加器累加data值
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使用之后进行释放回收
if (cumulation != null && !cumulation.isReadable()) {
numReads = 0;
cumulation.release();
cumulation = null;
} else if (++ numReads >= discardAfterReads) {//读取次数超过16次则丢弃
// We did enough reads already try to discard some bytes so we not risk to see a OOME.
// See https://github.com/netty/netty/issues/4275
numReads = 0;
discardSomeReadBytes();
}
int size = out.size();
decodeWasNull = !out.insertSinceRecycled();
//向下传递
fireChannelRead(ctx, out, size);
out.recycle();
}
} else {
ctx.fireChannelRead(msg);
}
}
这里可以看到首先创建了一个CodecOutputList,而这个类继承了AbstractList,也里可以理解为就是一个list,这个类里面维护了一个CODEC_OUTPUT_LISTS_POOL,这是一个FastThreadLocal,也就是每个线程会对应一个 list_pool,同时CodecOutputLists实现了CodecOutputListRecycler,这个是一个netty提供回收站功能,可以复用创建的对象。我们继续回到channelRead方法中,如果是第一写入就将data值赋值给cumulation。如果不是第一次写入直接累加器方法将新的数据累加。
private Cumulator cumulator = MERGE_CUMULATOR;
public static final Cumulator MERGE_CUMULATOR = new Cumulator() {
@Override
public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
final ByteBuf buffer;
//如果cumulation可以读取的字节数小于要读取的字节数则扩展
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;
}
};
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;
}
可以看到这里就是不停的累加新加入的数据,组成一个大的bytebuf,然后调用callDecode方法,这个是解码的核心方法,我们跟踪到这个方法
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();
// Check if this handler was removed before continuing with decoding.
// If it was removed, it is not safe to continue to operate on the buffer.
//
// See:
// - https://github.com/netty/netty/issues/4635
//如果ctx已经移除则返回
if (ctx.isRemoved()) {
break;
}
outSize = 0;
}
//可读字节长度
int oldInputLength = in.readableBytes();
//由子类重写解析方法
decodeRemovalReentryProtection(ctx, in, out);
// Check if this handler was removed before continuing the loop.
// If it was removed, it is not safe to continue to operate on the buffer.
//
// See https://github.com/netty/netty/issues/1664
if (ctx.isRemoved()) {
break;
}
//解析失败则直接跳出解析方法
if (outSize == out.size()) {
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) {
throw e;
} catch (Exception cause) {
throw new DecoderException(cause);
}
}
可以看到这里会循环读取bytebuf中的数据,然后执行解码操作,如果成功解码出来了数据,就继续触发后续的 channeRead方法将解码出来的数据往后传递处理,如果在解析过程中解析失败了则会跳出当前的解码过程,同时如果解码过程中ChannelHandlerContext被移除了也会直接跳出循环。我们继续跟踪到正在的解码代码中
final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
throws Exception {
decodeState = STATE_CALLING_CHILD_DECODE;
try {
//由子类重写,解析出来的结果放入out中
decode(ctx, in, out);
} finally {
boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING;
decodeState = STATE_INIT;//恢复初始状态
if (removePending) {
handlerRemoved(ctx);
}
}
}
这里我们看到有个decode方法,解码就在这里执行
protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
他是一个抽象方法,由子类来扩展真正的解码逻辑。 我们也可以自己重写这个解码器实现自定义的解码逻辑。我
们这里看一下最简单的解码器StringDecoder,他继承了另一个解码类。
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
out.add(msg.toString(charset));
}
可以看到就是简单的将转换为指定编码的String,然后添加到结果集List中。 同时netty提供了几个默认的解码器
我们先来看看LineBasedFrameDecoder
protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
//查找/\r\n或者\n的位置
final int eol = findEndOfLine(buffer);
if (!discarding) {
if (eol >= 0) {
final ByteBuf frame;
//获取可读长度
final int length = eol - buffer.readerIndex();
//判断/\r\n或者\n,如果是前者则分隔符占用两个字节
final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
//需要读取的换行符长度,大于了最大长度
if (length > maxLength) {
//跳过这部分,从分隔符后开始在读
buffer.readerIndex(eol + delimLength);
fail(ctx, length);//记录跳过长度,并输出异常信息
return null;
}
//跳过换行符
if (stripDelimiter) {
//截取可读字节长度的buffer
frame = buffer.readRetainedSlice(length);
//跳过换行符长度
buffer.skipBytes(delimLength);
} else {
//不跳过换行符
frame = buffer.readRetainedSlice(length + delimLength);
}
return frame;
} else {
//如果没有发现换行符
final int length = buffer.readableBytes();
//读取长度大于最大读取长度则丢弃
if (length > maxLength) {
discardedBytes = length;
//修改读索引
buffer.readerIndex(buffer.writerIndex());
discarding = true;
offset = 0;
//如果快速失败则直接记录异常信息
if (failFast) {
fail(ctx, "over " + discardedBytes);
}
}
return null;
}
} else {
//如果是丢弃模式
if (eol >= 0) {
//如果有换行符,记录丢弃长度
final int length = discardedBytes + eol - buffer.readerIndex();
final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
buffer.readerIndex(eol + delimLength);
discardedBytes = 0;
discarding = false;
if (!failFast) {
fail(ctx, length);
}
} else {
//没有换行符,则直接加上可读长度
discardedBytes += buffer.readableBytes();
buffer.readerIndex(buffer.writerIndex());
}
return null;
}
}
decode方法是正在解析接入数据的方法,我们先分开来看上面的代码
private int findEndOfLine(final ByteBuf buffer) {
//可读长度
int totalLength = buffer.readableBytes();
//查找\n
int i = buffer.forEachByte(buffer.readerIndex() + offset, totalLength - offset, ByteProcessor.FIND_LF);
//判断\n的前面一个是不是\r,如果是就将查找到的位置-1
if (i >= 0) {
offset = 0;
if (i > 0 && buffer.getByte(i - 1) == '\r') {
i--;
}
} else {
offset = totalLength;
}
return i;
}
由于我们是行解码器,所以这里先查找换行分隔符,分隔符分为两种\n和\rn,所以这里先查找\n的位置,如果查找到后在查看前面的是不是\r如果是就将查询到的位置减1,如果不是就直接返回查找到的位置,如果没有查找到则返回-1。
if (!discarding) {
if (eol >= 0) {
final ByteBuf frame;
//获取可读长度
final int length = eol - buffer.readerIndex();
//判断/\r\n或者\n,如果是前者则分隔符占用两个字节
final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
//需要读取的换行符长度,大于了最大长度
if (length > maxLength) {
//跳过这部分,从分隔符后开始在读
buffer.readerIndex(eol + delimLength);
fail(ctx, length);//记录跳过长度,并输出异常信息
return null;
}
//跳过换行符
if (stripDelimiter) {
//截取可读字节长度的buffer
frame = buffer.readRetainedSlice(length);
//跳过换行符长度
buffer.skipBytes(delimLength);
} else {
//不跳过换行符
frame = buffer.readRetainedSlice(length + delimLength);
}
return frame;
} else {
//如果没有发现换行符
final int length = buffer.readableBytes();
//读取长度大于最大读取长度则丢弃
if (length > maxLength) {
discardedBytes = length;
//修改读索引
buffer.readerIndex(buffer.writerIndex());
discarding = true;
offset = 0;
//如果快速失败则直接记录异常信息
if (failFast) {
fail(ctx, "over " + discardedBytes);
}
}
return null;
}
}
如果是非丢弃模式,默认是费丢弃模式,如果查找到了换行符,计算可以读取的长度,计算换行分隔符长度,并且判断如果读取的长度大于了设置的最大读取长度就跳过这段内容,从分隔符后开始在读取,然后将异常在 pipeline中向后传播,返回null。如果长度没有查过最大长度,而且要跳过分隔符,就先截取可以读取的字节长度的bytebuf,跳过分隔符,然后返回,如果是不跳过分隔符,则连带着换行分隔符和数据本身一起返回。如果没有在查询过程中没有查找到换行分隔符,判断传入数据的可读取长度如果大于最大可读长度则直接将跳过这段数据然后设置为丢弃模式,记录丢弃的长度,如果快速失败则将异常信息向后传递,如果没有超出返回null值。
else {
//如果是丢弃模式
if (eol >= 0) {
//如果有换行符,记录丢弃长度
final int length = discardedBytes + eol - buffer.readerIndex();
final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
buffer.readerIndex(eol + delimLength);
discardedBytes = 0;
discarding = false;
if (!failFast) {
fail(ctx, length);
}
} else {
//没有换行符,则直接加上可读长度
discardedBytes += buffer.readableBytes();
buffer.readerIndex(buffer.writerIndex());
}
return null;
}
如果是丢弃模式,并且查找到了换行符则将这段数据内容包括换行分隔符一起丢弃,如果设置非快速失败则,向后传递解码异常信息。没有发现换行符,则设置丢弃长度为之前的丢弃长度加上当前数据的可读长度,并且直接将读索引设置为写索引,也就跳过了这段读取内容,然后null。
netty提供的解码器还有很多种不如:基于特定分隔符的DelimiterBasedFrameDecoder、基于特定长度的
FixedLengthFrameDecoder、基于pb文件的ProtobufDecoder等等我们这里不再详细介绍,如果感兴趣可以自己阅读codec下的内容。如果有分析错误的地方还请不吝指正。感谢!!!