【JAVA 網路程式設計系列】Netty -- 基本編解碼方式的支援

奮鬥企鵝CopperSun發表於2020-10-02

【JAVA 網路程式設計系列】Netty -- 基本編解碼方式的支援

【1】Netty 針對粘包/半包問題的解決方案

【1.1】粘包 / 半包問題的概念

【1.2】粘包 / 半包問題出現的原因

粘包 / 半包問題出現的根本原因TCP 是流式協議,訊息無邊界;

【1.3】解決方案與 Netty 支援

【1.3.1】定長法

方案簡介,固定長度確定訊息的邊界,比如傳輸的訊息分別為 ABC、D、EF;則根據最長的那條訊息即 ABC 的長度(3) 為固定長度,不足的補足;該方式最大的缺點就是浪費空間;

Netty 支援 -- FixedLengthFrameDecoder

// 處理 TCP 粘包與半包問題,固定長度解碼
public class FixedLengthFrameDecoder extends ByteToMessageDecoder {

    // 成員變數 frameLength 在建構函式中初始化
    public FixedLengthFrameDecoder(int frameLength) {
        // 初始化了幀的固定長度值
        this.frameLength = frameLength;
    }

    // 執行解碼操作
    protected Object decode(
            @SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) 
throws Exception {
        // 若 ByteBuf 中可以讀取的資料長度小於解碼的固定長度 frameLength,直接返回 null
        if (in.readableBytes() < frameLength) {
            return null;
        } else {
            // 繼續執行固定長度解碼操作,從 ByteBuf 中讀取 frameLength 長度的資料
            return in.readRetainedSlice(frameLength);
        }
    }

}

【1.3.2】分割符法

方案簡介,使用固定的分割符分割訊息,比如傳輸的訊息分別為 ABC、DEFG、HI\n,若使用 \n 作為分割符,就在訊息的邊界處加一個 \n 作為分割符,這樣接收方拿到訊息之後使用 \n 去分割訊息即可;該方式的缺點,1. 若分割符本身作為傳輸內容時要轉義,2. 需要掃描訊息的內容才能確定訊息的邊界位置;

Netty 支援 -- DelimiterBasedFrameDecoder

DelimiterBasedFrameDecoder 構造方法

// 處理 TCP 粘包與半包問題,分隔符解碼器
public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder {

    // maxFrameLength 解碼後幀的最大長度
    // stripDelimiter 解碼幀是否應去掉分隔符
    // failFast
    //      若設定為 true 則一旦讀取的資料長度大於最大幀長度的定義,則無論所有資料是否讀取完畢立即丟擲異常
    //      若設定為 false 則一旦讀取的資料長度大於最大幀長度的定義,則等到所有資料讀取完畢才會丟擲異常
    // delimiters 分隔符
    public DelimiterBasedFrameDecoder(
            int maxFrameLength, boolean stripDelimiter, boolean failFast, ByteBuf... delimiters) {

        // 驗證最大幀長的有效應
        validateMaxFrameLength(maxFrameLength);
        // 以下程式碼用於判斷 delimiters 的合法性
        if (delimiters == null) {
            throw new NullPointerException("delimiters");
        }
        if (delimiters.length == 0) {
            throw new IllegalArgumentException("empty delimiters");
        }
        // isLineBased 判斷分割符是否為 "\n" 或 "\r\n"
        // isSubclass 判斷解碼器是否為 DelimiterBasedFrameDecoder 的子類
        if (isLineBased(delimiters) && !isSubclass()) {
            // 若分割符為 "\n" 或 "\r\n" 並且解碼器不是 DelimiterBasedFrameDecoder 的子類
            // 則新建 LineBasedFrameDecoder 解碼器
            // LineBasedFrameDecoder 解碼器將根據 "\n" 或 "\r\n" 對幀資料進行解碼
            lineBasedDecoder = new LineBasedFrameDecoder(maxFrameLength, stripDelimiter, failFast);
            this.delimiters = null;
        } else {
            // 新建分隔符緩衝區
            this.delimiters = new ByteBuf[delimiters.length];
            for (int i = 0; i < delimiters.length; i ++) {
                ByteBuf d = delimiters[i];
                // 驗證位元組的有效應
                validateDelimiter(d);
                // 獲取分隔符
                this.delimiters[i] = d.slice(d.readerIndex(), d.readableBytes());
            }
            lineBasedDecoder = null;
        }
        // maxFrameLength 解碼後幀的最大長度
        this.maxFrameLength = maxFrameLength;
        // stripDelimiter 解碼幀是否應去掉分隔符
        this.stripDelimiter = stripDelimiter;
        // failFast 是否快速失敗處理
        this.failFast = failFast;
    }

}

