资讯专栏INFORMATION COLUMN

基于 Netty 的可插拔业务通信协议的实现「1」协议描述及基本消息对象设计

Barry_Ng / 628人阅读

摘要:基本消息对象的设计消息对象的设计主要由两部分组成特定数据帧对应的特定消息对象。该类包含上节数据帧主帧及子帧的所有公共信息,仅仅未包含子帧中的数据体信息,该需求由基本消息对象的子类实现。

开发工程中,有一个常见的需求:服务端程序和多个客户端程序通过 TCP 协议进行通信,通信双方需通信的消息种类众多,并且客户端的数量可能有数万个。为此,双方需要约定尽可能丰富、灵活的数据帧「数据包」协议,方便后续业务功能的设计。

本文设计了一种通信协议,为压缩数据量,该协议的数据帧以二进制方式进行传输并识别,即其基本单位为字节,必要时将部分字节流手动转化为可读文本。通过设定功能位来实现丰富的通信消息类型,并且采用注册的方式,可方便扩展新的业务消息类型,可灵活地增删通信消息对象。采用 Netty 框架保证高并发场景下程序的性能。

系统整体设计框图如下:

1. 通信数据帧协议的设计 1.1 数据帧主帧的帧格式

首先给出通用的数据帧格式如下,一个数据帧主帧由:帧识别位、帧功能位、设备号、数据长度、数据体等 5 部分组成。「其实最通用的数据帧只有帧识别位,根据帧识别位确定帧类型,从而确定其余四个部分,本文中帧识别位固定,帧格式即固定了」

帧识别位:确定数据帧的开始,亦确定本帧的帧类型。

帧功能位:确定该帧所传送的消息类型,特定的帧功能位对应特定的数据体。

设备号:设备的识别号,服务端据此识别不同的客户端。

数据长度:数据体所占用的字节数。

数据体:根据帧功能位,所确定的需传输的具体的消息。

1.2 数据帧子帧的帧格式

数据帧除数据体以外的部分称为帧头,考虑这样一种需求,如果某帧所要传输的数据体部分内容很少,导致一个帧的大部分容量均被帧头占据,导致有效数据的占比很小,这就产生了巨大的浪费,举例如下:

如一个开锁帧,只需传输一个开锁信号即可,消息的接收方、消息类型均体现在了帧头中,数据部分只需要 0 个或 1 个字节即可。

客户端需要向服务器发送自己的当前状态信息,该状态信息可能也只需要 1 个字节左右。

由于如上实际的需求,如果增大了每一帧的有效数据的占比,整个通信链路的数据量会明显减少,IO 负担也会因此减轻,所以据此继续对帧协议进行设计。

如上图,对数据帧主帧中的「数据体」部分进行进一步拆分,数据帧主帧的数据体部分由子帧组成,子帧由:子帧功能位、数据长度、数据体等 3 部分组成。

子帧功能位:确定该子帧所传送的消息类型,总而言之,主帧、子帧功能位共同确定了该子帧的消息类型。

数据长度:数据体所占用的字节数。

数据体:根据子帧功能位,所确定的需传输的具体的消息。

1.3 数据帧的帧格式总览

完整的帧格式如下图所示,数据帧主帧的数据体部分完全由子帧组成,通信双方通信时,可以往一个主帧中添加多个子帧,从而可以极大提高链路的使用效率。

2 数据帧处理模块的实现

数据帧已进行了如上精心设计,将设计的数据帧通过程序实现并投入实际使用才是最终目的。

2.1 数据帧处理的基本方法

以服务端的工作为例来进行说明。服务端程序监听指定端口,客户端通过 TCP 协议向服务器发送二进制数据消息,服务端接收到二进制数据并进行处理,此处采用责任链模式,Netty 框架内建了方便的基于责任链模式的消息处理方法:

第一个处理器将捕获的数据截取为一个一个协议约定的数据帧并送入下层处理器,如果捕获的二进制数据未符合协议约定的格式,则可以直接丢弃。「此处未考虑半包、粘包等场景」

第二个处理器捕获到约定的数据帧,则着手对不同类型数据帧进行解析,解析为不同类型的 Java 消息对象,并将反序列化成功并验证成功的 Java 对象送入下层处理器。如果上述过程失败,可以认为客户端设计不合理,导致出现无效消息,直接丢弃该对象,也可以继续通知服务端或客户端该异常情况。

第三个处理器捕获到正确的 Java 消息对象,则可以直接送入上层 Java 模块进行处理,此处可根据不同的对象类型送入不同的上层处理模块,或者在此处进行其他的工作「比如消息日志记录工作等」。

2.2 基本 Java 消息对象的设计

Java 消息对象的设计主要由两部分组成:

特定数据帧对应的特定 Java 消息对象。

特定 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);
}
相关项目参考「GitHub 项目基础框架开源」

Java & Vue.js「集群设备管理云平台『后端部分』」

基于 Vue.js 2.0 & Element 2.0 的集群设备管理云平台

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68607.html

相关文章

  • 基于 Netty 插拔业务通信协议实现「2」特定业务消息对象设计

    摘要:而实际两者之间的通信使用的是基于的自定义二进制数据帧,对象与数据帧之间需进行转换。该类实现了编码解码方法,故可对消息对象进行编码或对数据帧进行解码。该类的静态方法可通过指定功能消息对象生成相应的回复对象。 本文为该系列的第二篇文章,设计需求为:服务端程序和众多客户端程序通过 TCP 协议进行通信,通信双方需通信的消息种类众多。上一篇文章详细描述了该通信协议的二进制数据帧格式以及基本 J...

    Yuqi 评论0 收藏0
  • 基于 Netty 插拔业务通信协议实现「3」业务注册实际工作流程

    摘要:本文仍以该实例为例,探讨该自定义通信协议的具体工作流程,以及如何以注册的形式灵活插拔通信消息对象。进行二进制数据帧的解码操作时,数据帧中已包含了消息的功能位,据此可获取相应的编解码器,而后可以对该数据帧进行解析,生成相应的消息对象。 本文为该系列的第三篇文章,设计需求为:服务端程序和众多客户端程序通过 TCP 协议进行通信,通信双方需通信的消息种类众多。上一篇文章以一个具体的需求为例,...

    LdhAndroid 评论0 收藏0
  • Hyperledger Fabric(介绍)

    摘要:比特币和以太币属于一类区块链,我们将其归类为公共无许可的区块链技术。例如,在单个企业中部署时,或由受信任的权威机构运作,完全拜占庭容错的共识可能被认为是不必要的,并且对性能和吞吐量造成过度的拖累。 介绍 一般而言,区块链是一个不可变的交易分类账,维护在一个分布式对等节点网络中。这些节点通过应用已经由共识协议验证的交易来维护分类帐的副本,该交易被分组为包括将每个块绑定到前一个块的散列的块...

    yunhao 评论0 收藏0
  • Spring整合Netty、WebSocket互联网聊天系统

    摘要:当用户注销或退出时,释放连接,清空对象中的登录状态。聊天管理模块系统的核心模块,这部分主要使用框架实现,功能包括信息文件的单条和多条发送,也支持表情发送。描述读取完连接的消息后,对消息进行处理。 0.前言 最近一段时间在学习Netty网络框架,又趁着计算机网络的课程设计,决定以Netty为核心,以WebSocket为应用层通信协议做一个互联网聊天系统,整体而言就像微信网页版一样,但考虑...

    My_Oh_My 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<