Netty詭異報錯did not read anything but decoded a message

前言

用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的數據讀掉。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章