感慨
紙上得來終覺淺,源碼閱讀是進一步提高自身水平的手段。但源碼無數,並不是什麼樣的源碼都值得一讀。
須知任何技術都是爲了解決特定問題的,先針對問題進行思考,然後再讀源碼,會事半功倍。
本文按照一定的閱讀源碼思路來逐步解析ByteToMessageDecoder源碼。
ByteToMessageDecoder
外圍信息解析
繼承關係:
public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter
關鍵方法:
protected abstract void decode(ChannelHandlerContext var1, ByteBuf var2, List<Object> var3) throws Exception;
使用方式:
public class A extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
// byteBuf中,信息完整且能處理掉的,處理結果放到list中;信息不完整的,不消耗byteBuf,等下次完整了再處理;不能處理的,丟棄掉
}
}
從上述內容入手,可以解析到信息如下:
- 繼承了ChannelInboundHandlerAdapter,必然繼承其channelRead方法,該方法是我們閱讀源碼的入口。
- 提供了新的decode方法,將變化的部分交給開發人員自由實現
- 不變的部分,主要是byteBuf有保留上一次處理結果的能力,並不是一個一次性消耗的對象,用於針對TCP拆包問題,這是ByteToMessageDecoder的關鍵
思考:
- 必然存在某個變量,用於保留處理不完的ByteBuf
- 必然有某個方式,將處理不完的ByteBuf和當前的ByteBuf拼接到一起
解析深入源碼
channelRead外層結構
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
// (略)這裏是第二層結構
} else {
ctx.fireChannelRead(msg);
}
}
說明:解析源碼的目的不僅僅是在於瞭解其實現,實際上,在瞭解問題場景的情況下,一個合格的程序員,花點功夫也能實現。源碼往往是精心設計的,不僅是其準確性,其可讀性、組織規範、容錯性等方面也有很高的參考價值。
解析:其表達的意思很明確,判斷傳入類型,類型符合則處理,不符合則透傳。這是一個組織規範上的問題,在自寫處理時,要套用該模式。同時也說明了,設計上可以按照類型去做不同類型的處理,如下:
// 解析多種不同的輸入格式。
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
// (略)
} else if (msg instanceof String) {
// (略)
} else {
ctx.fireChannelRead(msg);
}
}
channelRead第二層內容
CodecOutputList out = CodecOutputList.newInstance();
boolean var10 = false;
try {
var10 = true;
ByteBuf data = (ByteBuf)msg;
this.first = this.cumulation == null;
if (this.first) {
this.cumulation = data;
} else {
this.cumulation = this.cumulator.cumulate(ctx.alloc(), this.cumulation, data);
}
this.callDecode(ctx, this.cumulation, out);
var10 = false;
} catch (DecoderException var11) {
throw var11;
} catch (Exception var12) {
throw new DecoderException(var12);
} finally {
if (var10) {
if (this.cumulation != null && !this.cumulation.isReadable()) {
this.numReads = 0;
this.cumulation.release();
this.cumulation = null;
} else if (++this.numReads >= this.discardAfterReads) {
this.numReads = 0;
this.discardSomeReadBytes();
}
int size = out.size();
this.decodeWasNull = !out.insertSinceRecycled();
fireChannelRead(ctx, out, size);
out.recycle();
}
}
if (this.cumulation != null && !this.cumulation.isReadable()) {
this.numReads = 0;
this.cumulation.release();
this.cumulation = null;
} else if (++this.numReads >= this.discardAfterReads) {
this.numReads = 0;
this.discardSomeReadBytes();
}
int size = out.size();
this.decodeWasNull = !out.insertSinceRecycled();
fireChannelRead(ctx, out, size);
out.recycle();
解析:在這裏,我們找到了用於拼接的cumulation,如下:
ByteBuf data = (ByteBuf)msg;
this.first = this.cumulation == null;
if (this.first) {
this.cumulation = data;
} else {
this.cumulation = this.cumulator.cumulate(ctx.alloc(), this.cumulation, data);
}
解析:並且,這個對象爲null時,直接被替換爲當前輸入的數據,cumulate就是其處理兩個buff拼接的代碼。
接着看整體代碼組織結構
// (略)一些臨時變量
try {
var10 = true;
// (略)這個地方會拋異常
var10 = false;
} catch (DecoderException var11) {
throw var11;
} catch (Exception var12) {
throw new DecoderException(var12);
} finally {
if (var10) {
// (略)某處理1
}
}
// (略)某處理2
解析:從這個結構看,並沒有什麼需要特別關注的,但是,其中的處理1和處理2代碼是完全一致的!!我們來思考一下這裏爲什麼要以這種方式組織代碼,暗藏何種玄機。
將代碼抽象如下:
if (條件1) {
處理1,一旦執行,將不會再滿足條件1
} else if (條件2) {
處理2
}
處理3
結合上述流程組織,在不拋異常時,處理1和處理2只能進入其中一個;而在拋異常時,處理1和處理2可能都會執行。這是這個組織結構想表達的意思。
那麼,爲什麼不將其抽取成一個方法進行調用呢?本人理解如下:
- 代碼簡短,必要性不高,更何況沒有其他方法需要調用類似的代碼
- 湊到一起,並不影響閱讀理解,不必太過拘泥於代碼規範
- 實際上,沒有什麼特別的理由,只是開發人員覺得無所謂,怎麼方便怎麼搞
代碼塊
if (this.cumulation != null && !this.cumulation.isReadable()) {
this.numReads = 0;
this.cumulation.release();
this.cumulation = null;
} else if (++this.numReads >= this.discardAfterReads) {
this.numReads = 0;
this.discardSomeReadBytes();
}
int size = out.size();
this.decodeWasNull = !out.insertSinceRecycled();
fireChannelRead(ctx, out, size);
out.recycle();
解析:
- cumulation有數據時,清空掉
- cumulation無數據時,累加numReads,numReads超過某個閾值時,清空已讀字節,假如缺少清理數據的操作,那麼有很大概率導致cumulation只增不減的情況。體現內存管理細節
- 下半段則是處理輸出數據
Cumulator
接口
public interface Cumulator {
ByteBuf cumulate(ByteBufAllocator var1, ByteBuf var2, ByteBuf var3);
}
實現1(ByteToMessageDecoder使用)
public static final ByteToMessageDecoder.Cumulator MERGE_CUMULATOR = new ByteToMessageDecoder.Cumulator() {
public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
ByteBuf buffer;
if (cumulation.writerIndex() <= cumulation.maxCapacity() - in.readableBytes() && cumulation.refCnt() <= 1 && !cumulation.isReadOnly()) {
buffer = cumulation;
} else {
buffer = ByteToMessageDecoder.expandCumulation(alloc, cumulation, in.readableBytes());
}
buffer.writeBytes(in);
in.release();
return buffer;
}
};
解析:
- 首先是一些容量判斷,容量充足則無需擴容,不足則調用expandCumulation進行擴容
- buffer.writeBytes(in),將內容拼接
static ByteBuf expandCumulation(ByteBufAllocator alloc, ByteBuf cumulation, int readable) {
ByteBuf oldCumulation = cumulation;
cumulation = alloc.buffer(cumulation.readableBytes() + readable);
cumulation.writeBytes(oldCumulation);
oldCumulation.release();
return cumulation;
}
實現2(ByteToMessageDecoder無使用)
public static final ByteToMessageDecoder.Cumulator COMPOSITE_CUMULATOR = new ByteToMessageDecoder.Cumulator() {
public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
Object buffer;
if (cumulation.refCnt() > 1) {
buffer = ByteToMessageDecoder.expandCumulation(alloc, cumulation, in.readableBytes());
((ByteBuf)buffer).writeBytes(in);
in.release();
} else {
CompositeByteBuf composite;
if (cumulation instanceof CompositeByteBuf) {
composite = (CompositeByteBuf)cumulation;
} else {
composite = alloc.compositeBuffer(2147483647);
composite.addComponent(true, cumulation);
}
composite.addComponent(true, in);
buffer = composite;
}
return (ByteBuf)buffer;
}
};
解析:
- 當有多個引用時,直接擴容
- 只有1個引用時,採用CompositeByteBuf去組合數據,相比MERGE_CUMULATOR,composite.addComponent並非值複製,其效率更高。
疑問:
- 爲啥不用COMPOSITE_CUMULATOR而是用MERGE_CUMULATOR?
因爲TCP粘包拆包問題沒那麼頻繁,往往是湊齊一個完整數據包的,採用MERGE_CUMULATOR並沒有什麼嚴重問題。同時拆包問題往往是初始Buff容量不足才導致拆包,而這裏將Buff擴容後,後續發生拆包的可能性就低了。 - 爲啥MERGE_CUMULATOR不用判斷cumulation.refCnt() > 1?
因爲確保了這個條件不會發生,所以不必寫。 - 爲啥COMPOSITE_CUMULATOR不用判斷容量?
因爲傳進來的buff肯定是剛剛好的一小段,不會有多餘的容量,直接擴容即可。 - 既然COMPOSITE_CUMULATOR沒有用到,爲啥還要實現?
因爲setCumulator可以更改實現,某些特殊情形下,或許會可以考慮更改。
上文提到的單元測試中,加入setCumulator,同樣是可以正常運行的。
public TestDecoder() {
setCumulator(COMPOSITE_CUMULATOR);
}
https://blog.csdn.net/a215095167/article/details/104905170
總結
閱讀源碼,最主要一點是不要怕。精心設計的源碼,可讀性是比較強的。如果可讀性差,要麼是自身還需要歷練,學習更多知識,要麼是選用的源碼沒有什麼學習價值。因此,閱讀源碼也是一個自我認識的過程。