Netty原始碼解析8-ChannelHandler例項之CodecHandler

王知無發表於2019-02-22

請戳GitHub原文: github.com/wangzhiwubi…

更多文章關注:多執行緒/集合/分散式/Netty/NIO/RPC

編解碼處理器作為Netty程式設計時必備的ChannelHandler,每個應用都必不可少。Netty作為網路應用框架,在網路上的各個應用之間不斷進行資料互動。而網路資料交換的基本單位是位元組,所以需要將本應用的POJO物件編碼為位元組資料傳送到其他應用,或者將收到的其他應用的位元組資料解碼為本應用可使用的POJO物件。這一部分,又和JAVA中的序列化和反序列化對應。幸運的是,有很多其他的開源工具(protobuf,thrift,json,xml等等)可方便的處理POJO物件的序列化,可參見這個連結。 在網際網路中,Netty使用TCP/UDP協議傳輸資料。由於Netty基於非同步事件處理以及TCP的一些特性,使得TCP資料包會發生粘包現象。想象這樣的情況,客戶端與服務端建立連線後,連線傳送了兩條訊息:

+------+   +------+
| MSG1 |   | MSG2 |
+------+   +------+
複製程式碼

在網際網路上傳輸資料時,連續傳送的兩條訊息,在服務端極有可能被合併為一條:

 +------------+
| MSG1  MSG2 |
+------------+
複製程式碼

這還不是最壞的情況,由於路由器的拆包和重組,可能收到這樣的兩個資料包:

 +----+     +---------+         +-------+    +-----+ 
    | MS |     |  G1MSG2 |  或者  | MSG1M |    | SG2 | 
    +----+     +---------+        +-------+    +-----+
複製程式碼

而服務端要正確的識別出這樣的兩條訊息,就需要編碼器的正確工作。為了正確的識別出訊息,業界有以下幾種做法:

使用定界符分割訊息,一個特例是使用換行符分隔每條訊息。 使用定長的訊息。 在訊息的某些欄位指明訊息長度。

明白了這些,進入正題,分析Netty的編碼框架ByteToMessageDecoder。

ByteToMessageDecoder

在分析之前,需要說明一點:ByteToMessage容易引起誤解,解碼結果Message會被認為是JAVA物件POJO,但實際解碼結果是訊息幀。也就是說該解碼器處理TCP的粘包現象,將網路傳送的位元組流解碼為具有確定含義的訊息幀,之後的解碼器再將訊息幀解碼為實際的POJO物件。 明白了這點,再次回顧兩條訊息傳送的最壞情況,可知要正確取得兩條訊息,需要一個記憶體區域儲存訊息,當收到MS時繼續等待第二個包G1MSG2到達再進行解碼操作。在ByteToMessageDecoder中,這個記憶體區域被抽象為Cumulator,直譯累積器,可自動擴容累積位元組資料,Netty將其定義為一個介面:

    public interface Cumulator {
        ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in);
    }
複製程式碼

其中,兩個ByteBuf引數cumulation指已經累積的位元組資料,in表示該次channelRead()讀取到的新資料。返回ByteBuf為累積資料後的新累積區(必要時候自動擴容)。自動擴容的程式碼如下:

 static ByteBuf expandCumulation(ByteBufAllocator alloc, ByteBuf cumulation, 
                                       int newReadBytes) {
        ByteBuf oldCumulation = cumulation;
        // 擴容後新的緩衝區
        cumulation = alloc.buffer(oldCumulation.readableBytes() + readable);
        cumulation.writeBytes(oldCumulation);
        // 舊的緩衝區釋放
        oldCumulation.release();
        return cumulation;
    }
複製程式碼

自動擴容的方法簡單粗暴,直接使用大容量的Bytebuf替換舊的ByteBuf。Netty定義了兩個累積器,一個為MERGE_CUMULATOR:

public static final Cumulator MERGE_CUMULATOR = new Cumulator() {
        @Override
        public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
            ByteBuf buffer;
            // 1.累積區容量不夠容納資料
            // 2.使用者使用了slice().retain()或duplicate().retain()使refCnt增加
            if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes()
                    || cumulation.refCnt() > 1) {
                buffer = expandCumulation(alloc, cumulation, in.readableBytes());
            } else {
                buffer = cumulation;
            }
            buffer.writeBytes(in);
            in.release();
            return buffer;
        }
    };
