springboot+mina框架服务端的实现(二) ------ 心跳包、自定义Session及其管理类、业务处理类、mina连接的创建

接上一节:springboot+mina框架服务端的实现(一) ------ pom依赖、mina配置类、自定义协议以及编解码器的实现

参考博客:矢落叶の博客

四、 心跳包的实现

先简单介绍下keepAlive的机制:

首先,需要搞清楚TCP keepalive是干什么用的。从名字理解就能够知道,keepalive就是用来检测一个tcp connection是否还连接正常。当一个tcpconnection建立好之后,如果双方都不发送数据的话,tcp协议本身是不会发送其它的任何数据的,也就是说,在一个idle的connection上,两个socket之间不产生任何的数据交换。从另一个方面讲,当一个connection建立之后,链接双方可以长时间的不发送任何数据,比如几天,几星期甚至几个月,但该connection仍然存在。

所以,这就可能出现一个问题。举例来说,server和client建立了一个connection,server负责接收client的request。当connection建立好之后,client由于某种原因机器停机了。但server端并不知道,所以server就会一直监听着这个connection,但其实这个connection已经失效了。

keepalive就是为这样的场景准备的。当把一个socket设置成了keepalive,那么这个socket空闲一段时间后,它就会向对方发送数据来确认对方仍然存在。放在上面的例子中,如果client停机了,那么server所发送的keepalive数据就不会有response,这样server就能够确认client完蛋了(至少从表面上看是这样)。

MINA本身提供了一个过滤器类: org.apache.mina.filter.keepalive.KeepAliveFilter ,该过滤器用于在IO空闲的时候发送并且反馈心跳包(keep-alive request/response)。
该类构造函数中参数有三个分别是:
(1)KeepAvlieMessageFactory: 该实例引用用于判断接受与发送的包是否是心跳包,以及心跳请求包的实现
(2)IdleStatus: 该过滤器所关注的空闲状态,默认认为读取空闲。 即当读取通道空闲的时候发送心跳包
(3)KeepAliveRequestTimeoutHandler: 心跳包请求后超时无反馈情

先看KeepAliveMessageFactory接口:

public interface KeepAliveMessageFactory {

    boolean isRequest(IoSession session, Object message);

    boolean isResponse(IoSession session, Object message);

    Object getRequest(IoSession session);

    Object getResponse(IoSession session, Object request);
}

实现这个接口:

public class KeepAliveFactoryImpl implements KeepAliveMessageFactory {

    static final Logger logger = LoggerFactory.getLogger(KeepAliveFactoryImpl.class);

    @Resource
    private BaseHandler keepAliveHandler;

    // 用来判断接收到的消息是不是一个心跳请求包,是就返回true[接收端使用]
    @Override
    public boolean isRequest(IoSession session, Object message) {
        if (message instanceof MyPack) {
            MyPack pack = (MyPack) message;
            if (Const.HEART_BEAT == pack.getModule()) {
                return true;
            }
        }
        return false;
    }

    // 用来判断接收到的消息是不是一个心跳回复包,是就返回true[发送端使用]
    @Override
    public boolean isResponse(IoSession session, Object message) {
        // TODO Auto-generated method stub
        return false;
    }

    // 在需要发送心跳时,用来获取一个心跳请求包[发送端使用]
    @Override
    public Object getRequest(IoSession session) {
        // TODO Auto-generated method stub
        return null;
    }

    // 在需要回复心跳时,用来获取一个心跳回复包[接收端使用]
    @Override
    public Object getResponse(IoSession session, Object request) {
        MyPack attendPack = (MyPack) request;
        if (null == session.getAttribute(Const.SESSION_KEY)) {
        	// 需要先进行登录
            return new MyPack(Const.AUTHEN, attendPack.getSeq(), "fail");
        }
        // 将超时次数置为0
        session.setAttribute(Const.TIME_OUT_KEY, 0);
        return new MyPack(Const.HEART_BEAT, attendPack.getSeq(), "success");
    }

}

超时后的处理放在业务处理类的sessionIdle方法中实现,稍后详细介绍

五、 自定义Session类及其管理类

