拆包的原理
關于拆包原理的上一篇博文 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.基于長度的拆包
上面這類數(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拆完包之后,是這個樣子
長度域被截掉,我們只需要指定另外一個參數(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ù)
只需要基于第一種情況,調(diào)整第二個參數(shù)既可以實現(xiàn)
new LengthFieldBasedFrameDecoder(Integer.MAX, 4, 4);
lengthFieldOffset
是4,表示跳過4個字節(jié)之后的才是長度域
4.基于可調(diào)整長度的拆包
有些時候,二進制協(xié)議可能會設計成如下方式
即長度域在前,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,比如下面這種
拆完之后,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ù)包的長度,比如如下這種情況
其中長度域字段的值為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)用了一下 ByteBuf
的retainedSlice
api,該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)用 ByteBuf
的retainedSlice
來實現(xiàn)無內(nèi)存copy的拆包
如果你覺得看的不過癮,想系統(tǒng)學習Netty原理,那么你一定不要錯過我的Netty源碼分析系列視頻:https://coding.imooc.com/class/230.html