netty系列之:netty中的frame解碼器

flydean發表於2022-04-28

簡介

netty中的資料是通過ByteBuf來進行傳輸的,一個ByteBuf中可能包含多個有意義的資料,這些資料可以被稱作frame,也就是說一個ByteBuf中可以包含多個Frame。

對於訊息的接收方來說,接收到了ByteBuf,還需要從ByteBuf中解析出有用而資料,那就需要將ByteBuf中的frame進行拆分和解析。

一般來說不同的frame之間會有有些特定的分隔符,我們可以通過這些分隔符來區分frame,從而實現對資料的解析。

netty為我們提供了一些合適的frame解碼器,通過使用這些frame解碼器可以有效的簡化我們的工作。下圖是netty中常見的幾個frame解碼器:

netty系列之:netty中的frame解碼器

接下來我們來詳細介紹一下上面幾個frame解碼器的使用。

LineBasedFrameDecoder

LineBasedFrameDecoder從名字上看就是按行來進行frame的區分。根據作業系統的不同,換行可以有兩種換行符,分別是 "\n" 和 "\r\n" 。

LineBasedFrameDecoder的基本原理就是從ByteBuf中讀取對應的字元來和"\n" 跟 "\r\n",可以了可以準確的進行字元的比較,這些frameDecoder對字元的編碼也會有一定的要求,一般來說是需要UTF-8編碼。因為在這樣的編碼中,"\n"和"\r"是以一個byte出現的,並且不會用在其他的組合編碼中,所以用"\n"和"\r"來進行判斷是非常安全的。

LineBasedFrameDecoder中有幾個比較重要的屬性,一個是maxLength的屬性,用來檢測接收到的訊息長度,如果超出了長度限制,則會丟擲TooLongFrameException異常。

還有一個stripDelimiter屬性,用來判斷是否需要將delimiter過濾掉。

還有一個是failFast,如果該值為true,那麼不管frame是否讀取完成,只要frame的長度超出了maxFrameLength,就會丟擲TooLongFrameException。如果該值為false,那麼TooLongFrameException會在整個frame完全讀取之後再丟擲。

LineBasedFrameDecoder的核心邏輯是先找到行的分隔符的位置,然後根據這個位置讀取到對應的frame資訊,這裡來看一下找到行分隔符的findEndOfLine方法:

    private int findEndOfLine(final ByteBuf buffer) {
        int totalLength = buffer.readableBytes();
        int i = buffer.forEachByte(buffer.readerIndex() + offset, totalLength - offset, ByteProcessor.FIND_LF);
        if (i >= 0) {
            offset = 0;
            if (i > 0 && buffer.getByte(i - 1) == '\r') {
                i--;
            }
        } else {
            offset = totalLength;
        }
        return i;
    }

這裡使用了一個ByteBuf的forEachByte對ByteBuf進行遍歷。我們要找的字元是:ByteProcessor.FIND_LF。

最後LineBasedFrameDecoder解碼之後的物件還是一個ByteBuf。

DelimiterBasedFrameDecoder

上面講的LineBasedFrameDecoder只對行分隔符有效,如果我們的frame是以其他的分隔符來分割的話LineBasedFrameDecoder就用不了了,所以netty提供了一個更加通用的DelimiterBasedFrameDecoder,這個frameDecoder可以自定義delimiter:

public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder {

        public DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf delimiter) {
        this(maxFrameLength, true, delimiter);
    }

傳入的delimiter是一個ByteBuf,所以delimiter可能不止一個字元。

為了解決這個問題在DelimiterBasedFrameDecoder中定義了一個ByteBuf的陣列:

    private final ByteBuf[] delimiters;

    delimiters= delimiter.readableBytes();

這個delimiters是通過呼叫delimiter的readableBytes得到的。

DelimiterBasedFrameDecoder的邏輯和LineBasedFrameDecoder差不多,都是通過對比bufer中的字元來對bufer中的資料進行擷取,但是DelimiterBasedFrameDecoder可以接受多個delimiters,所以它的用處會根據廣泛。

FixedLengthFrameDecoder

除了進行ByteBuf中字元比較來進行frame拆分之外,還有一些其他常見的frame拆分的方法,比如根據特定的長度來區分,netty提供了一種這樣的decoder叫做FixedLengthFrameDecoder。

public class FixedLengthFrameDecoder extends ByteToMessageDecoder 

FixedLengthFrameDecoder也是繼承自ByteToMessageDecoder,它的定義很簡單,可以傳入一個frame的長度:

    public FixedLengthFrameDecoder(int frameLength) {
        checkPositive(frameLength, "frameLength");
        this.frameLength = frameLength;
    }

然後呼叫ByteBuf的readRetainedSlice方法來讀取固定長度的資料:

in.readRetainedSlice(frameLength)

最後將讀取到的資料返回。

LengthFieldBasedFrameDecoder

還有一些frame中包含了特定的長度欄位,這個長度欄位表示ByteBuf中有多少可讀的資料,這樣的frame叫做LengthFieldBasedFrame。

netty中也提供了一個對應的處理decoder:

public class LengthFieldBasedFrameDecoder extends ByteToMessageDecoder 

讀取的邏輯很簡單,首先讀取長度,然後再根據長度再讀取資料。為了實現這個邏輯,LengthFieldBasedFrameDecoder提供了4個欄位,分別是 lengthFieldOffset,lengthFieldLength,lengthAdjustment和initialBytesToStrip。

lengthFieldOffset指定了長度欄位的開始位置,lengthFieldLength定義的是長度欄位的長度,lengthAdjustment是對lengthFieldLength進行調整,initialBytesToStrip表示是否需要去掉長度欄位。

聽起來好像不太好理解,我們舉幾個例子,首先是最簡單的:

   BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)
   +--------+----------------+      +--------+----------------+
   | Length | Actual Content |----->| Length | Actual Content |
   | 0x000C | "HELLO, WORLD" |      | 0x000C | "HELLO, WORLD" |
   +--------+----------------+      +--------+----------------+

要編碼的訊息有個長度欄位,長度欄位後面就是真實的資料,0x000C是一個十六進位制,表示的資料是12,也就是"HELLO, WORLD" 中字串的長度。

這裡4個屬性的值是:

   lengthFieldOffset   = 0
   lengthFieldLength   = 2
   lengthAdjustment    = 0
   initialBytesToStrip = 0 

表示的是長度欄位從0開始,並且長度欄位佔有兩個位元組,長度不需要調整,也不需要對欄位進行調整。

再來看一個比較複雜的例子,在這個例子中4個屬性值如下:

   lengthFieldOffset   = 1  
   lengthFieldLength   = 2
   lengthAdjustment    = 1  
   initialBytesToStrip = 3  

對應的編碼資料如下所示:

BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)
   +------+--------+------+----------------+      +------+----------------+
   | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
   | 0xCA | 0x000C | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |
   +------+--------+------+----------------+      +------+----------------+

上面的例子中長度欄位是從第1個位元組開始的(第0個位元組是HDR1),長度欄位佔有2個位元組,長度再調整一個位元組,最終資料的開始位置就是1+2+1=4,然後再擷取前3個位元組的資料,得到了最後的結果。

總結

netty提供的這幾個基於字符集的frame decoder基本上能夠滿足我們日常的工作需求了。當然,如果你傳輸的是一些更加複雜的物件,那麼可以考慮自定義編碼和解碼器。自定義的邏輯步驟和上面我們講解的保持一致就行了。

本文已收錄於 http://www.flydean.com/14-5-netty-frame-decoder/

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

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

相關文章