方便对session会话进行管理,方便对session会话集合获取和删除

服务端接收到新的Session后,构造一个封装类,实现session 的部分方法,并额外实现方法

5.1 MySession 类

就一个处理业务的方法,使用自定义Session代替IoSession

public class MySession implements Serializable {

    private static final long serialVersionUID = 1L;

    // 不参与序列化
    private transient IoSession session;

    // session在本机器的ID
    private Long nid;
    // session绑定的服务ip
    private String host;
    // 访问端口
    private int port;
    // session绑定的设备
    private String account;

    public MySession() {
    }

    public MySession(IoSession session) {
        this.session = session;
        this.host = ((InetSocketAddress) session.getRemoteAddress()).getAddress().getHostAddress();
        this.port = ((InetSocketAddress) session.getRemoteAddress()).getPort();
        this.nid = session.getId();
    }

    /**
     * 将key-value自定义属性,存储到IO会话中
     */
    public void setAttribute(String key, Object value) {
        if (null != session) {
            session.setAttribute(key, value);
        }
    }

    /**
     * 从IO的会话中,获取key的value
     */
    public Object getAttribute(String key) {
        if (null != session) {
            return session.getAttribute(key);
        }
        return null;
    }

    /**
     *  在IO的会话中,判断是否存在包含key-value
     */
    public boolean containsAttribute(String key) {
        if (null != session) {
            return session.containsAttribute(key);
        }
        return false;
    }

    /**
     * 从IO的会话中,删除key
     */
    public void removeAttribute(String key) {
        if (null != session) {
            session.removeAttribute(key);
        }
    }

    /**
     *  获取IP地址
     */
    public SocketAddress getRemoteAddress() {
        if (null != session) {
            return session.getRemoteAddress();
        }
        return null;
    }

    /**
     * 将消息对象 message发送到当前连接的对等体(异步)
     * 当消息被真正发送到对等体的时候,IoHandler.messageSent(IoSession,Object)会被调用。
     * @param msg 发送的消息
     */
    public void write(MyPack msg) {
        if (null != session) {
            session.write(msg).isWritten();
        }
    }

    /**
     * 会话是否已经连接
     */
    public boolean isConnected() {
        if (null != session) {
            return session.isConnected();
        }
        return false;
    }

    /**
     * 关闭当前连接。如果参数 immediately为 true的话
     * 连接会等到队列中所有的数据发送请求都完成之后才关闭;否则的话就立即关闭。
     */
    public void close(boolean immediately) {
        if (null != session) {
            if (immediately) {
                session.closeNow();
            } else {
                session.closeOnFlush();
            }
        }
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (null == obj) {
            return false;
        }
        if (this.getClass() != obj.getClass()) {
            return false;
        }
        // 强转为当前类
        MySession session = (MySession) obj;
        if (session.nid != null && nid != null) {
            return session.nid.longValue() == nid.longValue() && session.host.equals(host) && session.port == port;
        }
        return false;
    }

    public String toString() {
        return "session host:" + this.host + " port:" + this.port + " nid:" + this.nid;
    }
	
	// getter/setter...
}

5.2 Session管理接口及其实现类

public interface SessionManager {

    /**
     * 添加session
     */
    void addSession(String device, MySession session);

    /**
     * 获取session
     */
    MySession getSession(String device);

    /**
     * 替换Session
     */
    void replaceSession(String device, MySession session);

    /**
     * 删除session
     */
    void removeSession(String device);

    /**
     * 删除session
     */
    void removeSession(MySession session);

}

实现类:

@Configuration
public class DefaultSessionManagerImpl extends Observable implements SessionManager {

    /**
     * 存放session的线程安全的map集合
     */
    private static ConcurrentHashMap<String, MySession> sessions = new ConcurrentHashMap<>();

    /**
     * 线程安全的自增类,用于统计连接数
     */
    private static final AtomicInteger connectionsCounter = new AtomicInteger(0);

    /**
     * 添加session
     */
    @Override
    public void addSession(String account, MySession session) {
        if (null != session) {
            sessions.put(account, session);
            connectionsCounter.incrementAndGet();
            // 被观察者方法,拉模型
            setChanged();
            notifyObservers();
        }
    }

