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