netty系列之:內建的Frame detection

flydean發表於2021-08-19

簡介

上篇文章我們講到了netty中怎麼自定義編碼和解碼器,但是自定義實現起來還是挺複雜的,一般沒有特殊必要的情況下,大家都希望越簡單越好,其難點就是找到ByteBuf中的分割點,將ByteBuf分割成為一個個的可以處理的單元。今天本文講講netty中自帶的分割處理機制。

Frame detection

在上一章,我們提到了需要有一種手段來區分ByteBuf中不同的資料,也就是說找到ByteBuf中不同資料的分割點。如果首先將ByteBuf分割成一個個的獨立的ByteBuf,再對獨立的ByteBuf進行處理就會簡單很多。

netty中提供了4個分割點的編碼器,我們可以稱之為Frame detection,他們分別是DelimiterBasedFrameDecoder, FixedLengthFrameDecoder, LengthFieldBasedFrameDecoder, 和 LineBasedFrameDecoder。

這幾個類都是ByteToMessageDecoder的子類,接下來我們一一進行介紹。

DelimiterBasedFrameDecoder

首先是DelimiterBasedFrameDecoder,看名字就知道這個是根據delimiter對bytebuf進行分割的解碼器。什麼是delimiter呢?

netty中有一個Delimiters類,專門定義分割的字元,主要有兩個delimiter,分別是nulDelimiter和lineDelimiter:

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' }),
        };
    }

nullDelimiter用來處理0x00,主要用來處理Flash XML socket或者其他的類似的協議。

lineDelimiter用來處理回車和換行符,主要用來文字檔案的處理中。

對於DelimiterBasedFrameDecoder來說,如果有多個delimiter的話,會選擇將ByteBuf分割最短的那個,舉個例子,如果我們使用DelimiterBasedFrameDecoder(Delimiters.lineDelimiter()) ,因為lineDelimiter中實際上有兩個分割方式,回車+換行或者換行,如果遇到下面的情況:

   +--------------+
   | ABC\nDEF\r\n |
   +--------------+

DelimiterBasedFrameDecoder會選擇最短的分割結果,也就說將上面的內容分割成為:

   +-----+-----+
   | ABC | DEF |
   +-----+-----+

而不是

   +----------+
   | ABC\nDEF |
   +----------+

FixedLengthFrameDecoder

這個類會將ByteBuf分成固定的長度,比如收到了下面的4塊byte資訊:

   +---+----+------+----+
   | A | BC | DEFG | HI |
   +---+----+------+----+

如果使用一個FixedLengthFrameDecoder(3) ,則會將上面的ByteBuf分成下面的幾個部分:

   +-----+-----+-----+
   | ABC | DEF | GHI |
   +-----+-----+-----+

LengthFieldBasedFrameDecoder

這個類就更加靈活一點,可以根據資料中的length欄位取出後續的byte陣列。LengthFieldBasedFrameDecoder非常靈活,它有4個屬性來控制他們分別是lengthFieldOffset、lengthFieldLength、lengthAdjustment和initialBytesToStrip。

lengthFieldOffset是長度欄位的起始位置,lengthFieldLength是長度欄位本身的長度,lengthAdjustment是對目標資料長度進行調整,initialBytesToStrip是解密過程中需要刪除的byte數目。理解不了?沒關係,我們來舉幾個例子。

首先看一個最簡單的:

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

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

上面的設定表示,length是從第0位開始的,長度是2個位元組。其中Ox00C=12, 這也是“HELLO, WORLD” 的長度。

如果不想要Length欄位,可以通過設定initialBytesToStrip把length刪除:

   lengthFieldOffset   = 0
   lengthFieldLength   = 2
   lengthAdjustment    = 0
   initialBytesToStrip = 2 (= length 欄位的長度)
  
   BEFORE DECODE (14 bytes)         AFTER DECODE (12 bytes)
   +--------+----------------+      +----------------+
   | Length | Actual Content |----->| Actual Content |
   | 0x000C | "HELLO, WORLD" |      | "HELLO, WORLD" |
   +--------+----------------+      +----------------+

lengthAdjustment是對Length欄位的值進行調整,因為在有些情況下Length欄位可能包含了整條資料的長度,也就是Length+內容,所以需要在解析的時候進行調整,比如下面的例子,真實長度其實是0x0C,但是傳入的卻是0x0E,所以需要減去Length欄位的長度2,也就是將lengthAdjustment設定為-2。

   lengthFieldOffset   =  0
   lengthFieldLength   =  2
   lengthAdjustment    = -2 (= Length欄位的長度)
   initialBytesToStrip =  0

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

LineBasedFrameDecoder

LineBasedFrameDecoder專門處理文字檔案中的一行結束。也就是 "\n" 和 "\r\n",他和DelimiterBasedFrameDecoder很類似,但是DelimiterBasedFrameDecoder更加通用。

總結

有了上面4個Frame detection裝置之後,就可以在pipline中首先新增這些Frame detection,然後再新增自定義的handler,這樣在自定義的handler中就不用考慮讀取ByteBuf的長度問題了。

比如在StringDecoder中,如果已經使用了 LineBasedFrameDecoder , 那麼在decode方法中可以假設傳入的ByteBuf就是一行字串,那麼可以直接這樣使用:

    protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
        out.add(msg.toString(charset));
    }

是不是很簡單?

本文已收錄於 http://www.flydean.com/15-netty-buildin-frame-detection/

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

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

相關文章