複製程式碼

可知,兩種情況下會擴容:

  1. 累積區容量不夠容納新讀入的資料
  2. 使用者使用了slice().retain()或duplicate().retain()使refCnt增加並且大於1,此時擴容返回一個新的累積區ByteBuf,方便使用者對老的累積區ByteBuf進行後續處理。

另一個累積器為COMPOSITE_CUMULATOR:

public static final Cumulator COMPOSITE_CUMULATOR = new Cumulator() {
        @Override
        public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
            ByteBuf buffer;
            if (cumulation.refCnt() > 1) {
                buffer = expandCumulation(alloc, cumulation, in.readableBytes());
                buffer.writeBytes(in);
                in.release();
            } else {
                CompositeByteBuf composite;
                if (cumulation instanceof CompositeByteBuf) {
                    composite = (CompositeByteBuf) cumulation;
                } else {
                    composite = alloc.compositeBuffer(Integer.MAX_VALUE);
                    composite.addComponent(true, cumulation);
                }
                composite.addComponent(true, in);
                buffer = composite;
            }
            return buffer;
        }
    };
複製程式碼

這個累積器只在第二種情況refCnt>1時擴容,除此之外處理和MERGE_CUMULATOR一致,不同的是當cumulation不是CompositeByteBuf時會建立新的同類CompositeByteBuf,這樣最後返回的ByteBuf必定是CompositeByteBuf。使用這個累積器後,當容量不夠時並不會進行記憶體複製,只會講新讀入的in加到CompositeByteBuf中。需要注意的是:此種情況下雖然不需記憶體複製,卻要求使用者維護複雜的索引,在某些使用中可能慢於MERGE_CUMULATOR。故Netty預設使用MERGE_CUMULATOR累積器。 累積器分析完畢,步入正題ByteToMessageDecoder,首先看類簽名:

public abstract class ByteToMessageDecoder extends
                                ChannelInboundHandlerAdapter
複製程式碼

該類是一個抽象類,其中的抽象方法只有一個decode():

protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, 
List<Object> out) throws Exception;
複製程式碼

使用者使用了該解碼框架後,只需實現該方法就可定義自己的解碼器。引數in表示累積器已累積的資料,out表示本次可從累積資料解碼出的結果列表,結果可為POJO物件或者ByteBuf等等Object。 關注一下成員變數,以便更好的分析:

    ByteBuf cumulation; // 累積區
    private Cumulator cumulator = MERGE_CUMULATOR; // 累積器
    // 設定為true後每個channelRead事件只解碼出一個結果
    private boolean singleDecode;   // 某些特殊協議使用
    private boolean decodeWasNull;  // 解碼結果為空
    private boolean first;  // 是否首個訊息
    // 累積區不丟棄位元組的最大次數,16次後開始丟棄
    private int discardAfterReads = 16;
    private int numReads;   // 累積區不丟棄位元組的channelRead次數
複製程式碼

下面,直接進入channelRead()事件處理:

 public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 只對ByteBuf處理即只對位元組資料進行處理
        if (msg instanceof ByteBuf) {
            // 解碼結果列表
            CodecOutputList out = CodecOutputList.newInstance();
            try {
                ByteBuf data = (ByteBuf) msg;
                first = cumulation == null; // 累積區為空表示首次解碼
                if (first) {
                    // 首次解碼直接使用讀入的ByteBuf作為累積區
                    cumulation = data;
                } else {
                    // 非首次需要進行位元組資料累積
                    cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
                }
                callDecode(ctx, cumulation, out); // 解碼操作
            } catch (DecoderException e) {
                throw e;
            } catch (Throwable t) {
                throw new DecoderException(t);
            } finally {
                if (cumulation != null && !cumulation.isReadable()) {
                    // 此時累積區不再有位元組資料,已被處理完畢
                    numReads = 0;
                    cumulation.release();
                    cumulation = null;
                } else if (++ numReads >= discardAfterReads) {
                    // 連續discardAfterReads次後
                    // 累積區還有位元組資料,此時丟棄一部分資料
                    numReads = 0;
                    discardSomeReadBytes(); // 丟棄一些已讀位元組
                }

                int size = out.size();
                // 本次沒有解碼出資料,此時size=0
                decodeWasNull = !out.insertSinceRecycled();
                fireChannelRead(ctx, out, size); // 觸發事件
                out.recycle();  // 回收解碼結果
            }
        } else {
            ctx.fireChannelRead(msg);
        }
    }
