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

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

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

系统整体设计框图如下:

系统整体设计框图

1. 通信数据帧协议的设计

1.1 数据帧主帧的帧格式

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

数据帧格式
  • 帧识别位:确定数据帧的开始,亦确定本帧的帧类型。
  • 帧功能位:确定该帧所传送的消息类型,特定的帧功能位对应特定的数据体。
  • 设备号:设备的识别号,服务端据此识别不同的客户端。
  • 数据长度:数据体所占用的字节数。
  • 数据体:根据帧功能位,所确定的需传输的具体的消息。

1.2 数据帧子帧的帧格式

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

  • 如一个开锁帧,只需传输一个开锁信号即可,消息的接收方、消息类型均体现在了帧头中,数据部分只需要 0 个或 1 个字节即可。
  • 客户端需要向服务器发送自己的当前状态信息,该状态信息可能也只需要 1 个字节左右。

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

数据帧中子帧格式

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

  • 子帧功能位:确定该子帧所传送的消息类型,总而言之,主帧、子帧功能位共同确定了该子帧的消息类型。
  • 数据长度:数据体所占用的字节数。
  • 数据体:根据子帧功能位,所确定的需传输的具体的消息。

1.3 数据帧的帧格式总览

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

数据帧的帧格式总览

2 数据帧处理模块的实现

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

2.1 数据帧处理的基本方法

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

  1. 第一个处理器将捕获的数据截取为一个一个协议约定的数据帧并送入下层处理器,如果捕获的二进制数据未符合协议约定的格式,则可以直接丢弃。「此处未考虑半包、粘包等场景」
  2. 第二个处理器捕获到约定的数据帧,则着手对不同类型数据帧进行解析,解析为不同类型的 Java 消息对象,并将反序列化成功并验证成功的 Java 对象送入下层处理器。如果上述过程失败,可以认为客户端设计不合理,导致出现无效消息,直接丢弃该对象,也可以继续通知服务端或客户端该异常情况。
  3. 第三个处理器捕获到正确的 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 项目基础框架开源」

  1. Java & Vue.js「集群设备管理云平台『后端部分』」
  2. 基于 Vue.js 2.0 & Element 2.0 的集群设备管理云平台
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容

  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,960评论 6 13
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,652评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,079评论 25 707
  • #清凉法语# 何为观心如镜? 小和尚问老和尚:何为观心如镜?为何人要学镜?老和尚道:镜有三优,人却做不到。小和尚说...
    xcy无名阅读 554评论 0 0
  • 喜乐的早晨 写一篇长文 好好地忆想 忘了 保存
    纪泰恩恩阅读 226评论 0 2