Netty报错did not read anything but decoded a message

半兽人 发表于: 2018-04-25   最后更新时间: 2018-04-25 20:19:34  
{{totalSubscript}} 订阅, 6,024 游览

前言

用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并且复制其readerIndexwriterIndex等,并没有真的复制缓冲区,所以这样几乎不消耗额外性能。之后就安全地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的数据读掉。

更新于 2018-04-25

查看netty更多相关的文章或提一个关于netty的问题,也可以与我们一起分享文章