DelimiterBasedFrameDecoder 的解碼方法

// 處理 TCP 粘包與半包問題,分隔符解碼器
public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder {

    // 具體的解碼操作
    protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
        // 判斷是否使用 LineBasedFrameDecoder 器
        if (lineBasedDecoder != null) {
            // 直接使用 LineBasedFrameDecoder 解碼器解碼
            // 並返回解碼結果
            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;
            }
        }

        if (minDelim != null) {
            // 讀取到分割符的情況
            int minDelimLength = minDelim.capacity();
            ByteBuf frame;
            // discardingTooLongFrame 丟棄過長幀處理標誌
            if (discardingTooLongFrame) {
                // discardingTooLongFrame 為 true 表明持續丟棄過長幀處理中
                // 此時讀取到分隔符表明可以重新讀取資料了
                // We've just finished discarding a very large frame.
                // Go back to the initial state.
                discardingTooLongFrame = false;
                // 丟棄已讀取的資料
                buffer.skipBytes(minFrameLength + minDelimLength);
                // 復位相關引數
                int tooLongFrameLength = this.tooLongFrameLength;
                this.tooLongFrameLength = 0;
                if (!failFast) {
                    // 此時說明可以重新讀取資料了,丟擲異常
                    fail(tooLongFrameLength);
                }
                return null;
            }
            // minFrameLength 從讀取的緩衝資料中依據分割符擷取的最小幀的長度
            // maxFrameLength 解碼後幀的最大允許長度
            if (minFrameLength > maxFrameLength) {
                // 條件成立表明資料解碼失敗
                // Discard read frame.
                // 已讀資料緩衝區中丟棄已經讀取的幀
                buffer.skipBytes(minFrameLength + minDelimLength);
                // 丟擲異常
                fail(minFrameLength);
                return null;
            }

            if (stripDelimiter) {
                // 從當前索引重新讀取一段長度為 minFrameLength 的資料
                frame = buffer.readRetainedSlice(minFrameLength);
                // 讀取的 buffer 跳過分割符的長度
                buffer.skipBytes(minDelimLength);
            } else {
                // 讀取包含分隔符的資料幀
                frame = buffer.readRetainedSlice(minFrameLength + minDelimLength);
            }
            // 返回解碼後的幀資料
            return frame;
        } else {
            // 處理分割符為 null 的情況
            if (!discardingTooLongFrame) {
                // 過長幀標誌為 false 的情況
                if (buffer.readableBytes() > maxFrameLength) {
                    // 讀取的資料長度大於最大幀長卻沒有找到分割符表明該段讀取的資料無效需要丟棄
                    // Discard the content of the buffer until a delimiter is found.
                    tooLongFrameLength = buffer.readableBytes();
                    // 丟棄當前讀取的資料
                    buffer.skipBytes(buffer.readableBytes());
                    // 過長幀標誌置位表明丟棄過幀的處理操作正在處理中
                    discardingTooLongFrame = true;
                    if (failFast) {
                        // 快速結束則立馬丟擲異常
                        fail(tooLongFrameLength);
                    }
                }
            } else {
                // 未讀取到分隔符並且
                // 過長幀標誌為 true 的情況表明需要持續丟棄過長幀
                // 則繼續丟棄讀取到的資料
                // Still discarding the buffer since a delimiter is not found.
                tooLongFrameLength += buffer.readableBytes();
                buffer.skipBytes(buffer.readableBytes());
            }
            return null;
        }
    }

}

LineBasedFrameDecoder 的構造方法

public class LineBasedFrameDecoder extends ByteToMessageDecoder {

    public LineBasedFrameDecoder(final int maxLength, final boolean stripDelimiter, 
final boolean failFast) {
        // 解碼後的最大幀長
        this.maxLength = maxLength;
        // 標記是否需要快速失敗
        this.failFast = failFast;
        // 標記是否需要保留分隔符
        this.stripDelimiter = stripDelimiter;
    }

}

 LineBasedFrameDecoder 的解碼方法

