Netty解讀源碼ByteToMessageDecoder

感慨

紙上得來終覺淺,源碼閱讀是進一步提高自身水平的手段。但源碼無數,並不是什麼樣的源碼都值得一讀。
須知任何技術都是爲了解決特定問題的,先針對問題進行思考,然後再讀源碼,會事半功倍。
本文按照一定的閱讀源碼思路來逐步解析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,等下次完整了再處理;不能處理的,丟棄掉
    }
}

從上述內容入手,可以解析到信息如下:

  1. 繼承了ChannelInboundHandlerAdapter,必然繼承其channelRead方法,該方法是我們閱讀源碼的入口。
  2. 提供了新的decode方法,將變化的部分交給開發人員自由實現
  3. 不變的部分,主要是byteBuf有保留上一次處理結果的能力,並不是一個一次性消耗的對象,用於針對TCP拆包問題,這是ByteToMessageDecoder的關鍵

思考:

  1. 必然存在某個變量,用於保留處理不完的ByteBuf
  2. 必然有某個方式,將處理不完的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可能都會執行。這是這個組織結構想表達的意思。
那麼,爲什麼不將其抽取成一個方法進行調用呢?本人理解如下:

  1. 代碼簡短,必要性不高,更何況沒有其他方法需要調用類似的代碼
  2. 湊到一起,並不影響閱讀理解,不必太過拘泥於代碼規範
  3. 實際上,沒有什麼特別的理由,只是開發人員覺得無所謂,怎麼方便怎麼搞

代碼塊

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

解析:

  1. cumulation有數據時,清空掉
  2. cumulation無數據時,累加numReads,numReads超過某個閾值時,清空已讀字節,假如缺少清理數據的操作,那麼有很大概率導致cumulation只增不減的情況。體現內存管理細節
  3. 下半段則是處理輸出數據

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

解析:

  1. 首先是一些容量判斷,容量充足則無需擴容,不足則調用expandCumulation進行擴容
  2. 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. 當有多個引用時,直接擴容
  2. 只有1個引用時,採用CompositeByteBuf去組合數據,相比MERGE_CUMULATOR,composite.addComponent並非值複製,其效率更高。

疑問:

  1. 爲啥不用COMPOSITE_CUMULATOR而是用MERGE_CUMULATOR?
    因爲TCP粘包拆包問題沒那麼頻繁,往往是湊齊一個完整數據包的,採用MERGE_CUMULATOR並沒有什麼嚴重問題。同時拆包問題往往是初始Buff容量不足才導致拆包,而這裏將Buff擴容後,後續發生拆包的可能性就低了。
  2. 爲啥MERGE_CUMULATOR不用判斷cumulation.refCnt() > 1?
    因爲確保了這個條件不會發生,所以不必寫。
  3. 爲啥COMPOSITE_CUMULATOR不用判斷容量?
    因爲傳進來的buff肯定是剛剛好的一小段,不會有多餘的容量,直接擴容即可。
  4. 既然COMPOSITE_CUMULATOR沒有用到,爲啥還要實現?
    因爲setCumulator可以更改實現,某些特殊情形下,或許會可以考慮更改。
    上文提到的單元測試中,加入setCumulator,同樣是可以正常運行的。
  public TestDecoder() {
        setCumulator(COMPOSITE_CUMULATOR);
    }

https://blog.csdn.net/a215095167/article/details/104905170

總結

閱讀源碼,最主要一點是不要怕。精心設計的源碼,可讀性是比較強的。如果可讀性差,要麼是自身還需要歷練,學習更多知識,要麼是選用的源碼沒有什麼學習價值。因此,閱讀源碼也是一個自我認識的過程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章