tcp client

Mina 自定义硬件通讯协议框架搭建(TCP Client)

2018.03.04 18:49:29字数 1057阅读 2323

Apache MINA 是一个能够帮助用户开发高性能和高伸缩性网络应用程序的框架。它通过Java nio技术基于TCP/IP和UDP/IP协议提供了抽象的、事件驱动的、异步的API。

使用背景

大三读完,出去实习。接触到的第一个框架。我本是一名JAVA黑 微笑, 奈何实习公司仅JAVA开发,好吧,路转粉。
由于是边学边做, 难免在学习过程中遇到了很多坑,不过最终还是解决了,这里就不一一叙述了。
本文将根据自己搭建的框架的过程写此文(参考了很多前辈的代码,总结出来)。

项目背景

项目是根据一套设备(其实是一种超大型LED屏幕)厂商提供的通讯协议去控制设备,该协议是硬件厂家自己定制。
设备数目: 450套

自定义词说明

名称说明
VMS指设备

通讯协议

名称说明
类型TCP/IP
客户端MINA
服务端VMS

数据包格式

包头、类型、数据长度、数据、校验码、包尾
名称偏移位置长度值范围
包头01固定字符‘*’
类型110-127
数据长度22数据字节数
数据4n协议类型决定
校验码n+42类型、数据长度、数据三部分的所有字节的CRC-16码
包尾n+61字符‘#’
转义问题: 
    其中字符'\'为转意符,所有除头尾外的字节,如果是'*','#','\'在通讯时换成"\*","\#","\\" 。 

大小端问题:
    凡涉及多字节数据均为低字节在前

创建项目

开发环境: IntelliJ IDEA (学生可以免费申请收费版(hhh)),创建的是一个Maven的工程。

<!-- 日志工具-->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.7</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.21</version>
</dependency>

<!-- MINA  -->
<dependency>
    <groupId>org.apache.mina</groupId>
    <artifactId>mina-core</artifactId>
    <version>2.0.7</version>
</dependency>

目录结构

|-- pom.xml
`-- src
    |-- com
    |   `-- taoroot
    |       `-- mina
    |           |-- protocol                                // 协议 目录
    |           |   |-- IMessageBody.java
    |           |   |-- MyBuffer.java                       // IoBuffer 封装
    |           |   |-- VMSMessage.java                     
    |           |   |-- VMSMessageFactory.java              // 解析工厂
    |           |   |-- VMSMessageHeader.java               // 表头部分内容
    |           |   |-- VMS_00.java                         // 心跳数据包
    |           |   |-- VMS_14.java                         // 巡检数据包
    |           |-- client                                  // Mina 目录
    |           |   |-- ClientSessionHandler.java
    |           |   |-- ConnectListener.java
    |           |   |-- MinaClient.java                     // 程序路口
    |           |   |-- ExceptionHandler.java               // 心跳包超时处理类
    |           |   |-- VMSKeepAliveFilter.java             // 心跳包拦截器
    |           |   |-- VMSKeepAliveMessageFactory.java     // 心跳包工厂
    |           |   |-- VMSMessageCodecFactory.java     
    |           |   |-- VMSMessageDecoder.java
    |           |   `-- VMSMessageEncoder.java
    |           `-- util
    |               |-- ByteUtil.java
    |               |-- ClassUtils.java
    |               |-- ConfigUtil.java
    |               |-- CrcCodeUtil.java
    |               |-- MinaUtil.java
    |               |-- RGBUtil.java
    |-- config.properties                                   // 默认配置
    `-- log4j.properties                                    // 日志配置

启动服务 MinaClient

vmsMap:

用来接收来自设备列表
key: 设备编号, value: 设备地址
编号是唯一的,后端下发指令时,用此编号区分设备

sessionMap

设备对应的session
key: 设备编号, value: IoSession
只有连接成功的设备才会存入,所以在下一次获取列表后,将重新尝试连接。

初始化Mina以后, 系统将定时从后端获取设备列表存入vmsMap中,与sessionMap中的编号比较。
有几次情况需要考虑:

