netty源碼分析之LengthFieldBasedFrameDecoder

拆包的原理

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

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

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

LengthFieldBasedFrameDecoder 的用法

1.基于長度的拆包

Paste_Image.png

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

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

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

2.基于長度的截斷拆包

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

Paste_Image.png

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

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

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

3.基于偏移長度的拆包

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

Paste_Image.png

只需要基于第一種情況,調(diào)整第二個參數(shù)既可以實現(xiàn)

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

lengthFieldOffset 是4,表示跳過4個字節(jié)之后的才是長度域

4.基于可調(diào)整長度的拆包

有些時候,二進制協(xié)議可能會設計成如下方式

Paste_Image.png

即長度域在前,header在后,這種情況又是如何來調(diào)整參數(shù)達到我們想要的拆包效果呢?

1.長度域在數(shù)據(jù)包最前面表示無偏移,lengthFieldOffset 為 0
2.長度域的長度為3,即lengthFieldLength為3
2.長度域表示的包體的長度略過了header,這里有另外一個參數(shù),叫做 lengthAdjustment,包體長度調(diào)整的大小,長度域的數(shù)值表示的長度加上這個修正值表示的就是帶header的包,這里是 12+2,header和包體一共占14個字節(jié)

最后,代碼實現(xiàn)為

new LengthFieldBasedFrameDecoder(Integer.MAX, 0, 3, 2, 0);

5.基于偏移可調(diào)整長度的截斷拆包

更變態(tài)一點的二進制協(xié)議帶有兩個header,比如下面這種

Paste_Image.png

拆完之后,HDR1 丟棄,長度域丟棄,只剩下第二個header和有效包體,這種協(xié)議中,一般HDR1可以表示magicNumber,表示應用只接受以該magicNumber開頭的二進制數(shù)據(jù),rpc里面用的比較多

我們?nèi)匀豢梢酝ㄟ^設置netty的參數(shù)實現(xiàn)

1.長度域偏移為1,那么 lengthFieldOffset為1
2.長度域長度為2,那么lengthFieldLength為2
3.長度域表示的包體的長度略過了HDR2,但是拆包的時候HDR2也被netty當作是包體的的一部分來拆,HDR2的長度為1,那么 lengthAdjustment 為1
4.拆完之后,截掉了前面三個字節(jié),那么 initialBytesToStrip 為 3

最后,代碼實現(xiàn)為

   new LengthFieldBasedFrameDecoder(Integer.MAX, 1, 2, 1, 3);

6.基于偏移可調(diào)整變異長度的截斷拆包

前面的所有的長度域表示的都是不帶header的包體的長度,如果讓長度域表示的含義包含整個數(shù)據(jù)包的長度,比如如下這種情況

Paste_Image.png

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

這里除了長度域表示的含義和上一種情況不一樣之外,其他都相同,因為netty并不了解業(yè)務情況,你需要告訴netty的是,長度域后面,再跟多少字節(jié)就可以形成一個完整的數(shù)據(jù)包,這里顯然是13個字節(jié),而長度域的值為16,因此減掉3才是真是的拆包所需要的長度,lengthAdjustment為-3

這里的六種情況是netty源碼里自帶的六中典型的二進制協(xié)議,相信已經(jīng)囊括了90%以上的場景,如果你的協(xié)議是基于長度的,那么可以考慮不用字節(jié)來實現(xiàn),而是直接拿來用,或者繼承他,做些簡單的修改即可

如此強大的拆包器其實現(xiàn)也是非常優(yōu)雅,下面我們來一起看下netty是如何來實現(xiàn)

LengthFieldBasedFrameDecoder 源碼剖析

構(gòu)造函數(shù)

關于LengthFieldBasedFrameDecoder 的構(gòu)造函數(shù),我們只需要看一個就夠了

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

構(gòu)造函數(shù)做的事很簡單,只是把傳入的參數(shù)簡單地保存在field,這里的大多數(shù)field在前面已經(jīng)闡述過,剩下的幾個補充說明下
1.byteOrder 表示字節(jié)流表示的數(shù)據(jù)是大端還是小端,用于長度域的讀取
2.lengthFieldEndOffset表示緊跟長度域字段后面的第一個字節(jié)的在整個數(shù)據(jù)包中的偏移量
3.failFast,如果為true,則表示讀取到長度域,TA的值的超過maxFrameLength,就拋出一個 TooLongFrameException,而為false表示只有當真正讀取完長度域的值表示的字節(jié)之后,才會拋出 TooLongFrameException,默認情況下設置為true,建議不要修改,否則可能會造成內(nèi)存溢出

