netty源碼分析之LengthFieldBasedFrameDecoder

拆包的原理

關于拆包原理的上一篇博文 netty源碼分析之拆包器的奧秘 中已詳細闡述,這里簡單總結下:netty的拆包過程和自己寫手工拆包并沒有什么不同,都是將字節累加到一個容器里面,判斷當前累加的字節數據是否達到了一個包的大小,達到一個包大小就拆開,進而傳遞到上層業務解碼handler

之所以netty的拆包能做到如此強大,就是因為netty將具體如何拆包抽象出一個decode方法,不同的拆包器實現不同的decode方法,就能實現不同協議的拆包

這篇文章中要講的就是通用拆包器LengthFieldBasedFrameDecoder,如果你還在自己實現人肉拆包,不妨了解一下這個強大的拆包器,因為幾乎所有和長度相關的二進制協議都可以通過TA來實現,下面我們先看看他有哪些用法

LengthFieldBasedFrameDecoder 的用法

1.基于長度的拆包

Paste_Image.png

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

new LengthFieldBasedFrameDecoder(Integer.MAX, 0, 4);

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

2.基于長度的截斷拆包

如果我們的應用層解碼器不需要使用到長度字段,那么我們希望netty拆完包之后,是這個樣子

Paste_Image.png

長度域被截掉,我們只需要指定另外一個參數就可以實現,這個參數叫做 initialBytesToStrip,表示netty拿到一個完整的數據包之后向業務解碼器傳遞之前,應該跳過多少字節

new LengthFieldBasedFrameDecoder(Integer.MAX, 0, 4, 0, 4);

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

3.基于偏移長度的拆包

下面這種方式二進制協議是更為普遍的,前面幾個固定字節表示協議頭,通常包含一些magicNumber,protocol version 之類的meta信息,緊跟著后面的是一個長度域,表示包體有多少字節的數據

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源碼分析系列視頻:https://coding.imooc.com/class/230.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容