Netty源碼分析之LengthFieldBasedFrameDecoder

拆包的原理

關於拆包原理的上一篇博文 netty源碼分析之拆包器的奧祕 中已詳細闡述,這裏簡單總結下:netty的拆包過程和本身寫手工拆包並無什麼不一樣,都是將字節累加到一個容器裏面,判斷當前累加的字節數據是否達到了一個包的大小,達到一個包大小就拆開,進而傳遞到上層業務解碼handlerhtml

之因此netty的拆包能作到如此強大,就是由於netty將具體如何拆包抽象出一個decode方法,不一樣的拆包器實現不一樣的decode方法,就能實現不一樣協議的拆包java

這篇文章中要講的就是通用拆包器LengthFieldBasedFrameDecoder,若是你還在本身實現人肉拆包,不妨瞭解一下這個強大的拆包器,由於幾乎全部和長度相關的二進制協議均可以經過TA來實現,下面咱們先看看他有哪些用法api

LengthFieldBasedFrameDecoder 的用法

1.基於長度的拆包

Paste_Image.png

上面這類數據包協議比較常見的,前面幾個字節表示數據包的長度(不包括長度域),後面是具體的數據。拆完以後數據包是一個完整的帶有長度域的數據包(以後便可傳遞到應用層解碼器進行解碼),建立一個以下方式的LengthFieldBasedFrameDecoder便可實現這類協議微信

new LengthFieldBasedFrameDecoder(Integer.MAX, 0, 4);
複製代碼

其中 1.第一個參數是 maxFrameLength 表示的是包的最大長度,超出包的最大長度netty將會作一些特殊處理,後面會講到 2.第二個參數指的是長度域的偏移量lengthFieldOffset,在這裏是0,表示無偏移 3.第三個參數指的是長度域長度lengthFieldLength,這裏是4,表示長度域的長度爲4less

2.基於長度的截斷拆包

若是咱們的應用層解碼器不須要使用到長度字段,那麼咱們但願netty拆完包以後,是這個樣子ide

Paste_Image.png

長度域被截掉,咱們只須要指定另一個參數就能夠實現,這個參數叫作 initialBytesToStrip,表示netty拿到一個完整的數據包以後向業務解碼器傳遞以前,應該跳過多少字節函數

new LengthFieldBasedFrameDecoder(Integer.MAX, 0, 4, 0, 4);
複製代碼

前面三個參數的含義和上文相同,第四個參數咱們後面再講,而這裏的第五個參數就是initialBytesToStrip,這裏爲4,表示獲取完一個完整的數據包以後,忽略前面的四個字節,應用解碼器拿到的就是不帶長度域的數據包源碼分析

3.基於偏移長度的拆包

下面這種方式二進制協議是更爲廣泛的,前面幾個固定字節表示協議頭,一般包含一些magicNumber,protocol version 之類的meta信息,緊跟着後面的是一個長度域,表示包體有多少字節的數據post

Paste_Image.png

只須要基於第一種狀況,調整第二個參數既能夠實現學習

new LengthFieldBasedFrameDecoder(Integer.MAX, 4, 4);
複製代碼

lengthFieldOffset 是4,表示跳過4個字節以後的纔是長度域

4.基於可調整長度的拆包

有些時候,二進制協議可能會設計成以下方式

Paste_Image.png

即長度域在前,header在後,這種狀況又是如何來調整參數達到咱們想要的拆包效果呢?

1.長度域在數據包最前面表示無偏移,lengthFieldOffset 爲 0 2.長度域的長度爲3,即lengthFieldLength爲3 2.長度域表示的包體的長度略過了header,這裏有另一個參數,叫作 lengthAdjustment,包體長度調整的大小,長度域的數值表示的長度加上這個修正值表示的就是帶header的包,這裏是 12+2,header和包體一共佔14個字節

最後,代碼實現爲

new LengthFieldBasedFrameDecoder(Integer.MAX, 0, 3, 2, 0);
複製代碼

5.基於偏移可調整長度的截斷拆包

更變態一點的二進制協議帶有兩個header,好比下面這種