複製程式碼

解碼結果列表CodecOutputList是Netty定製的一個特殊列表,該列表線上程中被快取,可迴圈使用來儲存解碼結果,減少不必要的列表例項建立,從而提升效能。由於解碼結果需要頻繁儲存,普通的ArrayList難以滿足該需求,故定製化了一個特殊列表,由此可見Netty對優化的極致追求。 注意finally塊的第一個if情況滿足時,即累積區的資料已被讀取完畢,請考慮釋放累積區的必要性。想象這樣的情況,當一條訊息被解碼完畢後,如果客戶端長時間不傳送訊息,那麼,服務端儲存該條訊息的累積區將一直佔據服務端記憶體浪費資源,所以必須釋放該累積區。 第二個if情況滿足時,即累積區的資料一直在channelRead讀取資料進行累積和解碼,直到達到了discardAfterReads次(預設16),此時累積區依然還有資料。在這樣的情況下,Netty主動丟棄一些位元組,這是為了防止該累積區佔用大量記憶體甚至耗盡記憶體引發OOM。 處理完這些情況後,最後統一觸發ChannelRead事件,將解碼出的資料傳遞給下一個處理器。注意:當out=0時,統一到一起被處理了。 再看細節的discardSomeReadBytes()和fireChannelRead():

 protected final void discardSomeReadBytes() {
        if (cumulation != null && !first && cumulation.refCnt() == 1) {
            cumulation.discardSomeReadBytes();
        }
    }
    
    static void fireChannelRead(ChannelHandlerContext ctx, CodecOutputList msgs, 
                        int numElements) {
        for (int i = 0; i < numElements; i ++) {
            ctx.fireChannelRead(msgs.getUnsafe(i));
        }
    }
複製程式碼

程式碼比較簡單,只需注意discardSomeReadBytes中,累積區的refCnt() == 1時才丟棄資料是因為:如果使用者使用了slice().retain()和duplicate().retain()使refCnt>1,表明該累積區還在被使用者使用,丟棄資料可能導致使用者的困惑,所以須確定使用者不再使用該累積區的已讀資料,此時才丟棄。 下面分析解碼核心方法callDecode():

 protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        try {
            while (in.isReadable()) {
                int outSize = out.size();

                if (outSize > 0) {
                    // 解碼出訊息就立即處理,防止訊息等待
                    fireChannelRead(ctx, out, outSize);
                    out.clear();
                    
                    // 使用者主動刪除該Handler,繼續操作in是不安全的
                    if (ctx.isRemoved()) {
                        break;
                    }
                    outSize = 0;
                }

                int oldInputLength = in.readableBytes();
                decode(ctx, in, out);   // 子類需要實現的具體解碼步驟

                // 使用者主動刪除該Handler,繼續操作in是不安全的
                if (ctx.isRemoved()) {
                    break; 
                }
                
                // 此時outSize都==0(這的程式碼容易產生誤解 應該直接使用0)
                if (outSize == out.size()) {
                    if (oldInputLength == in.readableBytes()) {
                        // 沒有解碼出訊息,且沒讀取任何in資料
                        break;
                    } else {
                        // 讀取了一部份資料但沒有解碼出訊息
                        // 說明需要更多的資料,故繼續
                        continue;
                    }
                }

                // 執行到這裡outSize>0 說明已經解碼出訊息
                if (oldInputLength == in.readableBytes()) {
                    // 解碼出訊息但是in的讀索引不變,使用者的decode方法有Bug
                    throw new DecoderException(
                            "did not read anything but decoded a message.");
                }
                
                // 使用者設定一個channelRead事件只解碼一次
                if (isSingleDecode()) {
                    break; 
                }
            }
        } catch (DecoderException e) {
            throw e;
        } catch (Throwable cause) {
            throw new DecoderException(cause);
        }
    }
複製程式碼

