基於 Netty 的可插拔業務通訊協議的實現「2」特定業務訊息物件的設計
本文為該系列的第二篇文章,設計需求為:服務端程式和眾多客戶端程式通過 TCP 協議進行通訊,通訊雙方需通訊的訊息種類眾多。上一篇文章詳細描述了該通訊協議的二進位制資料幀格式以及基本 Java 訊息類,假設通訊雙方「服務端、客戶端」均由 Netty 框架構建而成,雙方在程式內部使用 Java 訊息物件,通訊雙方資訊互動採用的是自定義二進位制幀格式,本文通過一個具體例項,探討指定的 Java 訊息物件與其相應的二進位制資料幀相互轉換的方法。
1 特定 Java 訊息物件通訊舉例
本小節以一個具體的需求為例,講述該自定義通訊協議的工作流程。該需求為:對某一個特定的客戶端進行命名。該需求的具體工作流程描述如下:
服務端需主動向指定的客戶端傳送訊息,對客戶端設定指定的名稱,客戶端接到指定的訊息並驗證合法後,需向服務端反饋訊息接受成功的確認回覆,伺服器接收到該回復後,即可認為對客戶端進行命名的訊息傳送成功並且名字設定成功,若服務端在指定的時間內未收到回覆,需進行重發或者向上層「如管理員或資料庫」反饋該客戶端的異常。
上述過程使用 UML 序列圖演示如下:
由上圖可以直觀地看出:管理員對伺服器的操作以及伺服器對管理員的反饋均為動作,Server 與 Client 之間的通訊以 Java 的視角均通過 Java 訊息物件,共需兩個物件:客戶端別名設定物件、客戶端別名設定回覆物件。而實際兩者之間的通訊使用的是基於 TCP 的自定義二進位制資料幀,物件與資料幀之間需進行轉換。
2 該任務所需 Java 訊息類的設計
上小節所述過程需要兩個 Java 訊息類,如下所示:
- 客戶端別名設定類
/**
* 「訊息物件」客戶端別名設定
*/
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 方法;而對於客戶端別名設定回覆,直接使用了通用回覆類,減小了設計的複雜度。
該自定義幀協議有一個設計要點:每一個功能性訊息類均有相對應的特定回覆類。從功能位的角度來看,該兩種類的主幀功能位之間存在如下關係:
訊息回覆類功能位 - 訊息類功能位 = 0x10
即兩類的功能位數值之差以十六進位制表示為 0x10。據此設計功能性 Java 訊息類後,不需要專門設計對應的回覆類,系統會自行使用該通用回覆類進行工作。
3 該任務所需訊息類編解碼器的設計
編碼器可將 Java 訊息物件編碼為資料幀,解碼器可講資料幀解碼為指定的 Java 訊息物件,上節所述的兩種訊息類均需要相對應的編解碼器,如下所示:
3.1 客戶端別名設定編解碼器類
該類相比於基礎類,新增了編解碼器的靜態工廠方法,手動傳入功能位及功能文字描述,進而生成包含這些引數的編解碼器。如此設計,使得所有訊息的功能位和文字描述均能夠統一管理,降低維護成本。
該類實現了編碼、解碼方法,故可對訊息物件進行編碼或對資料幀進行解碼。該類的實現如下所示:
/**
* 「訊息物件編解碼器」客戶端別名設定
*/
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);
}
}
3.2 通用回覆編解碼器類
該類相比於基礎類,新增了編解碼器的靜態工廠方法,實現了編解碼器,理由與上小節相同。該類的 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);
}
}