public class LineBasedFrameDecoder extends ByteToMessageDecoder {

    protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
        // 確定 buffer 中的一行資料索引值
        final int eol = findEndOfLine(buffer);
        // discarding
        // True if we're discarding input because we're already over maxLength
        // True 表明正在進行過長幀丟棄操作
        if (!discarding) {
            // discarding == false,未處於過長幀丟棄操作中
            if (eol >= 0) {
                // 在 buffer 中確定了完整的一行,在此類中表明存在 \r\n 或 \n 的分割符
                final ByteBuf frame;
                final int length = eol - buffer.readerIndex();
                // 確定分隔符的長度
                final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;

                if (length > maxLength) {
                    // 依據分隔符解碼的幀長大於最大幀長,表明資料有誤
                    // 重新設定 readerIndex 表明丟棄已讀取的資料
                    buffer.readerIndex(eol + delimLength);
                    // 丟擲異常
                    fail(ctx, length);
                    return null;
                }
                // 根據 stripDelimiter 標記刪除解碼幀中的分隔符
                if (stripDelimiter) {
                    // 丟棄分割符
                    frame = buffer.readRetainedSlice(length);
                    buffer.skipBytes(delimLength);
                } else {
                    // 保留分隔符
                    frame = buffer.readRetainedSlice(length + delimLength);
                }
                // 返回解碼後的資料
                return frame;
            } else {
                // 沒有找到分割符
                final int length = buffer.readableBytes();
                if (length > maxLength) {
                    // 已讀取的資料大於最大幀長則刪除資料並丟擲異常
                    discardedBytes = length;
                    // 刪除所有已經讀取的資料
                    buffer.readerIndex(buffer.writerIndex());
                    // 幀過長標記置位
                    discarding = true;
                    offset = 0;
                    if (failFast) {
                        // 是否需要快速失敗處理
                        fail(ctx, "over " + discardedBytes);
                    }
                }
                return null;
            }
        } else {
            // 仍處在幀過長的處理過程中
            if (eol >= 0) {
                // 發現分隔符,此時表明可以重新讀取資料
                final int length = discardedBytes + eol - buffer.readerIndex();
                final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
                // 將讀取的資料刪除
                buffer.readerIndex(eol + delimLength);
                discardedBytes = 0;
                discarding = false;
                if (!failFast) {
                    // 表明可以重新讀取了,丟擲異常
                    fail(ctx, length);
                }
            } else {
                // 未發現分隔符繼續讀取資料,更新待刪除的資料長度
                discardedBytes += buffer.readableBytes();
                // 未發現分隔符繼續讀取資料
                buffer.readerIndex(buffer.writerIndex());
            }
            return null;
        }
    }

}

【1.3.3】長度 + 內容法

方案簡介,使用固定的位元組數儲存訊息的長度,後面跟上訊息的內容,讀取訊息的時候先讀取長度,再一次性把訊息的內容讀取出來;比如,傳輸的訊息分別為 ABC、DEFG、HI;則在訊息前面分別加上長度一起傳輸,後面再跟上內容,達到區分資料塊的目的;該方式的缺點是需要預先知道訊息的最大長度;

Netty 支援 -- LengthFieldBasedFrameDecoder

LengthFieldBasedFrameDecoder 處理的情況

