本文爲該系列的第二篇文章,設計需求爲:服務端程序和衆多客戶端程序經過 TCP 協議進行通訊,通訊雙方需通訊的消息種類衆多。上一篇文章詳細描述了該通訊協議的二進制數據幀格式以及基本 Java 消息類,假設通訊雙方「服務端、客戶端」均由 Netty 框架構建而成,雙方在程序內部使用 Java 消息對象,通訊雙方信息交互採用的是自定義二進制幀格式,本文經過一個具體實例,探討指定的 Java 消息對象與其相應的二進制數據幀相互轉換的方法。java
本小節以一個具體的需求爲例,講述該自定義通訊協議的工做流程。該需求爲:對某一個特定的客戶端進行命名。該需求的具體工做流程描述以下:數據庫
服務端需主動向指定的客戶端發送消息,對客戶端設置指定的名稱,客戶端接到指定的消息並驗證合法後,需向服務端反饋消息接受成功的確認回覆,服務器接收到該回復後,便可認爲對客戶端進行命名的消息發送成功而且名字設置成功,若服務端在指定的時間內未收到回覆,需進行重發或者向上層「如管理員或數據庫」反饋該客戶端的異常。服務器
上述過程使用 UML 序列圖演示以下:架構
由上圖能夠直觀地看出:管理員對服務器的操做以及服務器對管理員的反饋均爲動做,Server 與 Client 之間的通訊以 Java 的視角均經過 Java 消息對象,共需兩個對象:客戶端別名設置對象、客戶端別名設置回覆對象。而實際二者之間的通訊使用的是基於 TCP 的自定義二進制數據幀,對象與數據幀之間需進行轉換。框架
上小節所述過程須要兩個 Java 消息類,以下所示:ide
/** * 「消息對象」客戶端別名設置 */ public class MsgDeviceName extends BaseMsg { private final String name; public MsgDeviceName(BaseMsgCodec msgCodec, int groupId, int deviceId, String name) { super(msgCodec, groupId, deviceId); this.name = name; } public String getName() { return name; } @Override public String msgDetailToString() { return super.msgDetailToString() + "別名:" + name; } }
/** * 「消息對象」通用消息回覆 */ public class MsgReplyNormal extends BaseMsg { public MsgReplyNormal(BaseMsgCodec msgCodec, int groupId, int deviceId) { super(msgCodec, groupId, deviceId); } @Override public String msgDetailToString() { return super.msgDetailToString(); } }
客戶端別名設置類相比於基礎消息類,覆寫了消息細節描述方法,優化調試日誌的使用體驗。主要改變是,僅僅增長了客戶端別名的引用及其 Get 方法;而對於客戶端別名設置回覆,直接使用了通用回覆類,減少了設計的複雜度。優化
該自定義幀協議有一個設計要點:每個功能性消息類均有相對應的特定回覆類。從功能位的角度來看,該兩種類的主幀功能位之間存在以下關係:this
消息回覆類功能位 - 消息類功能位 = 0x10
即兩類的功能位數值之差以十六進制表示爲 0x10。據此設計功能性 Java 消息類後,不須要專門設計對應的回覆類,系統會自行使用該通用回覆類進行工做。編碼
編碼器可將 Java 消息對象編碼爲數據幀,解碼器可講數據幀解碼爲指定的 Java 消息對象,上節所述的兩種消息類均須要相對應的編解碼器,以下所示:spa
該類相比於基礎類,新增了編解碼器的靜態工廠方法,手動傳入功能位及功能文字描述,進而生成包含這些參數的編解碼器。如此設計,使得全部消息的功能位和文字描述均可以統一管理,下降維護成本。
該類實現了編碼、解碼方法,故可對消息對象進行編碼或對數據幀進行解碼。該類的實現以下所示:
/** * 「消息對象編解碼器」客戶端別名設置 */ public class MsgCodecDeviceName extends BaseMsgCodec { private static MsgCodecDeviceName msgCodec = null; public MsgCodecDeviceName (int majorMsgId, int subMsgId, String detail) { super(majorMsgId, subMsgId, detail); msgCodec = this; } public static MsgDeviceName create(int groupId, int deviceId, String name) { return new MsgDeviceName(msgCodec, groupId, deviceId, name); } @Override public ByteBuf code(BaseMsg msg, ByteBuf buffer) { MsgDeviceName message = (MsgDeviceName) msg; buffer.writeByte(message.getSubMsgId()); byte[] data = KyToArrayUtil.stringToArray(message.getName()); buffer.writeShort(data.length); buffer.writeBytes(data); return buffer; } @Override public MsgDeviceName decode(int groupId, int deviceId, byte[] data) { String name = KyToArrayUtil.arrayToString(data); return create(groupId, deviceId, name); } }
該類相比於基礎類,新增了編解碼器的靜態工廠方法,實現了編解碼器,理由與上小節相同。該類的 createByBaseMsg(BaseMsg)
靜態方法可經過指定功能消息對象生成相應的回覆對象。該類的實現以下所示:
/** * 「消息對象編解碼器」通用消息回覆 */ public class MsgCodecReplyNormal extends BaseMsgCodec { private static MsgCodecReplyNormal msgCodec = null; public MsgCodecReplyNormal(int majorMsgId, int subMsgId, String detail) { super(majorMsgId, subMsgId, detail); msgCodec = this; } /** * 根據收到的消息對象,建立新的通用消息回覆對象, * * @param msg 收到的消息對象 * @return 新的通用消息回覆對象 */ public static MsgReplyNormal createByBaseMsg(BaseMsg msg) { BaseMsgCodec msgCodec = MsgCodecToolkit.getMsgCodec(msg.getMajorMsgId() + 0x10, msg.getSubMsgId()); if (msgCodec == null) { return null; } return new MsgReplyNormal(msgCodec, msg.getGroupId(), msg.getDeviceId()); } /** * 建立新的通用消息回覆對象 * * @param groupId 組號 * @param deviceId 設備號 * @return 生成的通用消息回覆對象 */ private MsgReplyNormal create(int groupId, int deviceId) { return new MsgReplyNormal(this, groupId, deviceId); } @Override public ByteBuf code(BaseMsg msg, ByteBuf buffer) { MsgReplyNormal message = (MsgReplyNormal) msg; buffer.writeByte(message.getSubMsgId()); buffer.writeShort(0); return buffer; } @Override public MsgReplyNormal decode(int groupId, int deviceId, byte[] data) { return create(groupId, deviceId); } }