Netty 自带多个粘包拆包解码器。今天介绍 LineBasedFrameDecoder,换行符解码器。看起来简单,但当前版本(2018-09-04)是一个 bug,此 bug 截止本文发布时,已被修复。
虽然官方注释标注此解码器为换行符解码器,但我认为这是一个“换行符+最大长度”的一个解码器。
说下此解码器的逻辑:
首先限制单个包的大小,也是说,换行符和换行符之间的数据包的大小是用户配置的。超过最大则抛出异常。
每当有数据到来时,都会遍历缓冲区,找换行符,如果找到了,就返回数据;如果没找到,就记录此次读取到下标 offset,下次继续找。
如果找的过程中,长度超过用户设置的 maxLength,就进入丢弃模式:丢弃之前读取的所有字节,并根据 failFast 的属性决定什么时候抛出异常(如果是 true,则不等找到换行符,就抛出异常,反之,等找到换行符了,才抛出异常,这个异常是可以被用户的 exceptionCaught 方法捕获到的)。
整个流程大概如下图:
这里有个注意的地方:每当发现长度超出 maxLength 时,应该丢弃之前的所有数据,并重置 offset,代码如下:
if (length > maxLength) {// 如果大于愿意解码的最大长度,废弃 discardedBytes = length;// 废弃字节数 buffer.readerIndex(buffer.writerIndex());// 更新读下标 discarding = true;// 开启丢弃模式,因为tcp 包大小已经大于最大长度. offset = 0;// 最后一次扫描次数重置 if (failFast) {// 如果是快速失败,抛出异常 fail(ctx, "over " + discardedBytes); } }
但是,在丢弃模式的 netty 代码,却没有将 offset 重置为 0。代码如下:
·····忽略 } else { discardedBytes += buffer.readableBytes(); buffer.readerIndex(buffer.writerIndex()); // offset = 0; }
注意:offset 的代码是我加的,本来是没有的。这将导致 findEndOfLine 方法的下标越界。
private int findEndOfLine(final ByteBuf buffer) { int totalLength = buffer.readableBytes(); int i = buffer.forEachByte(buffer.readerIndex() + offset, totalLength - offset, ByteProcessor.FIND_LF); if (i >= 0) { offset = 0; if (i > 0 && buffer.getByte(i - 1) == '/r') { i--; } } else { offset = totalLength; } return i; }
这里,注意:每次读取不到换行符的时候,offset 都会设置为这次的读下标,而 decode 方法中,已经执行了 buffer.readerIndex(buffer.writerIndex()) 方法,更新了读下标,当执行 forEach 方法的时候(+ offset),就会出现下标越界!
具体的重现方法我已经提了 issue: issue#8256 地址
作者诺曼(苹果基础架构工程师)也进行了修改并提交了代码:
而这个 bug 的作者本来是想优化这个代码的,初始代码是没有这个 offset 优化的,结果写出来个 bug, = =!:
最后,最重要的一点是,这个 bug 是艿艿发现的!有关 Netty,Dubbo等一切 Java开源框架的疑问都可以找他!他的公众号:
总结,此文虽然没什么技术含量,但是给我提了一个醒,强如 netty ,也会有 bug,因此要具有批判精神,即使再权威,我们也要有怀疑精神,去研究,去反向推导,才能有真正的收获。