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---");
    }

}

八、 常量類

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---

測試稍後奉上,敬請期待~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章