/**
 * <pre>
 * <b>lengthFieldOffset</b>   = <b>0</b>
 * <b>lengthFieldLength</b>   = <b>2</b>
 * lengthAdjustment    = 0
 * initialBytesToStrip = 0 (= do not strip header)
 *
 * BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)
 * +--------+----------------+      +--------+----------------+
 * | Length | Actual Content |----->| Length | Actual Content |
 * | 0x000C | "HELLO, WORLD" |      | 0x000C | "HELLO, WORLD" |
 * +--------+----------------+      +--------+----------------+
 * </pre>
 *
 * <pre>
 * lengthFieldOffset   = 0
 * lengthFieldLength   = 2
 * lengthAdjustment    = 0
 * <b>initialBytesToStrip</b> = <b>2</b> (= the length of the Length field)
 *
 * BEFORE DECODE (14 bytes)         AFTER DECODE (12 bytes)
 * +--------+----------------+      +----------------+
 * | Length | Actual Content |----->| Actual Content |
 * | 0x000C | "HELLO, WORLD" |      | "HELLO, WORLD" |
 * +--------+----------------+      +----------------+
 * </pre>
 *
 * <pre>
 * lengthFieldOffset   =  0
 * lengthFieldLength   =  2
 * <b>lengthAdjustment</b>    = <b>-2</b> (= the length of the Length field)
 * initialBytesToStrip =  0
 *
 * BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)
 * +--------+----------------+      +--------+----------------+
 * | Length | Actual Content |----->| Length | Actual Content |
 * | 0x000E | "HELLO, WORLD" |      | 0x000E | "HELLO, WORLD" |
 * +--------+----------------+      +--------+----------------+
 * </pre>
 *
 * <pre>
 * <b>lengthFieldOffset</b>   = <b>2</b> (= the length of Header 1)
 * <b>lengthFieldLength</b>   = <b>3</b>
 * lengthAdjustment    = 0
 * initialBytesToStrip = 0
 *
 * BEFORE DECODE (17 bytes)                      AFTER DECODE (17 bytes)
 * +----------+----------+----------------+      +----------+----------+----------------+
 * | Header 1 |  Length  | Actual Content |----->| Header 1 |  Length  | Actual Content |
 * |  0xCAFE  | 0x00000C | "HELLO, WORLD" |      |  0xCAFE  | 0x00000C | "HELLO, WORLD" |
 * +----------+----------+----------------+      +----------+----------+----------------+
 * </pre>
 *
 * <pre>
 * lengthFieldOffset   = 0
 * lengthFieldLength   = 3
 * <b>lengthAdjustment</b>    = <b>2</b> (= the length of Header 1)
 * initialBytesToStrip = 0
 *
 * BEFORE DECODE (17 bytes)                      AFTER DECODE (17 bytes)
 * +----------+----------+----------------+      +----------+----------+----------------+
 * |  Length  | Header 1 | Actual Content |----->|  Length  | Header 1 | Actual Content |
 * | 0x00000C |  0xCAFE  | "HELLO, WORLD" |      | 0x00000C |  0xCAFE  | "HELLO, WORLD" |
 * +----------+----------+----------------+      +----------+----------+----------------+
 * </pre>
 *
 * <pre>
 * lengthFieldOffset   = 1 (= the length of HDR1)
 * lengthFieldLength   = 2
 * <b>lengthAdjustment</b>    = <b>1</b> (= the length of HDR2)
 * <b>initialBytesToStrip</b> = <b>3</b> (= the length of HDR1 + LEN)
 *
 * BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)
 * +------+--------+------+----------------+      +------+----------------+
 * | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
 * | 0xCA | 0x000C | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |
 * +------+--------+------+----------------+      +------+----------------+
 * </pre>
 *
 * <pre>
 * lengthFieldOffset   =  1
 * lengthFieldLength   =  2
 * <b>lengthAdjustment</b>    = <b>-3</b> (= the length of HDR1 + LEN, negative)
 * <b>initialBytesToStrip</b> = <b> 3</b>
 *
 * BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)
 * +------+--------+------+----------------+      +------+----------------+
 * | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
 * | 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |
 * +------+--------+------+----------------+      +------+----------------+
 * </pre>    
 */

LengthFieldBasedFrameDecoder 的構造方法

public class LengthFieldBasedFrameDecoder extends ByteToMessageDecoder {

    public LengthFieldBasedFrameDecoder(
            ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,
            int lengthAdjustment, int initialBytesToStrip, boolean failFast) {

        // byteOrder 長度位元組段的位元組序
        this.byteOrder = byteOrder;
        // maxFrameLength 解碼後幀的最大長度
        this.maxFrameLength = maxFrameLength;
        // lengthFieldOffset 長度位元組段的開始的偏移量
        this.lengthFieldOffset = lengthFieldOffset;
        // lengthFieldLength 長度位元組段的長度
        this.lengthFieldLength = lengthFieldLength;
        // lengthAdjustment 長度位元組段的補償值
        this.lengthAdjustment = lengthAdjustment;
        // lengthFieldEndOffset 長度位元組段的結束的偏移量
        lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength;
        // initialBytesToStrip 解碼後幀的初始部分的長度
        this.initialBytesToStrip = initialBytesToStrip;
        // failFast 標記是否進行快速失敗處理
        this.failFast = failFast;

    }

}

LengthFieldBasedFrameDecoder 的解碼方法