    /**
     * 获取session
     */
    @Override
    public MySession getSession(String account) {
        return sessions.get(account);
    }

    /**
     * 替换session,通过账号
     */
    @Override
    public void replaceSession(String account, MySession session) {
        sessions.put(account, session);
        // 被观察者方法,拉模型
        setChanged();
        notifyObservers();
    }

    /**
     * 移除session通过账号
     */
    @Override
    public void removeSession(String account) {
        sessions.remove(account);
        connectionsCounter.decrementAndGet();
        // 被观察者方法,拉模型
        setChanged();
        notifyObservers();
    }

    /**
     * 移除session通过session
     */
    @Override
    public void removeSession(MySession session) {
        String account = (String) session.getAttribute(Const.SESSION_KEY);
        removeSession(account);
    }

    public static ConcurrentHashMap<String, MySession> getSessions() {
        return sessions;
    }
}

六、 业务处理类

6.1 BaseHandler接口

业务处理类都需要实现该接口,该接口定义了一个处理业务逻辑的方法:

/**
 * Mina的请求处理接口,必须实现此接口
 *
 */
public interface BaseHandler {
    String process(MySession mySession, String content);
}

6.2 业务处理核心控制类

NioSocketAcceptor设置handler后,会调用messageReceived方法处理业务逻辑。
当读写空闲时间超过定义的时间后,会调用该handler的sessionIdle方法。

public class ServerHandler extends IoHandlerAdapter {
    static final Logger logger = LoggerFactory.getLogger(ServerHandler.class);

    private HashMap<Integer, BaseHandler> handlers = new HashMap<>();

    @Autowired
    SessionManager sessionManager;

    @Override
    public void messageReceived(IoSession session, Object message) throws Exception {
        MySession MySession = new MySession(session);
        MyPack MyPack = (MyPack) message;
        logger.info(MySession.toString() + ">>>>>> server received:" + message);

        MyPack response;

        // 如果是心跳包接口,则说明处理失败
        if (Const.HEART_BEAT == MyPack.getModule()) {
            logger.info(MySession.toString() + ">>>>>> server handler heartbeat error!");
            response = new MyPack(Const.AUTHEN, MyPack.getSeq(), "authen fail");
            MySession.write(response);
            MySession.close(false);
            return;
        }

        // 终端在未认证时连接进来,SERVER端要发送认证失败的包给终端,然后再断开连接,防止未知设备连到服务器
        if (null == MySession.getAttribute(Const.SESSION_KEY) && Const.AUTHEN != MyPack.getModule()) {
            logger.info(MySession.toString() + ">>>>>> need device authen!");
            response = new MyPack(Const.AUTHEN, MyPack.getSeq(), "authen fail");
            MySession.write(response);
            MySession.close(false);
            return;
        }

        BaseHandler handler = handlers.get(MyPack.getModule());
        String result = handler.process(MySession, MyPack.getBody());
        if (result == null) {
            logger.info(MySession.toString() + ">>>>>> need authen!");
            response = new MyPack(Const.AUTHEN, MyPack.getSeq(), "deal error");
            MySession.write(response);
            MySession.close(false);
        } else {
            logger.info(MySession.toString() + ">>>>>> succeed!");
            response = new MyPack(MyPack.getModule(), MyPack.getSeq(), result);
            MySession.write(response);
        }
    }