實現(xiàn)拆包抽象

netty源碼分析之拆包器的奧秘,我們已經(jīng)知道,具體的拆包協(xié)議只需要實現(xiàn)

void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) 

其中 in 表示目前為止還未拆的數(shù)據(jù),拆完之后的包添加到 out這個list中即可實現(xiàn)包向下傳遞

第一層實現(xiàn)比較簡單

@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函數(shù)decode做真正的拆包動作,下面分三個部分來分析一下這個重量級函數(shù)

獲取frame長度

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

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

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


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

// 調(diào)整包的長度,后面統(tǒng)一做拆分
frameLength += lengthAdjustment + lengthFieldEndOffset;

上面這一段內(nèi)容有個擴展點 getUnadjustedFrameLength,如果你的長度域代表的值表達的含義不是正常的int,short等基本類型,你可以重寫這個函數(shù)

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個字節(jié),比如 0x1234,但是TA的含義是10進制,即長度就是十進制的1234,那么覆蓋這個函數(shù)即可實現(xiàn)奇葩長度域拆包

2. 長度校驗

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

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

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

最后,調(diào)用failIfNecessary判斷是否需要拋出異常

private void failIfNecessary(boolean firstDetectionOfTooLongFrame) {
    // 不需要再丟棄后面的未讀字節(jié),就開始重置丟棄狀態(tài)
    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");
    }
}

丟棄模式的處理

如果讀者是一邊對著源碼,一邊閱讀本篇文章,就會發(fā)現(xiàn) LengthFieldBasedFrameDecoder.decoder 函數(shù)的入口處還有一段代碼在我們的前面的分析中被我省略掉了,放到這一小節(jié)中的目的是為了承接上一小節(jié),更加容易讀懂丟棄模式的處理

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

    failIfNecessary(false);
}

如上,如果當前處在丟棄模式,先計算需要丟棄多少字節(jié),取當前還需可丟棄字節(jié)和可讀字節(jié)的最小值,丟棄掉之后,進入 failIfNecessary,對照著這個函數(shù)看,默認情況下是不會繼續(xù)拋出異常,而如果設置了 failFast為false,那么等丟棄完之后,才會拋出異常,讀者可自行分析

跳過指定字節(jié)長度

丟棄模式的處理以及長度的校驗都通過之后,進入到跳過指定字節(jié)長度這個環(huán)節(jié)

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);

先驗證當前是否已經(jīng)讀到足夠的字節(jié),如果讀到了,在下一步抽取一個完整的數(shù)據(jù)包之前,需要根據(jù)initialBytesToStrip的設置來跳過某些字節(jié)(見文章開篇),當然,跳過的字節(jié)不能大于數(shù)據(jù)包的長度,否則就拋出 CorruptedFrameException 的異常

抽取frame

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

return frame;

到了最后抽取數(shù)據(jù)包其實就很簡單了,拿到當前累積數(shù)據(jù)的讀指針,然后拿到待抽取數(shù)據(jù)包的實際長度進行抽取,抽取之后,移動讀指針

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

抽取的過程是簡單的調(diào)用了一下 ByteBufretainedSliceapi,該api無內(nèi)存copy開銷

從真正抽取數(shù)據(jù)包來看看,傳入的參數(shù)為 int 類型,所以,可以判斷,自定義協(xié)議中,如果你的長度域是8個字節(jié)的,那么前面四個字節(jié)基本是沒有用的。

總結(jié)

1.如果你使用了netty,并且二進制協(xié)議是基于長度,考慮使用LengthFieldBasedFrameDecoder吧,通過調(diào)整各種參數(shù),一定會滿足你的需求
2.LengthFieldBasedFrameDecoder的拆包包括合法參數(shù)校驗,異常包處理,以及最后調(diào)用 ByteBufretainedSlice來實現(xiàn)無內(nèi)存copy的拆包

如果你覺得看的不過癮,想系統(tǒng)學習Netty原理,那么你一定不要錯過我的Netty源碼分析系列視頻:https://coding.imooc.com/class/230.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內(nèi)容