簡介
netty提供了一個從ByteBuf到使用者自定義的message的解碼器叫做ByteToMessageDecoder,要使用這個decoder,我們需要繼承這個decoder,並實現decode方法,從而在這個方法中實現ByteBuf中的內容到使用者自定義message物件的轉換。
那麼在使用ByteToMessageDecoder的過程中會遇到什麼問題呢?為什麼又會有一個ReplayingDecoder呢?帶著這個問題我們一起來看看吧。
ByteToMessageDecoder可能遇到的問題
要想實現自己的解碼器將ByteBuf轉換成為自己的訊息物件,可以繼承ByteToMessageDecoder,然後實現其中的decode方法即可,先來看下decode方法的定義:
protected void decode(ChannelHandlerContext ctx,
ByteBuf buf, List<Object> out) throws Exception
輸入的引數中buf是要解碼的ByteBuf,out是解碼過後的物件列表,我們需要把ByteBuf中的資料轉換成為我們自己的物件加入out的list中。
那麼這裡可能會遇到一個問題,因為我們在呼叫decode方法的時候buf中的資料可能還沒有準備好,比如我們需要一個Integer,但是buf中的資料不夠一個整數,那麼就需要一些buf中資料邏輯的判斷,我們以一個帶有訊息長度的Buf物件來描述一下這個過程。
所謂帶有訊息長度的Buf物件,就是說Buf訊息中的前4位,構成了一個整數,這個整數表示的是buf中後續訊息的長度。
所以我們讀取訊息進行轉換的流程是,先讀取前面4個位元組,得到訊息的長度,然後再讀取該長度的位元組,這就是我們真正要獲取的訊息內容。
來看一下如果是繼承自ByteToMessageDecoder應該怎麼實現這個邏輯呢?
public class IntegerHeaderFrameDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx,
ByteBuf buf, List<Object> out) throws Exception {
if (buf.readableBytes() < 4) {
return;
}
buf.markReaderIndex();
int length = buf.readInt();
if (buf.readableBytes() < length) {
buf.resetReaderIndex();
return;
}
out.add(buf.readBytes(length));
}
}
在decode中,我們首先需要判斷buf中可讀的位元組有沒有4個,沒有的話直接返回。如果有,則先讀取這4個位元組的長度,然後再判斷buf中的可讀位元組是否小於應該讀取的長度,如果小於,則說明資料還沒有準備好,需要呼叫resetReaderIndex進行重置。
最後,如果所有的條件都滿足,才真正進行讀取工作。
有沒有一個辦法可以不提前進行判斷,可以直接按照自己想要的內容來讀取buf的方式呢?答案就是ReplayingDecoder。
我們先來看一下上面的例子用ReplayingDecoder重寫是什麼情況:
public class IntegerHeaderFrameDecoder
extends ReplayingDecoder<Void> {
protected void decode(ChannelHandlerContext ctx,
ByteBuf buf, List<Object> out) throws Exception {
out.add(buf.readBytes(buf.readInt()));
}
}
使用ReplayingDecoder,我們可以忽略buf是否已經接收到了足夠的可讀資料,直接讀取即可。
相比之下ReplayingDecoder非常的簡單。接下來,我們來探究一下ReplayingDecoder的實現原理。
ReplayingDecoder的實現原理
ReplayingDecoder實際上是ByteToMessageDecoder的一個子類,它的定義如下:
public abstract class ReplayingDecoder<S> extends ByteToMessageDecoder
在ByteToMessageDecoder中,最重要的方法是channelRead,在這個方法中實際呼叫了callDecode(ctx, cumulation, out);
來實現cumulation到out的解碼過程。
ReplayingDecoder的祕密就在於對這個方法的重寫,我們來看下這個方法的具體實現:
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
replayable.setCumulation(in);
try {
while (in.isReadable()) {
int oldReaderIndex = checkpoint = in.readerIndex();
int outSize = out.size();
if (outSize > 0) {
fireChannelRead(ctx, out, outSize);
out.clear();
if (ctx.isRemoved()) {
break;
}
outSize = 0;
}
S oldState = state;
int oldInputLength = in.readableBytes();
try {
decodeRemovalReentryProtection(ctx, replayable, out);
if (ctx.isRemoved()) {
break;
}
if (outSize == out.size()) {
if (oldInputLength == in.readableBytes() && oldState == state) {
throw new DecoderException(
StringUtil.simpleClassName(getClass()) + ".decode() must consume the inbound " +
"data or change its state if it did not decode anything.");
} else {
continue;
}
}
} catch (Signal replay) {
replay.expect(REPLAY);
if (ctx.isRemoved()) {
break;
}
// Return to the checkpoint (or oldPosition) and retry.
int checkpoint = this.checkpoint;
if (checkpoint >= 0) {
in.readerIndex(checkpoint);
} else {
}
break;
}
if (oldReaderIndex == in.readerIndex() && oldState == state) {
throw new DecoderException(
StringUtil.simpleClassName(getClass()) + ".decode() method must consume the inbound data " +
"or change its state if it decoded something.");
}
if (isSingleDecode()) {
break;
}
}
} catch (DecoderException e) {
throw e;
} catch (Exception cause) {
throw new DecoderException(cause);
}
}
這裡的實現和ByteToMessageDecoder不同的是ReplayingDecoder中定義了一個checkpoint,這個checkpint是在嘗試進行資料解碼之初設定的:
int oldReaderIndex = checkpoint = in.readerIndex();
如果是在解碼的過程中出現了異常,則使用checkpoint重置index:
int checkpoint = this.checkpoint;
if (checkpoint >= 0) {
in.readerIndex(checkpoint);
} else {
}
這裡捕獲的異常是Signal,Signal是什麼呢?
Signal是一個Error物件:
public final class Signal extends Error implements Constant<Signal>
這個異常是從replayable中丟擲來的。
replayable是一個特有的ByteBuf物件,叫做ReplayingDecoderByteBuf:
final class ReplayingDecoderByteBuf extends ByteBuf
在ReplayingDecoderByteBuf中定義了Signal屬性:
private static final Signal REPLAY = ReplayingDecoder.REPLAY;
這個Signal異常是從ReplayingDecoderByteBuf中的get方法中丟擲的,這裡以getInt為例,看一下異常是如何丟擲的:
public int getInt(int index) {
checkIndex(index, 4);
return buffer.getInt(index);
}
getInt方法首先會去呼叫checkIndex方法進行buff中的長度檢測,如果小於要讀取的長度,則會丟擲異常REPLAY:
private void checkIndex(int index, int length) {
if (index + length > buffer.writerIndex()) {
throw REPLAY;
}
}
這就是Signal異常的由來。
總結
以上就是對ReplayingDecoder的介紹,雖然ReplayingDecoder好用,但是從它的實現可以看出,ReplayingDecoder是通過丟擲異常來不斷的重試,所以在某些特殊的情況下會造成效能的下降。
也就是說在減少我們程式碼量的同時,降低了程式的執行效率。看來要想馬兒跑又想馬兒不吃草,這樣的好事是不可能的了。
本文已收錄於 http://www.flydean.com/14-4-netty-replayingdecoder/
最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!