基於 Netty 的可插拔業務通訊協議的實現「1」協議描述及基本消息對象設計

開發工程中,有一個常見的需求:服務端程序和多個客戶端程序經過 TCP 協議進行通訊,通訊雙方需通訊的消息種類衆多,而且客戶端的數量可能有數萬個。爲此,雙方須要約定儘量豐富、靈活的數據幀「數據包」協議,方便後續業務功能的設計。java

本文設計了一種通訊協議,爲壓縮數據量,該協議的數據幀以二進制方式進行傳輸並識別,即其基本單位爲字節,必要時將部分字節流手動轉化爲可讀文本。經過設定功能位來實現豐富的通訊消息類型,而且採用註冊的方式,可方便擴展新的業務消息類型,可靈活地增刪通訊消息對象。採用 Netty 框架保證高併發場景下程序的性能。git

系統總體設計框圖以下:github

系統總體設計框圖

1. 通訊數據幀協議的設計

1.1 數據幀主幀的幀格式

首先給出通用的數據幀格式以下,一個數據幀主幀由:幀識別位、幀功能位、設備號、數據長度、數據體等 5 部分組成。「其實最通用的數據幀只有幀識別位,根據幀識別位肯定幀類型,從而肯定其他四個部分,本文中幀識別位固定,幀格式即固定了」後端

數據幀格式

  • 幀識別位:肯定數據幀的開始,亦肯定本幀的幀類型。
  • 幀功能位:肯定該幀所傳送的消息類型,特定的幀功能位對應特定的數據體。
  • 設備號:設備的識別號,服務端據此識別不一樣的客戶端。
  • 數據長度:數據體所佔用的字節數。
  • 數據體:根據幀功能位,所肯定的需傳輸的具體的消息。

1.2 數據幀子幀的幀格式

數據幀除數據體之外的部分稱爲幀頭,考慮這樣一種需求,若是某幀所要傳輸的數據體部份內容不多,致使一個幀的大部分容量均被幀頭佔據,致使有效數據的佔比很小,這就產生了巨大的浪費,舉例以下:服務器

  • 如一個開鎖幀,只需傳輸一個開鎖信號便可,消息的接收方、消息類型均體如今了幀頭中,數據部分只須要 0 個或 1 個字節便可。
  • 客戶端須要向服務器發送本身的當前狀態信息,該狀態信息可能也只須要 1 個字節左右。

因爲如上實際的需求,若是增大了每一幀的有效數據的佔比,整個通訊鏈路的數據量會明顯減小,IO 負擔也會所以減輕,因此據此繼續對幀協議進行設計。併發

數據幀中子幀格式

如上圖,對數據幀主幀中的「數據體」部分進行進一步拆分,數據幀主幀的數據體部分由子幀組成,子幀由:子幀功能位、數據長度、數據體等 3 部分組成。框架

  • 子幀功能位:肯定該子幀所傳送的消息類型,總而言之,主幀、子幀功能位共同肯定了該子幀的消息類型。
  • 數據長度:數據體所佔用的字節數。
  • 數據體:根據子幀功能位,所肯定的需傳輸的具體的消息。

1.3 數據幀的幀格式總覽

完整的幀格式以下圖所示,數據幀主幀的數據體部分徹底由子幀組成,通訊雙方通訊時,能夠往一個主幀中添加多個子幀,從而能夠極大提升鏈路的使用效率。高併發

數據幀的幀格式總覽

2 數據幀處理模塊的實現

數據幀已進行了如上精心設計,將設計的數據幀經過程序實現並投入實際使用纔是最終目的。性能

2.1 數據幀處理的基本方法

以服務端的工做爲例來進行說明。服務端程序監聽指定端口,客戶端經過 TCP 協議向服務器發送二進制數據消息,服務端接收到二進制數據並進行處理,此處採用責任鏈模式,Netty 框架內建了方便的基於責任鏈模式的消息處理方法:this

  1. 第一個處理器將捕獲的數據截取爲一個一個協議約定的數據幀並送入下層處理器,若是捕獲的二進制數據未符合協議約定的格式,則能夠直接丟棄。「此處未考慮半包、粘包等場景」
  2. 第二個處理器捕獲到約定的數據幀,則着手對不一樣類型數據幀進行解析,解析爲不一樣類型的 Java 消息對象,並將反序列化成功並驗證成功的 Java 對象送入下層處理器。若是上述過程失敗,能夠認爲客戶端設計不合理,致使出現無效消息,直接丟棄該對象,也能夠繼續通知服務端或客戶端該異常狀況。
  3. 第三個處理器捕獲到正確的 Java 消息對象,則能夠直接送入上層 Java 模塊進行處理,此處可根據不一樣的對象類型送入不一樣的上層處理模塊,或者在此處進行其餘的工做「好比消息日誌記錄工做等」。

