这个buffer应该是Mina继续接收外部系统的数据到buffer中导致,
Mina框架不停的接收数据,直到buffer区满,然后整个框架不停的解析出前5帧,到第6帧的时候,出错,然后dump出其尚未被解帧的数据。这就是第二段日志。
最后的高潮
到现在推理似乎很完美了,但是我突然觉得不对(另一位同事也提出了相同的疑问):
如果说Mina接收到新的数据放到buffer中的话,第6帧的前两个字节和后来发过来的若干字节不是又拼成了完整的一帧了么,那么后来为什么会一直出错了呢。如下图所示:
丢失的两字节
按照前面的推理,帧6的前两个字节30、32肯定是丢了,那么怎么丢的呢?推理又陷入了困境,怎么办?日志已经帮不了笔者了,毕竟日志的表现都已解释清楚。翻源码吧:
Bug的源头:
如果有问题,肯定出在将数据放在Buffer中的环节,于是笔者找到了这段代码:
if (appended) {
buf.flip();
} else {
// Reallocate the buffer if append operation failed due to
// derivation or disabled auto-expansion.
buf.flip();
......
}
问题出在buf.flip()上面,这段代码最后调用的代码是Java的Nio的Buffer的flip,代码如下:
public final Buffer flip() {
// 下面这一句导致了最终的Bug现象
limit = position;
position = 0;
mark = -1;
return this;
}
为什么呢?首先我们需要了解一下Nio Buffer的一些特点:
同时当Mina框架将数据(数据本身也是一个buffer)放到sessionBuffer的时候,也是将position到limit的数据放到新buffer中,
下面我们演绎一下第一次抛异常时候的flip前和flip后:
这样就清楚了,在buf.flip()后,由于limit变成了原position的位置,这样最后的两个字节30,32就被无情的丢弃了。这样整个sessionBuffer就变成:
为什么position在flip前没有指向limit的位置,是由于在每次读取前有一个checkBound的动作,在检查buffer数据不够后,不会推进position的位置,直接抛出异常:
static void checkBounds(int off, int len, int size) { // package-private
if ((off | len | (off + len) | (size - (off + len))) < 0)
throw new IndexOutOfBoundsException();
}
这样所有的都说的通了,也完美了解释了所有的现象。
正确代码
private boolean handeMessage(IoBuffer in,ProtocolDecoderOutput out){ int lenDes = 4; byte[] data = new byte[lenDes]; in.mark(); // 前4字节校验代码 if(in.remaining() < lenDes){ // 由于未消费字节,无需reset return false; } in.get(data,0,lenDes); int messageLen = decodeLength(data); if(in.remaining() < messageLen){ logger.warn("未接收完毕"); in.reset(); return false; }else{ ...... } } |
为什么线上一直稳定
随着网络不断发展的今天,一些短小的帧很难出现中间断开的粘包现象。而在一个好几百字节的包中,前4个字节正好出错的概率那更是微乎其微。这样就导致Bug难复现,很难抓住。即使猜到是这里,也没有足够的证据来证明。
总结
Mina/Netty等各种网络框架给我们解决粘包问题提供了非常好的解决方案。但是我们写代码的时候也不能掉以轻心,必须时刻以当前可能读不够字节的心态去读取buffer中的数据,不然就可能遭重。
在此感谢给力的各位同事们,是你们的各种反驳让我能够找到最终的源头,也让我对网络框架有了更加深刻的理解。