前言
用netty做数据校验的时候,很自然的想法是写一个decoder,比如XXXXChecksumDecoder,如果校验出错,就丢弃这个数据包,一般来说,这种单纯的做数据校验的decoder,不会读走数据,就是说,传入的bytebuf大小如果是10,传出的bytebuf大小也应该是10,decoder只是做了一次数据校验,这个时候,经常遇到的问题是netty报错:did not read anything but decoded a message,就是提示使用者,必须读走一些字节。
问题解决方法
一般来说,很自然的想法是把decoder写成这样:
public class XXXXChecksumDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int checkSumRemote = in.readUnsignedShortLE(Header.CHECKSUM_POS);
in.setShortLE(Header.CHECKSUM_POS, 0);
int checkSumLocal = ChecksumUtil.calculateChecksumValue(in);
in.setShortLE(Header.CHECKSUM_POS, checkSumRemote);
if(checkSumLocal != checkSumRemote)
{
in.skipBytes(in.readableBytes());
System.out.println("CheckSum Error");
}else {
out.add(in);
}
}
}
这样会抛出异常XXXXChecksumDecoder.decode()did not read anything but decoded a message
既然netty提示说必须要读走一些byte,那么这样行不行呢?
public class XXXXChecksumDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int checkSumRemote = in.readUnsignedShortLE(Header.CHECKSUM_POS);
in.setShortLE(Header.CHECKSUM_POS, 0);
int checkSumLocal = ChecksumUtil.calculateChecksumValue(in);
in.setShortLE(Header.CHECKSUM_POS, checkSumRemote);
if(checkSumLocal != checkSumRemote)
{
in.skipBytes(in.readableBytes());
System.out.println("CheckSum Error");
}else {
in.skipBytes(in.readableBytes());//这里跳过所有可读字节
out.add(in);
}
}
}
但是如果这样,上层decoder收到的bytebuf,就已经是被全部读过的了,那要怎么解决呢?正确的做法是这样:
public class XXXXChecksumDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int checkSumRemote = in.readUnsignedShortLE(Header.CHECKSUM_POS);
in.setShortLE(Header.CHECKSUM_POS, 0);
int checkSumLocal = ChecksumUtil.calculateChecksumValue(in);
in.setShortLE(Header.CHECKSUM_POS, checkSumRemote);
if(checkSumLocal != checkSumRemote)
{
System.out.println("CheckSum Error");
}else {
Bytebuf frame = in.retainedDuplicate();
out.add(frame);
}
in.skipBytes(in.readableBytes());
}
}
将in的副本返回给上层decoder
,并且跳过所有in的可读字节。因为retainedDuplicate()
只是将in的引用数加1并且复制其readerIndex
、writerIndex
等,并没有真的复制缓冲区,所以这样几乎不消耗额外性能。之后就安全地in.skipBytes(in.readableBytes())
读走所有字节。
问题的原因
为什么会出现这样的问题呢?源码中是这样的:
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
try {
while (in.isReadable()) {
int outSize = out.size();
int oldInputLength = in.readableBytes();
decode(ctx, in, out);
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 (Throwable cause) {
throw new DecoderException(cause);
}
}
源码中,如果List<Object> out
不增长的话,是不会抛出这个异常的,比如定长数据包解析中decoder一开始检查到可读数据没有达到数据包的大小,直接return,这时候是不会报异常的,只有decoder在out中增加了对象,就是说decoder产生了数据,但是却没有读in时,才会有这个错误,为什么要这样呢?Netty的作者给出了答案:
if you produce a message you need to also read something from the ByteBuf. This check was added to catch endless loops generated by user decoder bugs.
这是用来防止由decoder引起的无限循环的机制,这么想,如果每次decoder都生成一个新对象,但是in的readerIndex却不增长,这样再次调用decoder时,传入的in的readerIndex还是一样的,这时候decoder又会生成一个新对象,虽然不是一定的,但是这样容易引起无限循环,所以netty用异常来警告使用者,每次都必须从in里读出一些字节,如果不想读,像上面的checksum例子,那就必须复制一个in,然后把原来的in的数据读掉。