    /**
     * 心跳包超时处理
     */
    @Override
    public void sessionIdle(IoSession session, IdleStatus status) throws Exception {
        if (session.getAttribute(Const.TIME_OUT_KEY) == null) {
            session.closeNow();
            logger.error(
                    session.getAttribute(Const.SESSION_KEY) + " nid: " + session.getId() + " >>>>>> time_out_key null");
            return;
        }
        try {
            int isTimeoutNum = (int) session.getAttribute(Const.TIME_OUT_KEY);
            isTimeoutNum++;
            // 没有超过最大次数,超时次数加1
            if (isTimeoutNum < Const.TIME_OUT_NUM) {
                session.setAttribute(Const.TIME_OUT_KEY, isTimeoutNum);
            } else {
                // 超过最大次数,关闭会话连接
                String account = (String) session.getAttribute(Const.SESSION_KEY);
                // 移除device属性
                session.removeAttribute(Const.SESSION_KEY);
                // 移除超时属性
                session.removeAttribute(Const.TIME_OUT_KEY);
                sessionManager.removeSession(account);
                session.closeOnFlush();
                logger.info(">>>>>> client user: " + account + " more than " + Const.TIME_OUT_NUM
                        + " times have no response, connection closed! >>>>>>");
            }
        } catch (Exception e) {
            logger.error(
                    session.getAttribute(Const.SESSION_KEY) + " nid: " + session.getId() + " >>>>>> " + e.getMessage());
            session.closeNow();
        }
    }

    @Override
    public void sessionClosed(IoSession session) throws Exception {
        logger.info(session.getAttribute(Const.SESSION_KEY) + " nid: " + session.getId() + " >>>>>> sessionClosed ");
        // 移除account属性
        session.removeAttribute(Const.SESSION_KEY);
        // 移除超时属性
        session.removeAttribute(Const.TIME_OUT_KEY);
        String account = (String) session.getAttribute(Const.SESSION_KEY);
        sessionManager.removeSession(account);
        session.closeNow();
    }

    @Override
    public void sessionCreated(IoSession session) throws Exception {
        InetSocketAddress isa = (InetSocketAddress) session.getRemoteAddress();
        // IP
        String address = isa.getAddress().getHostAddress();
        session.setAttribute("address", address);
        logger.info(">>>>>> 来自" + address + " 的终端上线,sessionId:" + session.getId());
    }

    @Override
    public void sessionOpened(IoSession session) throws Exception {
        logger.info("Open a session ...");
    }

    @Override
    public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
        logger.error(
                ">>>>>> 终端用户:" + session.getAttribute(Const.SESSION_KEY) + "连接发生异常,即将关闭连接,原因:" + cause.getMessage());
    }

    @Override
    public void messageSent(IoSession session, Object message) throws Exception {
        logger.info(">>>>>>>>>>>>>>>>>>>> 发送消息成功 >>>>>>>>>>>>>>>>>>>>");
    }

    public HashMap<Integer, BaseHandler> getHandlers() {
        return handlers;
    }

    public void setHandlers(HashMap<Integer, BaseHandler> handlers) {
        this.handlers = handlers;
        logger.info(">>>>>> server handlers set success!");
    }

}

绑定账号handler:

public class BindHandler implements BaseHandler {

    static final Logger logger = LoggerFactory.getLogger(BindHandler.class);

    // 获取会话管理类
    @Autowired
    private SessionManager sessionManager;

    @Override
    public String process(MySession mySession, String content) {

        if (StringUtils.isBlank(content)) {
            return null;
        }

        try {
            JSONObject data = JSONObject.parseObject(content);

            // 检查账号是否存在
            String account = data.getString("account");
            // 可增加数据库、redis之类的认证
            if (StringUtils.isBlank(account)) {
                return null;
            }

            // 检查软件版本号
            String version = data.getString("version");
            // 可增加数据库、redis之类的认证
            if (!Const.VERSION.equals(version)) {
                return null;
            }

            mySession.setAttribute(Const.SESSION_KEY, account);
            mySession.setAttribute(Const.TIME_OUT_KEY, 0); // 超时次数设为0

            // 由于客户端断线服务端可能会无法获知的情况,客户端重连时,需要关闭旧的连接
            MySession oldSession = sessionManager.getSession(account);
            if (oldSession != null && !oldSession.equals(mySession)) {
                // 移除account属性
                oldSession.removeAttribute(Const.SESSION_KEY);
                // 移除超时时间
                oldSession.removeAttribute(Const.TIME_OUT_KEY);
                // 替换oldSession
                sessionManager.replaceSession(account, mySession);
                oldSession.close(false);
                logger.info(">>>>>> oldsession close!");
            }
            if (oldSession == null) {
                sessionManager.addSession(account, mySession);
            }
            logger.info(">>>>>> bind success: " + mySession.getNid());
        } catch (Exception e) {
            logger.error(">>>>>> bind error: " + e.getMessage());
            return null;
        }

        return "bind success";
    }

}

