關於拆包原理的上一篇博文 netty源碼分析之拆包器的奧祕 中已詳細闡述,這裏簡單總結下:netty的拆包過程和本身寫手工拆包並無什麼不一樣,都是將字節累加到一個容器裏面,判斷當前累加的字節數據是否達到了一個包的大小,達到一個包大小就拆開,進而傳遞到上層業務解碼handlerhtml
之因此netty的拆包能作到如此強大,就是由於netty將具體如何拆包抽象出一個decode
方法,不一樣的拆包器實現不一樣的decode
方法,就能實現不一樣協議的拆包java
這篇文章中要講的就是通用拆包器LengthFieldBasedFrameDecoder
,若是你還在本身實現人肉拆包,不妨瞭解一下這個強大的拆包器,由於幾乎全部和長度相關的二進制協議均可以經過TA來實現,下面咱們先看看他有哪些用法api
上面這類數據包協議比較常見的,前面幾個字節表示數據包的長度(不包括長度域),後面是具體的數據。拆完以後數據包是一個完整的帶有長度域的數據包(以後便可傳遞到應用層解碼器進行解碼),建立一個以下方式的LengthFieldBasedFrameDecoder
便可實現這類協議微信
new LengthFieldBasedFrameDecoder(Integer.MAX, 0, 4);
複製代碼
其中 1.第一個參數是 maxFrameLength
表示的是包的最大長度,超出包的最大長度netty將會作一些特殊處理,後面會講到 2.第二個參數指的是長度域的偏移量lengthFieldOffset
,在這裏是0,表示無偏移 3.第三個參數指的是長度域長度lengthFieldLength
,這裏是4,表示長度域的長度爲4less
若是咱們的應用層解碼器不須要使用到長度字段,那麼咱們但願netty拆完包以後,是這個樣子ide
長度域被截掉,咱們只須要指定另一個參數就能夠實現,這個參數叫作 initialBytesToStrip
,表示netty拿到一個完整的數據包以後向業務解碼器傳遞以前,應該跳過多少字節函數
new LengthFieldBasedFrameDecoder(Integer.MAX, 0, 4, 0, 4);
複製代碼
前面三個參數的含義和上文相同,第四個參數咱們後面再講,而這裏的第五個參數就是initialBytesToStrip
,這裏爲4,表示獲取完一個完整的數據包以後,忽略前面的四個字節,應用解碼器拿到的就是不帶長度域的數據包源碼分析
下面這種方式二進制協議是更爲廣泛的,前面幾個固定字節表示協議頭,一般包含一些magicNumber,protocol version 之類的meta信息,緊跟着後面的是一個長度域,表示包體有多少字節的數據post
只須要基於第一種狀況,調整第二個參數既能夠實現學習
new LengthFieldBasedFrameDecoder(Integer.MAX, 4, 4);
複製代碼
lengthFieldOffset
是4,表示跳過4個字節以後的纔是長度域
有些時候,二進制協議可能會設計成以下方式
即長度域在前,header在後,這種狀況又是如何來調整參數達到咱們想要的拆包效果呢?
1.長度域在數據包最前面表示無偏移,lengthFieldOffset
爲 0 2.長度域的長度爲3,即lengthFieldLength
爲3 2.長度域表示的包體的長度略過了header,這裏有另一個參數,叫作 lengthAdjustment
,包體長度調整的大小,長度域的數值表示的長度加上這個修正值表示的就是帶header的包,這裏是 12+2,header和包體一共佔14個字節
最後,代碼實現爲
new LengthFieldBasedFrameDecoder(Integer.MAX, 0, 3, 2, 0);
複製代碼
更變態一點的二進制協議帶有兩個header,好比下面這種
拆完以後,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);
複製代碼
前面的全部的長度域表示的都是不帶header的包體的長度,若是讓長度域表示的含義包含整個數據包的長度,好比以下這種狀況
其中長度域字段的值爲16, 其字段長度爲2,HDR1的長度爲1,HDR2的長度爲1,包體的長度爲12,1+1+2+12=16,又該如何設置參數呢?
這裏除了長度域表示的含義和上一種狀況不同以外,其餘都相同,由於netty並不瞭解業務狀況,你須要告訴netty的是,長度域後面,再跟多少字節就能夠造成一個完整的數據包,這裏顯然是13個字節,而長度域的值爲16,所以減掉3纔是真是的拆包所須要的長度,lengthAdjustment
爲-3
這裏的六種狀況是netty源碼裏自帶的六中典型的二進制協議,相信已經囊括了90%以上的場景,若是你的協議是基於長度的,那麼能夠考慮不用字節來實現,而是直接拿來用,或者繼承他,作些簡單的修改便可
如此強大的拆包器其實現也是很是優雅,下面咱們來一塊兒看下netty是如何來實現
關於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
作真正的拆包動做,下面分三個部分來分析一下這個重量級函數
// 若是當前可讀字節還未達到長度長度域的偏移,那說明確定是讀不到長度域的,直接不讀
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,那麼覆蓋這個函數便可實現奇葩長度域拆包
// 整個數據包的長度尚未長度域長,直接拋出異常
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
的異常
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);
}
複製代碼
抽取的過程是簡單的調用了一下 ByteBuf
的retainedSlice
api,該api無內存copy開銷
從真正抽取數據包來看看,傳入的參數爲 int
類型,因此,能夠判斷,自定義協議中,若是你的長度域是8個字節的,那麼前面四個字節基本是沒有用的。
1.若是你使用了netty,而且二進制協議是基於長度,考慮使用LengthFieldBasedFrameDecoder
吧,經過調整各類參數,必定會知足你的需求 2.LengthFieldBasedFrameDecoder
的拆包包括合法參數校驗,異常包處理,以及最後調用 ByteBuf
的retainedSlice
來實現無內存copy的拆包
若是你想系統地學Netty,個人小冊《Netty 入門與實戰:仿寫微信 IM 即時通信系統》能夠幫助你
![]()
若是你想系統學習Netty原理,那麼你必定不要錯過個人Netty源碼分析系列視頻:Java 讀源碼之 Netty 深刻剖析
![]()
![]()
![]()
![]()