迴圈中的第一個if分支,檢查解碼結果,如果已經解碼出訊息則立即將訊息傳播到下一個處理器進行處理,這樣可使訊息得到及時處理。在呼叫decode()方法的前後,都檢查該Handler是否被使用者從ChannelPipeline中刪除,如果刪除則跳出解碼步驟不對輸入緩衝區in進行操作,因為繼續操作in已經不安全。解碼完成後,對in解碼前後的讀索引進行了檢查,防止使用者的錯誤使用,如果使用者錯誤使用將丟擲異常。 至此,核心的解碼框架已經分析完畢,再看最後的一些邊角處理。首先是channelReadComplete()讀事件完成後的處理:

public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        numReads = 0;   // 連續讀次數置0
        discardSomeReadBytes(); // 丟棄已讀資料,節約記憶體
        if (decodeWasNull) {
            // 沒有解碼出結果,則期待更多資料讀入
            decodeWasNull = false;
            if (!ctx.channel().config().isAutoRead()) {
                ctx.read();
            }
        }
        ctx.fireChannelReadComplete();
    }

複製程式碼

如果channelRead()中沒有解碼出訊息,極有可能是資料不夠,由此呼叫ctx.read()期待讀入更多的資料。如果設定了自動讀取,將會在HeadHandler中呼叫ctx.read();沒有設定自動讀取,則需要此處顯式呼叫。 最後再看Handler從ChannelPipelien中移除的處理handlerRemoved():

 public final void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        ByteBuf buf = cumulation;
        if (buf != null) {
            cumulation = null;  // 釋放累積區,GC回收

            int readable = buf.readableBytes();
            if (readable > 0) {
                ByteBuf bytes = buf.readBytes(readable);
                buf.release();
                // 解碼器已被刪除故不再解碼,只將資料傳播到下一個Handler
                ctx.fireChannelRead(bytes);
            } else {
                buf.release();
            }

            numReads = 0;   // 置0,有可能被再次新增
            ctx.fireChannelReadComplete();
        }
        handlerRemoved0(ctx);   // 使用者可進行的自定義處理
    }
複製程式碼

當解碼器被刪除時,如果還有沒被解碼的資料,則將資料傳播到下一個處理器處理,防止丟失資料。此外,當連線不再有效觸發channelInactive事件或者觸發ChannelInputShutdownEvent時,則會呼叫callDecode()解碼,如果解碼出訊息,傳播到下一個處理器。這部分的程式碼不再列出。 至此,ByteToMessageDecoder解碼框架已分析完畢,下面,我們選用具體的例項進行分析。

LineBasedFrameDecoder

基於行分隔的解碼器LineBasedFrameDecoder是一個特殊的分隔符解碼器,該解碼器使用的分隔符為:windows的\r\n和類linux的\n。 首先看該類定義的成員變數:

    // 最大幀長度,超過此長度將丟擲異常TooLongFrameException
    private final int maxLength;
    // 是否快速失敗,true-檢測到幀長度過長立即丟擲異常不在讀取整個幀
    // false-檢測到幀長度過長依然讀完整個幀再丟擲異常
    private final boolean failFast;
    // 是否略過分隔符,true-解碼結果不含分隔符
    private final boolean stripDelimiter;

    // 超過最大幀長度是否丟棄位元組
    private boolean discarding;
    private int discardedBytes; // 丟棄的位元組數
複製程式碼

其中,前三個變數可由使用者根據實際情況配置,後兩個變數解碼時使用。 該子類覆蓋的解碼方法如下:

protected final void decode(ChannelHandlerContext ctx, ByteBuf in, 
                   List<Object> out) throws Exception {
        Object decoded = decode(ctx, in);
        if (decoded != null) {
            out.add(decoded);
        }
    }
複製程式碼

其中又定義了decode(ctx, in)解碼出單個訊息幀,事實上這也是其他編碼子類使用的方法。decode(ctx, in)方法處理很繞彎,只給出虛擬碼:

protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
        final int eol = findEndOfLine(buffer);
        if (!discarding) {
            if (eol >= 0) {
                // 此時已找到換行符
                if(!checkMaxLength()) {
                    return getFrame().retain();
                } 
                // 超過最大長度丟擲異常
            } else {
                if (checkMaxLength()) {
                    // 設定true表示下一次解碼需要丟棄位元組
                    discarding = true;  
                    if (failFast) {
                        // 丟擲異常
                    }
                } 
            }
        } else {
            if (eol >= 0) {
                // 丟棄換行符以及之前的位元組
                buffer.readerIndex(eol + delimLength);
            } else {
                // 丟棄收到的所有位元組
                buffer.readerIndex(buffer.writerIndex());
            }
        }
    }
