开发工程中,有一个常见的需求:服务端程序和多个客户端程序经过 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);
}
复制代码