基于 Netty 的可插拔业务通信协议的实现「3」业务注册及实际工作流程

本文为该系列的第三篇文章,设计需求为:服务端程序和众多客户端程序通过 TCP 协议进行通信,通信双方需通信的消息种类众多。上一篇文章以一个具体的需求为例,探讨了指定的 Java 消息对象与其相应的二进制数据帧相互转换的方法。本文仍以该实例为例,探讨该自定义通信协议的具体工作流程,以及如何以注册的形式灵活插拔通信消息对象。

1. 以注册的形式实现通信消息对象的统一管理

通过该系列的第二篇文章可知,各个消息对象的编解码器类均拥有一个静态工厂方法,用于手动传入功能位及功能文字描述,进而生成包含这些参数的编解码器。如此设计,使得所有消息的功能位和文字描述均能够统一管理,降低维护成本。

根据上述需求,可通过 Map 容器管理所有的编解码器,有如下优点:

  1. 进行消息对象生成操作时,可直接使用相应编解码器的消息对象静态创建方法。
  2. 进行消息对象的编码操作时,已拥有该 Java 消息对象,即可知道消息对象的功能位,据此可获取相应的编解码器;或者,每个 Java 消息对象均内含相应编解码器的引用,故可直接对该消息对象进行编码操作。
  3. 进行二进制数据帧的解码操作时,数据帧中已包含了消息的功能位,据此可获取相应的编解码器,而后可以对该数据帧进行解析,生成相应的 Java 消息对象。

通信消息对象注册方法如下所示:

/**
 * 消息对象的注册
 *
 * @param toolkit 消息对象编解码器容器的工具类
 */
private void initialMsg() {
    saveNormalMsgCodec(new MsgCodecDeviceUnlock(0x10, 0x11, "客户端解锁"));
    saveNormalMsgCodec(new MsgCodecDeviceClear(0x10, 0x13, "客户端初始化"));
    saveNormalMsgCodec(new MsgCodecDeviceId(0x10, 0x1B, "客户端ID设置"));
    saveNormalMsgCodec(new MsgCodecEmployeeName(0x10, 0x1C, "客户端别名设置"));
    ... ...
}

/**
 * 将普通消息对象及其回复消息对象的编解码器均保存到 HashMap 中
 *
 * @param baseMsgCodec 特定的消息对象编解码器
 */
private void saveNormalMsgCodec(BaseMsgCodec baseMsgCodec) {
    saveSpecialMsgCodec(baseMsgCodec);
    baseMsgCodec = new MsgCodecReplyNormal(baseMsgCodec.getMajorMsgId() + 0x10, baseMsgCodec.getSubMsgId(), baseMsgCodec.getDetail());
    saveSpecialMsgCodec(baseMsgCodec);
}

/**
 * 将消息对象的编解码器保存到 HashMap 中
 *
 * @param baseMsgCodec 特定的编解码器
 */
private void saveSpecialMsgCodec(BaseMsgCodec baseMsgCodec) {
    HASH_MAP.put(figureFrameId(baseMsgCodec.getMajorMsgId(), baseMsgCodec.getSubMsgId()), baseMsgCodec);
}

上述代码表明,如果有新的业务需求,需要增删「插拔」业务消息对象,只需在 initialMsg() 方法中,对相应编解码器的注册语句进行增删即可。

saveNormalMsgCodec(BaseMsgCodec) 方法可以同时注册特定业务消息对象及其通用回复消息对象,操作方法清晰、简洁。

所以,在启动该 Java 程序时,只需要在启动过程中,执行上述 initialMsg() 方法,即可完成所有业务消息对象的注册。

2. 多个消息对象自由组合进同一个数据帧的实现原理

由该系列的第一篇文章可知,如果某二进制数据帧所要传输的数据体部分内容很少,导致一个帧的大部分容量均被帧头占据,导致有效数据的占比很小,这就产生了巨大的浪费,故数据帧的数据体部分由子帧组成,同一类子帧均可被组装进同一个数据帧。如此做法,整个通信链路的数据量会明显减少,IO 负担也会因此减轻。

该需求的实现原理如下所示:

/**
 * 启动一个Channel的定时任务,用于间隔指定的时间对消息队列进行轮询,并发送指定数据帧
 *
 * @param deque     指定的消息发送队列
 * @param channelId 指定 Channel 的序号
 */
private void startMessageQueueTask(LinkedBlockingDeque<BaseMsg> deque, Integer channelId) {
    executorService.scheduleWithFixedDelay(() -> {
        try {
            BaseMsg baseMsg = deque.take();         // 从队列中取出一个消息对象,队列为空时阻塞
            Thread.sleep(AWAKE_TO_PROCESS_INTERVAL);// 等待极短的时间,保证队列中缓存尽可能多的对象
            Channel channel = touchChannel(channelId); // 获取指定的待发送的 Channel
            List<ByteBuf> dataList = new ArrayList<>();// 子帧容器
            ByteBuf data = baseMsg.subFrameEncode(channel.alloc().buffer());// 编码一个子帧
            dataList.add(data);
            touchNeedReplyMsg(baseMsg);            // 对该子帧设置检错重发任务
            int length = data.readableBytes();
            int flag = baseMsg.combineFrameFlag(); // 获取消息对象标识
            while (true) {
                BaseMsg subMsg = deque.peek();     // 查看队列中的第一个消息对象
                if (subMsg == null || subMsg.combineFrameFlag() != flag) {
                    break; // 消息对象标识不同,即欲生成的主帧帧头不同,不能组合进同一主帧
                }
                data = subMsg.subFrameEncode(channel.alloc().buffer());
                if (length + data.readableBytes() > FrameSetting.MAX_DATA_LENGTH) {
                    break;
                }
                length += data.readableBytes();
                dataList.add(data);                // 组合进了同一主帧
                deque.poll();                      // 从队列中移除该消息对象
                touchNeedReplyMsg(subMsg);
            }
            FrameMajorHeader frameHeader = new FrameMajorHeader(
                    baseMsg.getMajorMsgId(),
                    baseMsg.getGroupId(),
                    baseMsg.getDeviceId(),
                    length);                       // 生成主帧帧头消息对象
            channel.writeAndFlush(new SendableMsgContainer(frameHeader, dataList)); // 送入Channel进行发送
        } catch (InterruptedException e) {
            logger.warn("消息队列定时发送任务被中断");
        }
    }, channelId, CommSetting.FRAME_SEND_INTERVAL, TimeUnit.MILLISECONDS);
}

由代码可知,待发送的消息对象均被送入指定的发送队列进行缓存,某客户端相应的线程对队列进行操作,取出消息对象并进行编码、组装、发送等。当然,当客户端数量较多时,上述的线程实现方式可采用 Netty 的 NIO 方式进行优化,以降低系统开销。

由上述描述可知,欲发送一个消息对象,只需将该消息对象送入相应的发送队列即可。

3. 实际业务消息对象的编解码

3.1 消息对象的编码方式

由于每个 Java 消息对象均内含相应编解码器的引用,故可直接对该消息对象进行编码操作,代码如下:

public abstract class BaseMsg implements Cloneable {
    private final BaseMsgCodec msgCodec;
    ... ...

    /**
     * 将 java 消息对象编码为 TCP 子帧
     *
     * @param buffer 空白的 TCP 子帧的容器
     * @return 保存有 TCP 子帧的容器
     */
    public ByteBuf subFrameEncode(ByteBuf buffer) {
        return msgCodec.code(this, buffer);
    }
}

3.2 消息对象的解码方式

首先根据数据帧的帧头,即可解析出 FrameMajorHeader 对象,然后即可调用如下方法完成子帧的解析工作。实现原理文章开头已指出。

/**
 * TCP 帧解码为 Java 消息对象
 *
 * @param head     主帧头
 * @param subMsgId 子帧功能位
 * @param data     子帧数据
 * @return 已解码的 Java 对象
 */
public BaseMsg decode(FrameMajorHeader head, int subMsgId, byte[] data) {
    BaseMsgCodec msgCodec = MsgCodecToolkit.getMsgCodec(head.getMsgId(), subMsgId);
    return msgCodec.decode(head.getGroupId(), head.getDeviceId(), data);
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容