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