情况1. vmsMap中有, sessionMap中没有: 尝试创建IoSession,创建成功则加入sessionMap。
情况2. vmsMap中有, sessionMap中有,但设备地址发生改变: 关闭现有session, 然后按情况1处理。
情况3. vmsMap中没有,sessionMap中有, 关闭现有session,从sessionMap移除。

流程图

getList.png

代码实现

    // Mina 初始化配置
        connector = new NioSocketConnector();
        connector.setConnectTimeoutMillis(MinaUtil.CONNECT_TIMEOUT);
        // ioBuffer 日志(实际生产环境不需要)
        connector.getFilterChain().addLast( "logger", new LoggingFilter() );
        // 解码过滤层 (数据包转对象)
        connector.getFilterChain().addLast("vms_coder", new ProtocolCodecFilter(new VMSMessageCodecFactory()));
        // 超时过滤层 (对TCP在线,心跳包超时的设备主动断开连接)
        KeepAliveFilter heartBeat = new VMSKeepAliveFilter(new VMSKeepAliveMessageFactory());
        // 设置心跳频率
        heartBeat.setRequestInterval((int) MinaUtil.HEART_BEAT_RATE);
        connector.getFilterChain().addLast("heartbeat", heartBeat);
        // 业务处理类
        connector.setHandler(new ClientSessionHandler());
        IoSession session;

        // MQ 初始化
        // Mina本身不知道设备的网络地址,是通过订阅形式,从后端获取过来
        TopicSender.createTopic(VMS_SCREEN_LIST_TOPIC);
        MQListener.init();

        for (; ; ) {
            // 获取列表 (0是设备列表信息)
            MinaUtil.getDeviceScreen("", 0);
            // 是否被锁
            if (!StaticUtil.isIsRefreshDeviceMap()) {
                // 关锁
                StaticUtil.setIsRefreshDeviceMap(true);
                // 遍历设备列表
                for (String devNo : deviceMap.keySet()) {
                    // 新设备上线, 创建新连接
                    if (!ioSessionMap.containsKey(devNo)) {
                        newSocket(devNo, deviceMap.get(devNo));
                    } else {
                        // 查看设备对应的地址是否变化
                        session = ioSessionMap.get(devNo);
                        String currentAddress = "";  // 当前地址
                        String newAddressPort = deviceMap.get(devNo);  // 新地址
                        // 从session中获取出当前连接的地址
                        currentAddress = getAddressPort(session);
                        // 地址发生改变
                        if (!currentAddress.equals(newAddressPort)) {
                            session.close(true);    // 关闭目前连接的session
                            newSocket(devNo, newAddressPort);    // 用新地址创建连接
                        }
                    }
                }

                // 设备列表中已经删除了编号, session如果存在,也需要断开连接
                Iterator<Map.Entry<String, IoSession>> it = ioSessionMap.entrySet().iterator();
                while (it.hasNext()) {
                    Map.Entry<String, IoSession> entry = it.next();
                    String devNo = entry.getKey();
                    session = entry.getValue();
                    if (!deviceMap.containsKey(devNo)) {
                        it.remove();
                        // 从session中获取出当前连接的地址
                        String currentAddress = getAddressPort(session);
                        session.close(true);
                        sendOnlineStatus(devNo, currentAddress, VMS_OFFLINE_STATUS);
                        logger.info("设备: " + devNo + " 离线");
                    }
                }
                // 开锁
                StaticUtil.setIsRefreshDeviceMap(false);
            }
            // 休眠一段时间在去获取设备列表
            try {
                Thread.sleep(REFRESH_DEVICE_LIST_TIME * 1000);
            }catch (Exception e) {
                e.toString();
            }
        }
    }

编解码工厂 VMSMessageCodecFactory

public class VMSMessageCodecFactory implements ProtocolCodecFactory {
    private final VMSMessageDecoder decoder;       // 解码器
    private final VMSMessageEncoder encoder;       // 编码器
}

解码器

粘包

