Netty – 粘包和半包(下)
- 2019 年 11 月 3 日
- 筆記
今天重點來看一下 Netty 是如何實現封裝成幀(Framing)方案的。
解碼核心流程
之前介紹過三種解碼器FixedLengthFrameDecoder
、DelimiterBasedFrameDecoder
、LengthFieldBasedFrameDecoder
,它們都繼承自ByteToMessageDecoder
,而ByteToMessageDecoder
繼承自ChannelInboundHandlerAdapter
,其核心方法為channelRead
。因此,我們來看看ByteToMessageDecoder
的channelRead
方法:
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof ByteBuf) { CodecOutputList out = CodecOutputList.newInstance(); try { // 將傳入的消息轉化為data ByteBuf data = (ByteBuf) msg; // 最終實現的目標是將數據全部放進cumulation中 first = cumulation == null; // 第一筆數據直接放入 if (first) { cumulation = data; } else { // 不是第一筆數據就進行追加 cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data); } // 解碼 callDecode(ctx, cumulation, out); } // 以下程式碼省略,因為不屬於解碼過程 }
再來看看callDecode
方法:
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { try { while (in.isReadable()) { int outSize = out.size(); if (outSize > 0) { // 以下程式碼省略,因為初始狀態時,outSize 只可能是0,不可能進入這裡 } int oldInputLength = in.readableBytes(); // 在進行 decode 時,不執行handler的remove操作。 // 只有當 decode 執行完之後,開始清理數據。 decodeRemovalReentryProtection(ctx, in, out); // 省略以下程式碼,因為後面的內容也不是解碼的過程
再來看看decodeRemovalReentryProtection
方法:
final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { // 設置當前狀態為正在解碼 decodeState = STATE_CALLING_CHILD_DECODE; try { // 解碼 decode(ctx, in, out); } finally { // 執行hander的remove操作 boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING; decodeState = STATE_INIT; if (removePending) { handlerRemoved(ctx); } } } // 子類都重寫了該方法,每種實現都會有自己特殊的解碼方式 protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
從上面的過程可以總結出,在解碼之前,需要先將數據寫入cumulation
,當解碼結束後,需要通過 handler 進行移除。
具體解碼過程
剛剛說到decode
方法在子類中都有實現,那針對我們說的三種解碼方式,一一看其實現。
FixedLengthFrameDecoder
其源碼為:
@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 Object decode( @SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception { // 收集到的數據是否小於固定長度,小於就代表無法解析 if (in.readableBytes() < frameLength) { return null; } else { return in.readRetainedSlice(frameLength); } }
就和這個類的名字一樣簡單,就是固定長度進行解碼,因此,在設置該解碼器的時候,需要在構造方式里傳入frameLength
。
DelimiterBasedFrameDecoder
其源碼為:
@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 Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception { // 當前的分割符是否是換行分割符(n或者rn) if (lineBasedDecoder != null) { return lineBasedDecoder.decode(ctx, buffer); } // Try all delimiters and choose the delimiter which yields the shortest frame. int minFrameLength = Integer.MAX_VALUE; ByteBuf minDelim = null; // 其他分割符進行一次切分 for (ByteBuf delim: delimiters) { int frameLength = indexOf(buffer, delim); if (frameLength >= 0 && frameLength < minFrameLength) { minFrameLength = frameLength; minDelim = delim; } } // 以下程式碼省略
根據它的名字可以知道,分隔符才是它的核心。它將分割符分成兩類,只有換行分割符(n或者rn)
和其他
。因此,需要注意的是,你可以定義多種分割符,它都是支援的。
LengthFieldBasedFrameDecoder
該類比較複雜,如果直接看方法容易把自己看混亂,因此我準備結合類上的解釋,先看看其私有變數。
2 bytes length field at offset 1 in the middle of 4 bytes header, strip the first header field and the length field, the length field represents the length of the
whole message
Let's give another twist to the previous example. The only difference from the previous example is that the length field represents the length of the whole message instead of the message body, just like the third example. We have to count the length of HDR1 and Length into lengthAdjustment. Please note that we don't need to take the length of HDR2 into account because the length field already includes the whole header length.
* BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes) * +------+--------+------+----------------+ +------+----------------+ * | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content | * | 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" | * +------+--------+------+----------------+ +------+----------------+
lengthFieldOffset
: 該欄位代表 Length 欄位是從第幾個位元組開始的。上面的例子里,Length 欄位是從第1個位元組開始(HDR1 是第0個位元組),因此該值即為0。
lengthFieldLength
: 該欄位代表 Length 欄位所佔用的位元組數。上面的例子里,Length 欄位佔用2個位元組,因此該值為2。
lengthAdjustment
: 該欄位代表 Length 欄位結束位置到真正的內容開始位置的距離。上面例子里,因為 Length 欄位的含義是整個消息(包括 HDR1、Length、HDR2、Actual Content,一般 Length 指的只是 Actual Content),所以 Length 末尾到真正的內容開始位置(HDR1的開始處),相當於減少3個位元組,所以是-3。
initialBytesToStrip
: 展示時需要從 Length 欄位末尾開始跳過幾個位元組。上面例子里,因為真正的內容是從 HDR1 開始的,最終展示的內容是從 HDR2 開始的,所以中間差了3個位元組,所以該值是3。
該類的解碼方法比較複雜,有興趣的同學可以試著自己分析一下。
總結
這一篇主要是結合 Netty 里的源程式碼講解了 Netty 中封裝成幀(Framing)的三種方式,相信你一定有了不一樣的理解。