複製程式碼

該方法需要結合解碼框架的while迴圈反覆理解,每個if情況都是一次while迴圈,而變數discarding就成為控制每次解碼流程的狀態量,注意其中的狀態轉移。(想法:使用狀態機實現,則流程更清晰)

DelimiterBasedFrameDecoder

該解碼器是更通用的分隔符解碼器,可支援多個分隔符,每個分隔符可為一個或多個字元。如果定義了多個分隔符,並且可解碼出多個訊息幀,則選擇產生最小幀長的結果。例如,使用行分隔符\r\n和\n分隔:

    +--------------+
    | ABC\nDEF\r\n |
    +--------------+
複製程式碼

可有兩種結果:

+-----+-----+              +----------+   
| ABC | DEF |  (√)   和    | ABC\nDEF |  (×)
+-----+-----+              +----------+
複製程式碼

該編碼器可配置的變數與LineBasedFrameDecoder類似,只是多了一個ByteBuf[] delimiters用於配置具體的分隔符。 Netty在Delimiters類中定義了兩種預設的分隔符,分別是NULL分隔符和行分隔符:

  public static ByteBuf[] nulDelimiter() {
        return new ByteBuf[] {
                Unpooled.wrappedBuffer(new byte[] { 0 }) };
    }
    
    public static ByteBuf[] lineDelimiter() {
        return new ByteBuf[] {
                Unpooled.wrappedBuffer(new byte[] { '\r', '\n' }),
                Unpooled.wrappedBuffer(new byte[] { '\n' }),
        };
    }
複製程式碼

FixedLengthFrameDecoder

該解碼器十分簡單,按照固定長度frameLength解碼出訊息幀。如下的資料幀解碼為固定長度3的訊息幀示例如下:

+---+----+------+----+      +-----+-----+-----+
| A | BC | DEFG | HI |  ->  | ABC | DEF | GHI |
+---+----+------+----+      +-----+-----+-----+
複製程式碼

其中的解碼方法也十分簡單:

 protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        if (in.readableBytes() < frameLength) {
            return null;
        } else {
            return in.readSlice(frameLength).retain();
        }
    }
複製程式碼

LengthFieldBasedFrameDecoder

基於長度欄位的訊息幀解碼器,該解碼器可根據資料包中的長度欄位動態的解碼出訊息幀。一個推薦的二進位制傳輸協議可設計為如下格式:

+----------+------+----------+------+
|  頭部長度 |  頭部 |  資料長度 | 資料 |
+----------+------+----------+------+
複製程式碼

這樣的協議可滿足大多數場景使用,但不幸的是:很多情況下並不可以設計新的協議,往往要在老舊的協議上傳輸資料。由此,Netty將該解碼器設計的十分通用,只要有類似的長度欄位便能正確解碼出訊息幀。當然前提是:正確使用解碼器。 沒有什麼是完美的,由於該解碼器十分通用,所以有大量的配置變數:

    private final ByteOrder byteOrder;
    private final int maxFrameLength;
    private final boolean failFast;
    private final int lengthFieldOffset;
    private final int lengthFieldLength;
    private final int lengthAdjustment;
    private final int initialBytesToStrip;
複製程式碼