两个数据包的部分数据相连接

解码流程图

// todo

解码器源代码

public class VMSMessageDecoder extends CumulativeProtocolDecoder {
    private static Logger logger = Logger.getLogger(VMSMessageDecoder.class);

    protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
        if (in.remaining() < 1) {
            return false;
        }
        in.mark();
        byte[] data = new byte[in.remaining()];
        in.get(data);
        int pos = 0;
        in.reset();
        while (in.remaining() > 0) {
            in.mark();
            byte tag = in.get();
            //搜索包的开始位置
            if (tag == 0x2A && in.remaining() > 0) {
                tag = in.get();
                //寻找包的结束
                while (tag != 0x23) {
                    if (in.remaining() <= 0) {
                        in.reset(); //没有找到结束包,等待下一次包
                        return false;
                    }
                    tag = in.get();
                }
                pos = in.position();
                int packetLength = pos - in.markValue();
                if (packetLength > 1) {
                    byte[] tmp = new byte[packetLength];
                    in.reset();
                    in.get(tmp);
                    //解析
                    VMSMessage message = new VMSMessage();
                    message.ReadFromBytes(tmp);
                    out.write(message); //触发接收Message的事件
                }
            }
        }
        return false;
    }
}

编码器

编码器相对就简单很多。

编码器源代码

public class VMSMessageEncoder extends ProtocolEncoderAdapter {
    @Override
    public void encode(IoSession ioSession, Object message, ProtocolEncoderOutput out) throws Exception {
        IoBuffer buf = IoBuffer.allocate(500).setAutoExpand(true);
        VMSMessage vmsMessage = (VMSMessage) message;
        buf.put(vmsMessage.WriteToBytes());
        buf.flip();
        out.write(buf);
        out.flush();
        buf.free();
    }
}

心跳包机制

vms 和 mina 之间需要不能有超过2分钟的空闲状态。 需要定期发送心跳包,否则vms将进入离线状态,停止播放。
发送过程如下: mina 进入空闲状态, 发送心跳包, vms回发心跳包, mina 接收到心跳包。如果未在规定的时间内,接收到心跳包,超过三次,将主动关闭session。
mina 其实自带了一套心跳包拦截器,(上文mina配置代码)。可以将心跳包在IoHandler前处理掉,就不用再业务层去关心了。

MyKeepAliveFilter

public class MyKeepAliveFilter extends KeepAliveFilter {
    private static final int TIMEOUT = CmdOptionHandler.getTimeout();
    public MyKeepAliveFilter(KeepAliveMessageFactory messageFactory) {
        // super(心跳包工厂, 两遍都是空闲状态, 超时处理类, 上发超时时间, 下发超时时间)
        super(messageFactory, IdleStatus.BOTH_IDLE, new MyKeepAliveRequestTimeoutHandler(), TIMEOUT, TIMEOUT);
        //此消息不会继续传递,不会被业务层看见
        this.setForwardEvent(false);
    }
}

VMSKeepAliveMessageFactory

主要有两个功能:
第一,判断是否是心跳包,
第二,生成一个心跳包数据。
KeepAliveMessageFactory 提供了四个接口,当初看了网上教程说什么半工,双工什么的,一头雾水,最后硬着头皮,源代码了。当时猜测是 sessionIdle 发送了心跳包, messageReceived 接收心跳包。

KeepAliveFilter 解读