public class LengthFieldBasedFrameDecoder extends ByteToMessageDecoder {

    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        // discardingTooLongFrame 標記是否處於丟棄過長幀的處理過程中
        if (discardingTooLongFrame) {
            // 丟棄過長幀
            discardingTooLongFrame(in);
        }
        if (in.readableBytes() < lengthFieldEndOffset) {
            // 幀長度資料段未讀取完畢,直接返回
            return null;
        }
        // 獲取幀長度欄位的在 ByteBuf 中實際的起始偏移量
        int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;
        // 獲取幀長度值
        long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, 
lengthFieldLength, byteOrder);

        if (frameLength < 0) {
            // 幀長度值不能為負數,否則丟棄該幀並丟擲異常
            failOnNegativeLengthField(in, frameLength, lengthFieldEndOffset);
        }
        // 新增補償值確定實際幀資料的 StartOffset
        frameLength += lengthAdjustment + lengthFieldEndOffset;

        if (frameLength < lengthFieldEndOffset) {
            // 實際幀資料的 StartOffset 不可能小於 lengthFieldEndOffset
            failOnFrameLengthLessThanLengthFieldEndOffset(in, frameLength, lengthFieldEndOffset);
        }

        if (frameLength > maxFrameLength) {
            // 幀過長,進行丟棄過長幀處理
            exceededFrameLength(in, frameLength);
            return null;
        }

        // never overflows because it's less than maxFrameLength
        // maxFrameLength 為 int 型變數
        int frameLengthInt = (int) frameLength;
        if (in.readableBytes() < frameLengthInt) {
            // buffer 中讀取到的資料量小於幀長
            return null;
        }

        if (initialBytesToStrip > frameLengthInt) {
            // initialBytesToStrip 表示需要跳過的幀的初始部分
            // 條件成立表明實際幀資料的 StartOffset 在需要跳過的幀的初始部分的前面
            // 幀出現錯誤
            failOnFrameLengthLessThanInitialBytesToStrip(in, frameLength, initialBytesToStrip);
        }
        // 丟棄初始部分
        in.skipBytes(initialBytesToStrip);

        // extract frame,提取幀
        // 快取 buffer 的讀索引
        int readerIndex = in.readerIndex();
        // 去掉初始部分的實際幀的長度
        int actualFrameLength = frameLengthInt - initialBytesToStrip;
        // 提取幀
        ByteBuf frame = extractFrame(ctx, in, readerIndex, actualFrameLength);
        // 重新設定 buffer 的讀索引
        in.readerIndex(readerIndex + actualFrameLength);
        return frame;
    }

}

【1.3.4】Netty 中解碼器的繼承結構

【2】Netty 針對二次編解碼的解決方案

【2.1】一次解碼與二次解碼的作用

一次解碼主要用於解決粘包/半包的問題,將緩衝區中的位元組陣列按照協議本身的格式進行分割,分割後的資料仍是位元組陣列;二次解碼主要用於將位元組陣列轉換成 Java 物件,然後進入到相應的 Handler 中進行具體的業務邏輯處理;一次解碼與二次解碼的流程如下,其中主要運用了運用了分層的思想進行解耦,雖然一次解碼與二次解碼可以合併但不建議

【2.2】Netty 對一次編解碼與二次編解碼的處理

Netty 提供一次編解碼基類 MessageToByteEncoder/ByteToMessageDecoder 以及二次編解碼基類 MessageToMessageEncoder/MessageToMessageDecoder;通常,繼承自 MessageToByteEncoder/ByteToMessageDecoder 類的就是一次編解碼,繼承自 MessageToMessageEncoder/MessageToMessageDecoder 類的就是二次編解碼;

【2.3】Netty 中支援的二次編解碼方式

序列化方式優點缺點
serialization(優化過的 Java 序列化)Java 原生,使用方便報文太大,不便於閱讀,只能 Java 使用
json結構清晰,便於閱讀,效率較高,跨語言報文較大
protobuf使用方便,效率很高,報文很小,跨語言不便於閱讀

附錄

參考致謝

本部落格為博主學習筆記,同時參考了網上眾博主的博文以及相關專業書籍,在此表示感謝,本文若存在不足之處,請批評指正。

【1】慕課專欄,網路程式設計之Netty一站式精講

【2】極客時間,Netty原始碼剖析與實戰

相關文章