開發工程中,有一個常見的需求:服務端程序和多個客戶端程序經過 TCP 協議進行通訊,通訊雙方需通訊的消息種類衆多,而且客戶端的數量可能有數萬個。爲此,雙方須要約定儘量豐富、靈活的數據幀「數據包」協議,方便後續業務功能的設計。java
本文設計了一種通訊協議,爲壓縮數據量,該協議的數據幀以二進制方式進行傳輸並識別,即其基本單位爲字節,必要時將部分字節流手動轉化爲可讀文本。經過設定功能位來實現豐富的通訊消息類型,而且採用註冊的方式,可方便擴展新的業務消息類型,可靈活地增刪通訊消息對象。採用 Netty 框架保證高併發場景下程序的性能。git
系統總體設計框圖以下:github
首先給出通用的數據幀格式以下,一個數據幀主幀由:幀識別位、幀功能位、設備號、數據長度、數據體等 5 部分組成。「其實最通用的數據幀只有幀識別位,根據幀識別位肯定幀類型,從而肯定其他四個部分,本文中幀識別位固定,幀格式即固定了」後端
數據幀除數據體之外的部分稱爲幀頭,考慮這樣一種需求,若是某幀所要傳輸的數據體部份內容不多,致使一個幀的大部分容量均被幀頭佔據,致使有效數據的佔比很小,這就產生了巨大的浪費,舉例以下:服務器
因爲如上實際的需求,若是增大了每一幀的有效數據的佔比,整個通訊鏈路的數據量會明顯減小,IO 負擔也會所以減輕,因此據此繼續對幀協議進行設計。併發
如上圖,對數據幀主幀中的「數據體」部分進行進一步拆分,數據幀主幀的數據體部分由子幀組成,子幀由:子幀功能位、數據長度、數據體等 3 部分組成。框架
完整的幀格式以下圖所示,數據幀主幀的數據體部分徹底由子幀組成,通訊雙方通訊時,能夠往一個主幀中添加多個子幀,從而能夠極大提升鏈路的使用效率。高併發
數據幀已進行了如上精心設計,將設計的數據幀經過程序實現並投入實際使用纔是最終目的。性能
以服務端的工做爲例來進行說明。服務端程序監聽指定端口,客戶端經過 TCP 協議向服務器發送二進制數據消息,服務端接收到二進制數據並進行處理,此處採用責任鏈模式,Netty 框架內建了方便的基於責任鏈模式的消息處理方法:this
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);
}
複製代碼