netty系列之:netty中的自動解碼器ReplayingDecoder

flydean發表於2022-04-27

簡介

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/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章