Paste_Image.png

拆完以後,HDR1 丟棄,長度域丟棄,只剩下第二個header和有效包體,這種協議中,通常HDR1能夠表示magicNumber,表示應用只接受以該magicNumber開頭的二進制數據,rpc裏面用的比較多

咱們仍然能夠經過設置netty的參數實現

1.長度域偏移爲1,那麼 lengthFieldOffset爲1 2.長度域長度爲2,那麼lengthFieldLength爲2 3.長度域表示的包體的長度略過了HDR2,可是拆包的時候HDR2也被netty看成是包體的的一部分來拆,HDR2的長度爲1,那麼 lengthAdjustment 爲1 4.拆完以後,截掉了前面三個字節,那麼 initialBytesToStrip 爲 3

最後,代碼實現爲

new LengthFieldBasedFrameDecoder(Integer.MAX, 1, 2, 1, 3);
複製代碼

6.基於偏移可調整變異長度的截斷拆包

前面的全部的長度域表示的都是不帶header的包體的長度,若是讓長度域表示的含義包含整個數據包的長度,好比以下這種狀況

Paste_Image.png

其中長度域字段的值爲16, 其字段長度爲2,HDR1的長度爲1,HDR2的長度爲1,包體的長度爲12,1+1+2+12=16,又該如何設置參數呢?

這裏除了長度域表示的含義和上一種狀況不同以外,其餘都相同,由於netty並不瞭解業務狀況,你須要告訴netty的是,長度域後面,再跟多少字節就能夠造成一個完整的數據包,這裏顯然是13個字節,而長度域的值爲16,所以減掉3纔是真是的拆包所須要的長度,lengthAdjustment爲-3

這裏的六種狀況是netty源碼裏自帶的六中典型的二進制協議,相信已經囊括了90%以上的場景,若是你的協議是基於長度的,那麼能夠考慮不用字節來實現,而是直接拿來用,或者繼承他,作些簡單的修改便可

如此強大的拆包器其實現也是很是優雅,下面咱們來一塊兒看下netty是如何來實現

LengthFieldBasedFrameDecoder 源碼剖析

構造函數

關於LengthFieldBasedFrameDecoder 的構造函數,咱們只須要看一個就夠了

public LengthFieldBasedFrameDecoder( ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
    // 省略參數校驗部分
    this.byteOrder = byteOrder;
    this.maxFrameLength = maxFrameLength;
    this.lengthFieldOffset = lengthFieldOffset;
    this.lengthFieldLength = lengthFieldLength;
    this.lengthAdjustment = lengthAdjustment;
    lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength;
    this.initialBytesToStrip = initialBytesToStrip;
    this.failFast = failFast;
}
複製代碼

構造函數作的事很簡單,只是把傳入的參數簡單地保存在field,這裏的大多數field在前面已經闡述過,剩下的幾個補充說明下 1.byteOrder 表示字節流表示的數據是大端仍是小端,用於長度域的讀取 2.lengthFieldEndOffset表示緊跟長度域字段後面的第一個字節的在整個數據包中的偏移量 3.failFast,若是爲true,則表示讀取到長度域,TA的值的超過maxFrameLength,就拋出一個 TooLongFrameException,而爲false表示只有當真正讀取完長度域的值表示的字節以後,纔會拋出 TooLongFrameException,默認狀況下設置爲true,建議不要修改,不然可能會形成內存溢出

實現拆包抽象

netty源碼分析之拆包器的奧祕,咱們已經知道,具體的拆包協議只須要實現

void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) 複製代碼

其中 in 表示目前爲止還未拆的數據,拆完以後的包添加到 out這個list中便可實現包向下傳遞

第一層實現比較簡單

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

重載的protected函數decode作真正的拆包動做,下面分三個部分來分析一下這個重量級函數

獲取frame長度

1.獲取須要待拆包的包大小

// 若是當前可讀字節還未達到長度長度域的偏移,那說明確定是讀不到長度域的,直接不讀
if (in.readableBytes() < lengthFieldEndOffset) {
    return null;
}

