拆包的原理
關於拆包原理的上一篇博文 netty原始碼分析之拆包器的奧祕 中已詳細闡述,這裡簡單總結下:netty的拆包過程和自己寫手工拆包並沒有什麼不同,都是將位元組累加到一個容器裡面,判斷當前累加的位元組資料是否達到了一個包的大小,達到一個包大小就拆開,進而傳遞到上層業務解碼handler
之所以netty的拆包能做到如此強大,就是因為netty將具體如何拆包抽象出一個decode
方法,不同的拆包器實現不同的decode
方法,就能實現不同協議的拆包
這篇文章中要講的就是通用拆包器LengthFieldBasedFrameDecoder
,如果你還在自己實現人肉拆包,不妨瞭解一下這個強大的拆包器,因為幾乎所有和長度相關的二進位制協議都可以通過TA來實現,下面我們先看看他有哪些用法
LengthFieldBasedFrameDecoder 的用法
1.基於長度的拆包
上面這類資料包協議比較常見的,前面幾個位元組表示資料包的長度(不包括長度域),後面是具體的資料。拆完之後資料包是一個完整的帶有長度域的資料包(之後即可傳遞到應用層解碼器進行解碼),建立一個如下方式的LengthFieldBasedFrameDecoder
即可實現這類協議
new LengthFieldBasedFrameDecoder(Integer.MAX, 0, 4);
複製程式碼
其中
1.第一個引數是 maxFrameLength
表示的是包的最大長度,超出包的最大長度netty將會做一些特殊處理,後面會講到
2.第二個引數指的是長度域的偏移量lengthFieldOffset
,在這裡是0,表示無偏移
3.第三個引數指的是長度域長度lengthFieldLength
,這裡是4,表示長度域的長度為4
2.基於長度的截斷拆包
如果我們的應用層解碼器不需要使用到長度欄位,那麼我們希望netty拆完包之後,是這個樣子
長度域被截掉,我們只需要指定另外一個引數就可以實現,這個引數叫做 initialBytesToStrip
,表示netty拿到一個完整的資料包之後向業務解碼器傳遞之前,應該跳過多少位元組
new LengthFieldBasedFrameDecoder(Integer.MAX, 0, 4, 0, 4);
複製程式碼
前面三個引數的含義和上文相同,第四個引數我們後面再講,而這裡的第五個引數就是initialBytesToStrip
,這裡為4,表示獲取完一個完整的資料包之後,忽略前面的四個位元組,應用解碼器拿到的就是不帶長度域的資料包
3.基於偏移長度的拆包
下面這種方式二進位制協議是更為普遍的,前面幾個固定位元組表示協議頭,通常包含一些magicNumber,protocol version 之類的meta資訊,緊跟著後面的是一個長度域,表示包體有多少位元組的資料
只需要基於第一種情況,調整第二個引數既可以實現
new LengthFieldBasedFrameDecoder(Integer.MAX, 4, 4);
複製程式碼
lengthFieldOffset
是4,表示跳過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);
複製程式碼
5.基於偏移可調整長度的截斷拆包
更變態一點的二進位制協議帶有兩個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);
複製程式碼
6.基於偏移可調整變異長度的截斷拆包
前面的所有的長度域表示的都是不帶header的包體的長度,如果讓長度域表示的含義包含整個資料包的長度,比如如下這種情況
其中長度域欄位的值為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,則表示讀取到長度域,他的值的超過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,但是他的含義是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);
}
複製程式碼
抽取的過程是簡單的呼叫了一下 ByteBuf
的retainedSlice
api,該api無記憶體copy開銷
從真正抽取資料包來看看,傳入的引數為 int
型別,所以,可以判斷,自定義協議中,如果你的長度域是8個位元組的,那麼前面四個位元組基本是沒有用的。
總結
1.如果你使用了netty,並且二進位制協議是基於長度,考慮使用LengthFieldBasedFrameDecoder
吧,通過調整各種引數,一定會滿足你的需求
2.LengthFieldBasedFrameDecoder
的拆包包括合法引數校驗,異常包處理,以及最後呼叫 ByteBuf
的retainedSlice
來實現無記憶體copy的拆包
如果你想系統地學Netty,我的小冊《Netty 入門與實戰:仿寫微信 IM 即時通訊系統》可以幫助你
如果你想系統學習Netty原理,那麼你一定不要錯過我的Netty原始碼分析系列視訊:Java 讀原始碼之 Netty 深入剖析