變數byteOrder表示長度欄位的位元組序:大端或小端,預設為大端。如果對位元組序有疑問,請查閱其他資料,不再贅述。maxFrameLength和failFast與其他解碼器相同,控制最大幀長度和快速失敗拋異常,注意:該解碼器failFast預設為true。 接下來將重點介紹其它四個變數:

  • lengthFieldOffset表示長度欄位偏移量即在一個資料包中長度欄位的具體下標位置。標準情況,該長度欄位為資料部分長度。

  • lengthFieldLength表示長度欄位的具體位元組數,如一個int佔4位元組。該解碼器支援的位元組數有:1,2,3,4和8,其他則會丟擲異常。另外,還需要注意的是:長度欄位的結果為無符號數。

  • lengthAdjustment是一個長度調節量,當資料包的長度欄位不是資料部分長度而是總長度時,可將此值設定為頭部長度,便能正確解碼出包含整個資料包的結果訊息幀。注意:某些情況下,該值可設定為負數。

  • initialBytesToStrip表示需要略過的位元組數,如果我們只關心資料部分而不關心頭部,可將此值設定為頭部長度從而丟棄頭部。 下面我們使用具體的例子來說明:

  • 需求1:如下待解碼資料包,正確解碼為訊息幀,其中長度欄位在最前面的2位元組,資料部分為12位元組的字串"HELLO, WORLD",長度欄位0x000C=12 表示資料部分長度,資料包總長度則為14位元組。

       解碼前(14 bytes)                 解碼後(14 bytes)
       +--------+----------------+      +--------+----------------+
       | Length | Actual Content |----->| Length | Actual Content |
       | 0x000C | "HELLO, WORLD" |      | 0x000C | "HELLO, WORLD" |
       +--------+----------------+      +--------+----------------+
    複製程式碼

正確配置(只列出四個值中不為0的值):

 lengthFieldLength = 2;
複製程式碼
  • 需求2:需求1的資料包不變,訊息幀中去除長度欄位。

      解碼前(14 bytes)                 解碼後(12 bytes)
      +--------+----------------+      +----------------+
      | Length | Actual Content |----->| Actual Content |
      | 0x000C | "HELLO, WORLD" |      | "HELLO, WORLD" |
      +--------+----------------+      +----------------+
    複製程式碼

正確配置:

 lengthFieldLength   = 2;
    initialBytesToStrip = 2;
複製程式碼

需求3:需求1資料包中長度欄位表示資料包總長度。

解碼前(14 bytes)                 解碼後(14 bytes)
    +--------+----------------+      +--------+----------------+
    | Length | Actual Content |----->| Length | Actual Content |
    | 0x000E | "HELLO, WORLD" |      | 0x000E | "HELLO, WORLD" |
    +--------+----------------+      +--------+----------------+
複製程式碼

正確配置:

    lengthFieldLength =  2;
    lengthAdjustment  = -2;  // 調整長度欄位的2位元組
複製程式碼

需求4:綜合難度,資料包有兩個頭部HDR1和HDR2,長度欄位以及資料部分組成,其中長度欄位值表示資料包總長度。結果訊息幀需要第二個頭部HDR2和資料部分。請先給出答案再與標準答案比較,結果正確說明你已完全掌握了該解碼器的使用。

解碼前 (16 bytes)                               解碼後 (13 bytes)
+------+--------+------+----------------+      +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+      +------+----------------+
複製程式碼

正確配置:

 lengthFieldOffset   =  1;
    lengthFieldLength   =  2;
    lengthAdjustment    = -3;
    initialBytesToStrip =  3;
複製程式碼

本解碼器的解碼過程總體上較為複雜,由於解碼的程式碼是在while迴圈裡面,decode方法return或者丟擲異常時可看做一次迴圈結束,直到in中資料被解析完或者in的readerIndex讀索引不再增加才會從while迴圈跳出。使用狀態的思路理解,每個return或者丟擲異常看為一個狀態:

狀態1:丟棄過長幀狀態,可能是使用者設定了錯誤的幀長度或者實際幀過長。

 if (discardingTooLongFrame) {
        long bytesToDiscard = this.bytesToDiscard;
        int localBytesToDiscard = (int) Math.min(bytesToDiscard, in.readableBytes());
        in.skipBytes(localBytesToDiscard); // 丟棄實際的位元組數
        
        bytesToDiscard -= localBytesToDiscard;
        this.bytesToDiscard = bytesToDiscard;
        failIfNecessary(false);
    }
複製程式碼

變數localBytesToDiscard取得實際需要丟棄的位元組數,由於過長幀有兩種情況:a.使用者設定了錯誤的長度欄位,此時in中並沒有如此多的位元組;b.in中確實有如此長度的幀,這個幀確實超過了設定的最大長度。bytesToDiscard的計算是為了failIfNecessary()確定異常的丟擲,其值為0表示當次丟棄狀態已經丟棄了in中的所有資料,可以對新讀入in的資料進行處理;否則,還處於異常狀態。