// 拿到長度域的實際字節偏移 
int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;
// 拿到實際的未調整過的包長度
long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, lengthFieldLength, byteOrder);


// 若是拿到的長度爲負數,直接跳過長度域並拋出異常
if (frameLength < 0) {
    in.skipBytes(lengthFieldEndOffset);
    throw new CorruptedFrameException(
            "negative pre-adjustment length field: " + frameLength);
}

// 調整包的長度,後面統一作拆分
frameLength += lengthAdjustment + lengthFieldEndOffset;

複製代碼

上面這一段內容有個擴展點 getUnadjustedFrameLength,若是你的長度域表明的值表達的含義不是正常的int,short等基本類型,你能夠重寫這個函數

protected long getUnadjustedFrameLength(ByteBuf buf, int offset, int length, ByteOrder order) {
        buf = buf.order(order);
        long frameLength;
        switch (length) {
        case 1:
            frameLength = buf.getUnsignedByte(offset);
            break;
        case 2:
            frameLength = buf.getUnsignedShort(offset);
            break;
        case 3:
            frameLength = buf.getUnsignedMedium(offset);
            break;
        case 4:
            frameLength = buf.getUnsignedInt(offset);
            break;
        case 8:
            frameLength = buf.getLong(offset);
            break;
        default:
            throw new DecoderException(
                    "unsupported lengthFieldLength: " + lengthFieldLength + " (expected: 1, 2, 3, 4, or 8)");
        }
        return frameLength;
    }
複製代碼

好比,有的奇葩的長度域裏面雖然是4個字節,好比 0x1234,可是TA的含義是10進制,即長度就是十進制的1234,那麼覆蓋這個函數便可實現奇葩長度域拆包

2. 長度校驗

// 整個數據包的長度尚未長度域長,直接拋出異常
if (frameLength < lengthFieldEndOffset) {
    in.skipBytes(lengthFieldEndOffset);
    throw new CorruptedFrameException(
            "Adjusted frame length (" + frameLength + ") is less " +
            "than lengthFieldEndOffset: " + lengthFieldEndOffset);
}

// 數據包長度超出最大包長度,進入丟棄模式
if (frameLength > maxFrameLength) {
    long discard = frameLength - in.readableBytes();
    tooLongFrameLength = frameLength;

    if (discard < 0) {
        // 當前可讀字節已達到frameLength,直接跳過frameLength個字節,丟棄以後,後面有可能就是一個合法的數據包
        in.skipBytes((int) frameLength);
    } else {
        // 當前可讀字節未達到frameLength,說明後面未讀到的字節也須要丟棄,進入丟棄模式,先把當前累積的字節所有丟棄
        discardingTooLongFrame = true;
        // bytesToDiscard表示還須要丟棄多少字節
        bytesToDiscard = discard;
        in.skipBytes(in.readableBytes());
    }
    failIfNecessary(true);
    return null;
}

複製代碼

最後,調用failIfNecessary判斷是否須要拋出異常

private void failIfNecessary(boolean firstDetectionOfTooLongFrame) {
    // 不須要再丟棄後面的未讀字節,就開始重置丟棄狀態
    if (bytesToDiscard == 0) {
        long tooLongFrameLength = this.tooLongFrameLength;
        this.tooLongFrameLength = 0;
        discardingTooLongFrame = false;
        // 若是沒有設置快速失敗,或者設置了快速失敗而且是第一次檢測到大包錯誤,拋出異常,讓handler去處理
        if (!failFast ||
            failFast && firstDetectionOfTooLongFrame) {
            fail(tooLongFrameLength);
        }
    } else {
        // 若是設置了快速失敗,而且是第一次檢測到打包錯誤,拋出異常,讓handler去處理
        if (failFast && firstDetectionOfTooLongFrame) {
            fail(tooLongFrameLength);
        }
    }
}
複製代碼

前面咱們能夠知道failFast默認爲true,而這裏firstDetectionOfTooLongFrame爲true,因此,第一次檢測到大包確定會拋出異常

下面是拋出異常的代碼