// 空闲状态触发
public void sessionIdle(NextFilter nextFilter, IoSession session, IdleStatus status) throws Exception {
    if (status == interestedIdleStatus) {
        if (!session.containsAttribute(WAITING_FOR_RESPONSE)) {
            // 看来 getRequest 是用来获取一个发送心跳包数据接口
            Object pingMessage = messageFactory.getRequest(session);
            if (pingMessage != null) {
                nextFilter.filterWrite(session, new DefaultWriteRequest(pingMessage));
                if (getRequestTimeoutHandler() != KeepAliveRequestTimeoutHandler.DEAF_SPEAKER) {
                    markStatus(session);
                    if (interestedIdleStatus == IdleStatus.BOTH_IDLE) {
                        session.setAttribute(IGNORE_READER_IDLE_ONCE);
                    }
                } else {
                    resetStatus(session);
                }
            }
        } else {
            handlePingTimeout(session);
        }
    } 
}
// 接收到数据触发
public void messageReceived(NextFilter nextFilter, IoSession session, Object message) throws Exception {
    try {
        // 判断数据包是否是心跳包,
        if (messageFactory.isRequest(session, message)) {
            // 这里又获取了一个心跳包,所以说这里是用来判断,vms主动发送心跳包过来,然后mina回应一个心跳包
            // 在我们设备中,设备不主动发送心跳包,也不对设备主动发送心跳包做一个回应,所以,不需要这个逻辑
            // 所以让 isRequest 放回 false 就行, 
            Object pongMessage = messageFactory.getResponse(session, message);
            // 保险起见,让 getResponse 也返回null
            if (pongMessage != null) {
                nextFilter.filterWrite(session, new DefaultWriteRequest(pongMessage));
            }
        }
        // 这里也是判断心跳包,
        if (messageFactory.isResponse(session, message)) {
            // 里面是清空了 mina在sessionIdl下发送的心跳包标志位
            // 所以这心跳包 是用来是vms回发的心跳响应包。
            resetStatus(session);
        }
    } finally {
        if (!isKeepAliveMessage(session, message)) {
            nextFilter.messageReceived(session, message);
        }
    }
}

好了,到这里就知道怎么去写 VMSKeepAliveMessageFactory 了。
当初看教程的死活没看懂,还弄出了死循环,vms、mina一直在对送心跳包。有时候源代码才是最好的教程呀(hhh)。

心跳包计数器(业务逻辑扩展)

心跳包是在空闲状态下会以一定的频率发送,业务层需要有一个定时巡检的数据包,vms会返回自身状态信息,所以我就把这个单做了了一个计数器使用。mina每次发送一个心跳包,就自增一,达到触发值就发送状态包。
巡检间隔 = 心跳包发送间隔 * 触发值

VMSKeepAliveMessageFactory 源代码

public class VMSKeepAliveMessageFactory implements KeepAliveMessageFactory {
    private final static org.slf4j.Logger logger = LoggerFactory.getLogger(ClientSessionHandler.class);
    private VMSMessage vmsMessage;
    // 发送心跳包
    @Override
    public Object getRequest(IoSession arg0) {
        heartCountAdd(arg0);
        vmsMessage = new VMSMessage();
        vmsMessage.setMessageContents(new VMS_00());
        return vmsMessage;
    }
    // 拦截心跳包
    @Override
    public boolean isResponse(IoSession session, Object message) {
        return isHeartPage(message);
    }
    @Override
    public Object getResponse(IoSession arg0, Object arg1) {
        return null;
    }
    @Override
    public boolean isRequest(IoSession session, Object message) {
        return false;
    }
    //  心跳包 计数器
    private void heartCountAdd(IoSession session) {
        int heartCounter = (int) session.getAttribute(MinaUtil.VMS_HEARTBEAT_COUNT_ATTRIBUTE) + 1;
        session.setAttribute(MinaUtil.VMS_HEARTBEAT_COUNT_ATTRIBUTE, heartCounter);
        if(heartCounter > MinaUtil.VMS_HEARTBEAT_MAX) {
            session.setAttribute(MinaUtil.VMS_HEARTBEAT_COUNT_ATTRIBUTE, 0);
            heartCounterHandler(session);
        }
    }
    // 定时查询任务
    private void heartCounterHandler(IoSession session) {
        // 定期查询 设备状态
        VMSMessage message = new VMSMessage();
        message.setMessageContents(new VMS_0F());
        session.write(message);
    }
    // 判断是不是心跳包
    private boolean isHeartPage(Object message) {
        vmsMessage = (VMSMessage) message;
        if (vmsMessage.getMessageContents() instanceof VMS_00) {
            return true;
        }
        return false;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值