2.2 基本 Java 消息對象的設計

Java 消息對象的設計主要由兩部分組成:

  • 特定數據幀對應的特定 Java 消息對象。
  • 特定 Java 消息對象對應的特定的該消息對象編解碼器。

如下是基本 Java 消息對象:

public abstract class BaseMsg implements Cloneable {

    private final BaseMsgCodec msgCodec;
    private int groupId;
    private int deviceId;
    private int resendTimes = 0;

    protected BaseMsg(BaseMsgCodec msgCodec, int groupId, int deviceId) {
        this.msgCodec = msgCodec;
        this.groupId = groupId;
        this.deviceId = deviceId;
    }

    /**
     * 獲取該消息對象的細節描述
     *
     * @return 該消息對象的細節描述
     */
    public String msgDetailToString() {
        return msgCodec.getDetail() +
                "[majorMsgId=" + Integer.toHexString(msgCodec.getMajorMsgId()).toUpperCase() +
                ", subMsgId=" + Integer.toHexString(msgCodec.getSubMsgId()).toUpperCase() +
                ", groupId=" + groupId +
                ", deviceId=" + deviceId + ']';
    }

    /**
     * 重發該消息對象的記錄信息更新
     */
    public void doResend() {
        resendTimes++;
    }
}

由上述代碼可知,每一個消息對象均包含該對象對應編解碼器的引用,方便獲取該消息對象的擴展信息,或者方便將該消息對象從新序列化爲數據幀。該類包含上節數據幀主幀及子幀的全部公共信息,僅僅未包含子幀中的數據體信息,該需求由基本 Java 消息對象的子類實現。

該類由 abstract 修飾,是抽象類,沒法直接實例化,具體的工做由該類的子類完成,即由具體的真正業務相關的 Java 消息對象完成。

如下爲 Java 消息對象的基本編解碼器:

/**
 * 單個消息對象「幀」的編解碼器
 */
public abstract class BaseMsgCodec implements SubFramecoder, SubFramedecoder {

    private final int majorMsgId;
    private final int subMsgId;
    private final String detail;

    protected BaseMsgCodec(int majorMsgId, int subMsgId, String detail) {
        this.majorMsgId = majorMsgId;
        this.subMsgId = subMsgId;
        this.detail = detail;
    }

    public String getDetail() {
        return detail;
    }

    public int getMajorMsgId() {
        return majorMsgId;
    }

    public int getSubMsgId() {
        return subMsgId;
    }
}

由上述代碼可知,特定 Java 消息對象的編解碼器由數據幀的主幀、子幀功能位共同決定,這樣確保了消息編解碼器的規範,避免消息過多時的混亂。

Java 編解碼器實現了以下兩個接口,代表編解碼器可將 Java 消息對象編碼爲數據幀,或將數據幀解碼爲指定的 Java 消息對象:

public interface SubFramecoder {
    /**
     * 將 Java 消息對象編碼爲數據幀
     *
     * @param msg    消息對象
     * @param buffer TCP 數據幀的容器
     * @return 生成的 TCP 數據幀的 ByteBuf
     */
    ByteBuf code(BaseMsg msg, ByteBuf buffer);
}

public interface SubFramedecoder {
    /**
     * 將數據幀解碼爲指定的 Java 消息對象
     *
     * @param groupId  設備組 ID
     * @param deviceId 設備 ID
     * @param data     幀數據
     * @return 特定的 Java 消息對象
     */
    BaseMsg decode(int groupId, int deviceId, byte[] data);
}

相關項目參考「GitHub 項目基礎框架開源」

  1. Java & Vue.js「集羣設備管理雲平臺『後端部分』」
  2. 基於 Vue.js 2.0 & Element 2.0 的集羣設備管理雲平臺
相關文章
相關標籤/搜索