private void fail(long frameLength) {
    if (frameLength > 0) {
        throw new TooLongFrameException(
                        "Adjusted frame length exceeds " + maxFrameLength +
                        ": " + frameLength + " - discarded");
    } else {
        throw new TooLongFrameException(
                        "Adjusted frame length exceeds " + maxFrameLength +
                        " - discarding");
    }
}
複製代碼

丟棄模式的處理

若是讀者是一邊對着源碼,一邊閱讀本篇文章,就會發現 LengthFieldBasedFrameDecoder.decoder 函數的入口處還有一段代碼在咱們的前面的分析中被我省略掉了,放到這一小節中的目的是爲了承接上一小節,更加容易讀懂丟棄模式的處理

if (discardingTooLongFrame) {
    long bytesToDiscard = this.bytesToDiscard;
    int localBytesToDiscard = (int) Math.min(bytesToDiscard, in.readableBytes());
    in.skipBytes(localBytesToDiscard);
    bytesToDiscard -= localBytesToDiscard;
    this.bytesToDiscard = bytesToDiscard;

    failIfNecessary(false);
}
複製代碼

如上,若是當前處在丟棄模式,先計算須要丟棄多少字節,取當前還需可丟棄字節和可讀字節的最小值,丟棄掉以後,進入 failIfNecessary,對照着這個函數看,默認狀況下是不會繼續拋出異常,而若是設置了 failFast爲false,那麼等丟棄完以後,纔會拋出異常,讀者可自行分析

跳過指定字節長度

丟棄模式的處理以及長度的校驗都經過以後,進入到跳過指定字節長度這個環節

int frameLengthInt = (int) frameLength;
if (in.readableBytes() < frameLengthInt) {
    return null;
}

if (initialBytesToStrip > frameLengthInt) {
    in.skipBytes(frameLengthInt);
    throw new CorruptedFrameException(
            "Adjusted frame length (" + frameLength + ") is less " +
            "than initialBytesToStrip: " + initialBytesToStrip);
}
in.skipBytes(initialBytesToStrip);
複製代碼

先驗證當前是否已經讀到足夠的字節,若是讀到了,在下一步抽取一個完整的數據包以前,須要根據initialBytesToStrip的設置來跳過某些字節(見文章開篇),固然,跳過的字節不能大於數據包的長度,不然就拋出 CorruptedFrameException 的異常

抽取frame

int readerIndex = in.readerIndex();
int actualFrameLength = frameLengthInt - initialBytesToStrip;
ByteBuf frame = extractFrame(ctx, in, readerIndex, actualFrameLength);
in.readerIndex(readerIndex + actualFrameLength);

return frame;
複製代碼

到了最後抽取數據包其實就很簡單了,拿到當前累積數據的讀指針,而後拿到待抽取數據包的實際長度進行抽取,抽取以後,移動讀指針

protected ByteBuf extractFrame(ChannelHandlerContext ctx, ByteBuf buffer, int index, int length) {
    return buffer.retainedSlice(index, length);
}
複製代碼

抽取的過程是簡單的調用了一下 ByteBufretainedSliceapi,該api無內存copy開銷

從真正抽取數據包來看看,傳入的參數爲 int 類型,因此,能夠判斷,自定義協議中,若是你的長度域是8個字節的,那麼前面四個字節基本是沒有用的。

總結

1.若是你使用了netty,而且二進制協議是基於長度,考慮使用LengthFieldBasedFrameDecoder吧,經過調整各類參數,必定會知足你的需求 2.LengthFieldBasedFrameDecoder的拆包包括合法參數校驗,異常包處理,以及最後調用 ByteBufretainedSlice來實現無內存copy的拆包

若是你想系統地學Netty,個人小冊《Netty 入門與實戰:仿寫微信 IM 即時通信系統》能夠幫助你

image.png

若是你想系統學習Netty原理,那麼你必定不要錯過個人Netty源碼分析系列視頻:Java 讀源碼之 Netty 深刻剖析

image.png
image.png
image.png
image.png
相關文章
相關標籤/搜索