private void failIfNecessary(boolean firstDetectionOfTooLongFrame) {
        if (bytesToDiscard == 0) {
            long tooLongFrameLength = this.tooLongFrameLength;
            this.tooLongFrameLength = 0;
            // 由於已經丟棄所有資料,關閉丟棄模式
            discardingTooLongFrame = false;
            // 已經丟棄了所有位元組,當非快速失敗模式拋異常
            if (!failFast || firstDetectionOfTooLongFrame) {
                fail(tooLongFrameLength);
            }
        } else {
            if (failFast && firstDetectionOfTooLongFrame) {
                // 幀長度異常,快速失敗模式檢測到即拋異常
                fail(tooLongFrameLength);
            }
        }
    }

複製程式碼

可見,首次檢測到幀長度是一種特殊情況,在之後的一個狀態進行分析。請注意該狀態並不是都拋異常,還有可能進入狀態2。

狀態2:in中資料不足夠組成訊息幀,此時直接返回null等待更多資料到達。

    if (in.readableBytes() < lengthFieldEndOffset) {
        return null;
    }
複製程式碼

狀態3:幀長度錯誤檢測,檢測長度欄位為負值得幀以及加入調整長度後總長小於長度欄位的幀,均丟擲異常。

 int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;
    // 該方法取出長度欄位的值,不再深入分析
    long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, 
                             lengthFieldLength, byteOrder);
    if (frameLength < 0) {
        in.skipBytes(lengthFieldEndOffset);
        throw new CorruptedFrameException("...");
    }

    frameLength += lengthAdjustment + lengthFieldEndOffset;
    if (frameLength < lengthFieldEndOffset) {
        in.skipBytes(lengthFieldEndOffset);
        throw new CorruptedFrameException("...");
複製程式碼

狀態4:幀過長,由前述可知:可能是使用者設定了錯誤的幀長度或者實際幀過長

    if (frameLength > maxFrameLength) {
            long discard = frameLength - in.readableBytes();
            tooLongFrameLength = frameLength;

            if (discard < 0) {
                in.skipBytes((int) frameLength);
            } else {
                discardingTooLongFrame = true;
                bytesToDiscard = discard;
                in.skipBytes(in.readableBytes());
            }
            failIfNecessary(true);
            return null;
        }
複製程式碼

變數discard<0表示當前收到的資料足以確定是實際的幀過長,所以直接丟棄過長的幀長度;>0表示當前in中的資料並不足以確定是使用者設定了錯誤的幀長度,還是正確幀的後續資料位元組還沒有到達,但無論何種情況,將丟棄狀態discardingTooLongFrame標記設定為true,之後後續資料位元組進入狀態1處理。==0時,在failIfNecessary(true)無論如何都將丟擲異常,><0時,只有設定快速失敗才會丟擲異常。還需注意一點:failIfNecessary()的引數firstDetectionOfTooLongFrame的首次是指正確解析資料後發生的第一次發生的幀過長,可知會有很多首次。

狀態5:正確解碼出訊息幀。

 int frameLengthInt = (int) frameLength;
    if (in.readableBytes() < frameLengthInt) {
        return null;    // 到達的資料還達不到幀長
    }

    if (initialBytesToStrip > frameLengthInt) {
        in.skipBytes(frameLengthInt);   // 跳過位元組數錯誤
        throw new CorruptedFrameException("...");
    }
    in.skipBytes(initialBytesToStrip);

    // 正確解碼出資料幀
    int readerIndex = in.readerIndex();
    int actualFrameLength = frameLengthInt - initialBytesToStrip;
    ByteBuf frame = in.slice(readerIndex, actualFrameLength).retain();
    in.readerIndex(readerIndex + actualFrameLength);
    return frame;
複製程式碼

程式碼中混合了兩個簡單狀態,到達的資料還達不到幀長和使用者設定的忽略位元組數錯誤。由於較為簡單,故合併到一起。 至此解碼框架分析完畢。可見,要正確的寫出基於長度欄位的解碼器還是較為複雜的,如果開發時確有需求,特別要注意狀態的轉移。下面介紹較為簡單的編碼框架。

Netty原始碼解析8-ChannelHandler例項之CodecHandler

	請戳GitHub原文: https://github.com/wangzhiwubigdata/God-Of-BigData

                   關注公眾號,內推,面試,資源下載,關注更多大資料技術~
                   大資料成神之路~預計更新500+篇文章,已經更新60+篇~ 複製程式碼

相關文章