验证服务器时间handler:

public class TimeCheckHandler implements BaseHandler {

    static final Logger logger = LoggerFactory.getLogger(TimeCheckHandler.class);

    @Autowired

    @Override
    public String process(MySession mySession, String content) {

        if (StringUtils.isBlank(content)) {
            return null;
        }

        try {
            
            // 平台系统时间
            SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
            String time = sdf.format(new Date());
            return time;

        } catch (Exception e) {
            logger.error(">>>>>> time check error: " + e.getMessage());
            return null;
        }
    }

}

七、 创建mina连接

在Spring Boot中给我们提供了两个接口来帮助我们实现这样的需求-CommandLineRunner和ApplicationRunner

@Component
public class MinaServerRun implements CommandLineRunner{

    private static final Logger logger = LoggerFactory.getLogger(MinaServerRun.class);
    
    @Autowired
    private NioSocketAcceptor acceptor;
    
    public MinaServerRun(NioSocketAcceptor acceptor) {
        this.acceptor = acceptor;
    }
    
    @Override
    public void run(String... args) throws Exception {
        acceptor.bind(new InetSocketAddress(Const.PORT));
        logger.info("---springboot mina server start---");
        Runtime.getRuntime().addShutdownHook(new Thread() {

            @Override
            public void run() {
                logger.info("---server acceptor unbind---");
                acceptor.unbind();
                logger.info("---server acceptor dispose---");
                acceptor.dispose();
            }

        });
    }

}

额外添加了一个钩子方法,保证在使用kill指令杀掉tomcat进程时,可以关闭NioSocketAcceptor

八、 常量类

public class Const {
    
    public static final int PORT = 8090;
    
    // idel时间,单位秒
    public static final int IDELTIMEOUT = 180;

    // session_key采用设备编号
    public static final String SESSION_KEY = "account";

    // 超时KEY
    public static final String TIME_OUT_KEY = "time_out";
    
    // 超时次数
    public static final int TIME_OUT_NUM = 3;

    // 登录验证
    public static final int AUTHEN = 1;

    // 验证服务器时间
    public static final int TIME_CHECK = 2;
    
    // 心跳包
    public static final int HEART_BEAT = 3;

    // 版本
    public static final String VERSION = "V1.0";
}

九、 项目启动及测试

项目启动后,控制台信息如下:


  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.6.RELEASE)

2019-07-06 17:05:32.543  INFO 15572 --- [           main] org.my.SpringbootMinaApplication         : Starting SpringbootMinaApplication on PC-20161221CSLG with PID 15572 (D:\workspace\springboot_mina\target\classes started by Administrator in D:\workspace\springboot_mina)
2019-07-06 17:05:32.551  INFO 15572 --- [           main] org.my.SpringbootMinaApplication         : No active profile set, falling back to default profiles: default
2019-07-06 17:05:34.474  INFO 15572 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2019-07-06 17:05:34.518  INFO 15572 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2019-07-06 17:05:34.518  INFO 15572 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.21]
2019-07-06 17:05:34.711  INFO 15572 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2019-07-06 17:05:34.711  INFO 15572 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2078 ms
2019-07-06 17:05:34.849  INFO 15572 --- [           main] org.my.mina.handler.ServerHandler        : >>>>>> server handlers set success!
2019-07-06 17:05:35.284  INFO 15572 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2019-07-06 17:05:35.628  INFO 15572 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2019-07-06 17:05:35.639  INFO 15572 --- [           main] org.my.SpringbootMinaApplication         : Started SpringbootMinaApplication in 3.729 seconds (JVM running for 4.901)
2019-07-06 17:05:35.649  INFO 15572 --- [           main] org.my.mina.MinaServerRun                : ---springboot mina server start---

测试稍后奉上,敬请期待~

项目github地址:
https://github.com/gavinL93/springboot_mina

欢迎大家关注

你可能感兴趣的:(mina框架,springboot实战)