netty 超時,登錄,心跳,狀態模式等解問題

物聯網交流羣:651219170
不要只做伸手黨,希望大家能多分享多交流。

在使用 netty 的時候可能會出現:
1.設備鏈接 netty 之後,不做登錄操作,也不發送數據,白白浪費socket資源。
2.設備鏈接之後不做認證,就發送數據(對於這樣的流氓我們肯定是斷開了)。
3.設備鏈接之後,也登錄成功了,但是網絡異常,設備掉線了。這時候服務器是感知不到的(浪費資源)。
4.設備超時之後,一般我們要給他幾次機會的(我都是3次)。如果在允許的範圍內,有上行數據,或者心跳,則證明它還活着,我們就解除它的超時狀態。
還有好多情況 …..
對於這個問題,我來描述一下我的解決思路。有問題希望多多賜教。

需要了解的基礎

netty 服務器開發
netty Attribute 相關的 api
netty IdleStateHandler 超時處理類。
完美解決方案需要熟悉設計模式的狀態模式。(這裏可以作爲學習狀態模式非常好的例子)

解決思路

核心點就是每個 channle 都可以有自己的 attr。定義一個標記設備的狀態的 AttributeKey 。 後面判斷這個 Attribute 的值就知道設備是否登錄。
1.在用戶發送登錄包的時候查詢設備信息,設備信息校驗通過之後,設置設備attribute 值爲設置爲登錄。
2.上報數據的時候判斷是否attribute 值是否爲 true。沒有登錄的話就斷開鏈接。
3.如果設備鏈接之後不登錄,也不發送數據。這種情況,我們需要設置一個超時時間,如果超時沒有任何數據,就觸發超時自檢,檢查此 channle 的 attr 是不是已經登錄。沒有的話,就斷開鏈接。
4.用狀態圖把所有狀態,及各個狀態下的允許的行爲列出來。然後用狀態模式開發一個設備狀態類,做爲每個 channle 的 attr。

實現方案

設備狀態核心類

1.設備狀態圖
對每種不同狀態下的行爲作出了實現。例如在未登錄狀態下發生上行數據,或者心跳,會斷開鏈接,跳轉到了未連接狀態。在未登錄狀態下如果登錄成功了,則會進入到已登錄狀態。。。。
設備狀態

2.狀態模式代碼實現
描述在狀態切換過程中的所有行爲接口.

package com.yhy.state;

/**
 * describe:設備各種狀態下的行爲總和
 *
 * @author helloworldyu
 * @data 2018/3/27
 */
public interface IDeviceState {
    /**
     * 設備新建立鏈接
     * @param connectedTime 建立鏈接的時間
     * @param describe 描述在什麼時候進行的此動作
     */
    void onConnect(long connectedTime, String describe);

    /**
     * 斷開鏈接
     * @param describe 描述在什麼時候進行的此動作
     */
    void onDisconnect(String describe);

    /**
     * 登錄動作
     * @param deviceId 設備 id
     * @param lastUpdateTime 設備上行數據的時間
     * @param describe  描述在什麼時候進行的此動作
     */
    void onLoginSucc(String deviceId, long lastUpdateTime, String describe);

    /**
     * 登錄失敗
     * @param describe 描述在什麼時候進行的此動作
     */
    void onLoginFailed(String describe);

    /**
     * 只要有數據上報,都屬於心跳
     * @param lastUpdateTime  最新更新時間
     * @param describe 描述在什麼時候進行的此動作
     */
    void onHeartbeat(long lastUpdateTime, String describe);

    /**
     * 進入超時
     * @param describe
     */
    void onTimeout(String describe);

    /**
     * 返回當前狀態的名字
     */
    String getStateName();
}

狀態類的父類,提供了默認實現

package com.yhy.state;

/**
 * describe:所有狀態類的基類
 *
 * @author helloworldyu
 * @data 2018/3/27
 */
public abstract class AbstractState implements IDeviceState{
    protected DeviceStateContext stateCtx;

    public AbstractState( DeviceStateContext stateCtx) {
        this.stateCtx = stateCtx;
    }


    @Override
    public void onConnect(long connectedTime, String describe) {
        throw new IllegalStateException(getStateName()+" 此狀態不應該進行鏈接動作");
    }

    @Override
    public void onDisconnect(String describe) {
        throw new IllegalStateException(getStateName()+" 此狀態不應該進行斷開鏈接動作");
    }

    @Override
    public void onLoginSucc(String deviceId, long lastUpdateTime, String describe) {
        throw new IllegalStateException(getStateName()+" 此狀態不應該進行登錄動作");
    }

    @Override
    public void onLoginFailed(String describe) {
        throw new IllegalStateException(getStateName()+" 此狀態不應該進行登錄失敗動作");
    }

    @Override
    public void onHeartbeat(long lastUpdateTime, String describe) {
        throw new IllegalStateException(getStateName()+" 此狀態不應該進行心跳動作");
    }

    @Override
    public void onTimeout(String describe) {
        throw new IllegalStateException(getStateName()+" 此狀態不應該進行進入超時動作");
    }

}

未連接狀態類

package com.yhy.state;

/**
 * describe:未連接狀態
 *
 * @author helloworldyu
 * @data 2018/3/27
 */
public class NoConnectedState extends AbstractState{
    public NoConnectedState(DeviceStateContext ctx) {
        super(ctx);
    }

    @Override
    public void onConnect(long connectedTime, String describe) {
        stateCtx.setConnectTime(connectedTime);
        stateCtx.setState(new NoLoginState(this.stateCtx), describe);
    }

    @Override
    public void onDisconnect(String describe) {
        this.stateCtx.closeChannle(describe);
    }

    @Override
    public String getStateName() {
        return "noConnected";
    }
}

未登錄狀態類

package com.yhy.state;

/**
 * describe:未登錄狀態
 *
 * @author helloworldyu
 * @data 2018/3/27
 */
public class NoLoginState extends AbstractState{
    public NoLoginState(DeviceStateContext ctx) {
        super(ctx);
    }

    @Override
    public void onDisconnect(String describe) {
        this.stateCtx.closeChannle(describe);
    }

    @Override
    public void onLoginSucc(String deviceId, long lastUpdateTime, String describe) {
        //設置數據
        this.stateCtx.setDeviceId(deviceId);
        this.stateCtx.setLastUpdateTime(lastUpdateTime);
        //狀態轉移
        this.stateCtx.setState(new LoggedState(this.stateCtx),describe );
    }

    @Override
    public void onLoginFailed(String describe) {
        //爲登錄模式下,登錄失敗,直接斷開鏈接。
        this.stateCtx.closeChannle(describe);
    }
//
//  @Override
//  public void onHeartbeat(long lastUpdateTime, String describe) {
//      //未登錄狀態下,不允許發送除登錄包外的任何數據包,斷開鏈接
//      this.stateCtx.closeChannle(describe);
//  }


//
//  @Override
//  public void onTimeout(String describe) {
//      //在未登錄狀態下,超時無數據,直接斷開鏈接
//      this.stateCtx.closeChannle(describe);
//  }

    @Override
    public String getStateName() {
        return "noLogin";
    }
}

已登錄狀態類

package com.yhy.state;

/**
 * describe:
 *
 * @author helloworldyu
 * @data 2018/3/27
 */
public class LoggedState extends AbstractState{
    public LoggedState(DeviceStateContext stateCtx) {
        super(stateCtx);
    }

    @Override
    public void onDisconnect(String describe) {
        //直接關閉鏈接
        this.stateCtx.closeChannle(describe);
    }

    @Override
    public void onHeartbeat(long lastUpdateTime, String describe) {
        //把當前狀態放進去
        this.stateCtx.setState(this, describe );
        //狀態不變更新 lastUpdateTime
        this.stateCtx.setLastUpdateTime(lastUpdateTime);
    }

    @Override
    public void onTimeout(String describe) {
        //狀態模式設置爲超時狀態
        this.stateCtx.setState( new TimeoutState(this.stateCtx),describe );
    }

    @Override
    public String getStateName() {
        return "logged";
    }
}

超時狀態類

package com.yhy.state;

/**
 * describe:超時無數據狀態
 *
 * @author helloworldyu
 * @data 2018/3/27
 */
public class TimeoutState extends AbstractState{
    public static final int MAX_TIMEOUT = 3;


    /**
     * 進入超時狀態的次數,如果超過 3 次則斷開鏈接
     */
    private int count;

    public TimeoutState(DeviceStateContext stateCtx) {
        super(stateCtx);
        this.count=1;
    }


    @Override
    public void onTimeout(String describe) {
        //把當前狀態放進去
        this.stateCtx.setState(this, describe);
        this.count++;
        //連續 timeout 到一定次數就關閉連接,切換到 斷開鏈接狀態
        if( this.count >= MAX_TIMEOUT ){
            //斷開鏈接
            this.stateCtx.closeChannle(describe);
        }
    }

    @Override
    public void onHeartbeat(long lastUpdateTime, String describe) {
        //=======更新最後更新時間=========
        this.stateCtx.setLastUpdateTime(lastUpdateTime);
        //=======狀態轉換爲已登錄=========
        this.stateCtx.setState(new LoggedState(this.stateCtx), describe);
    }

    @Override
    public String getStateName() {
        return "timeout";
    }
}

設備當前狀態類

package com.yhy.state;

import io.netty.channel.Channel;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * describe:設備狀態切換類
 *
 * @author helloworldyu
 * @data 2018/3/27
 */
public class DeviceStateContext implements IDeviceState {
    /**
     * 是否開啓記錄所有的狀態轉變
     */
    boolean history;
    /**
     * 記錄狀態轉換的歷史
     */
    private static class HistoryInfoDTO{
        private String describe;
        private String state;

        public HistoryInfoDTO(String describe, String state) {
            this.describe = describe;
            this.state = state;
        }

        @Override
        public String toString() {
            return "HistoryInfoDTO{" +
                    "describe='" + describe + '\'' +
                    ", state='" + state + '\'' +
                    '}';
        }
    }
    List<HistoryInfoDTO> historyState = new ArrayList<>();
    /**
     *  防止競爭的讀寫鎖
     */
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();


    /**
     * 設備的上下文信息
     */
    private Channel channel;

    /**
     * 設備的 deviceId
     */
    private String deviceId;

    /**
     * 鏈接時間
     */

    private long connectTime;

    /**
     * 設備的上次更新時間
     */
    private long lastUpdateTime;

    /**
     * 設備當前狀態
     */
    private IDeviceState state;

    /**
     * @param channel 管理的 channel 信息
     */
    public DeviceStateContext(Channel channel) {
        this.channel = channel;
        setState(new NoConnectedState(this), "初始化");
    }

    /**
     * @param channel 管理的 channel 信息
     * @param history true 開始記錄歷史狀態
     */
    public DeviceStateContext(Channel channel, boolean history) {
        this.history = history;
        this.channel = channel;
        setState(new NoConnectedState(this),"初始化" );
    }

    ///////////////////////////get/set////////////////////////

    public Channel getChannel() {
        return channel;
    }

    public void setChannel(Channel channel) {
        this.channel = channel;
    }

    public String getDeviceId() {
        return deviceId;
    }

    public void setDeviceId(String deviceId) {
        this.deviceId = deviceId;
    }

    public long getConnectTime() {
        return connectTime;
    }

    public void setConnectTime(long connectTime) {
        this.connectTime = connectTime;
    }

    public long getLastUpdateTime() {
        return lastUpdateTime;
    }

    public void setLastUpdateTime(long lastUpdateTime) {
        this.lastUpdateTime = lastUpdateTime;
    }

    public IDeviceState getState() {
        return state;
    }

    public void setState(IDeviceState state, String describe) {
        this.state = state;
        //把每次切換的狀態加入到歷史狀態中
        historyState.add(new HistoryInfoDTO(describe,state.getStateName()));
    }


    ///////////////////////////狀態切換////////////////////////


    @Override
    public void onConnect(long connectTime, String describe) {
        lock.writeLock().lock();
        try {
            state.onConnect( connectTime,describe );
        }finally {
            lock.writeLock().unlock();
        }
    }

    @Override
    public void onDisconnect(String describe) {
        lock.writeLock().lock();
        try {
            state.onDisconnect(describe);
        }finally {
            lock.writeLock().unlock();
        }
    }

    @Override
    public void onLoginSucc(String deviceId, long lastUpdateTime, String describe) throws IllegalStateException{
        lock.writeLock().lock();
        try {
            state.onLoginSucc( deviceId, lastUpdateTime,describe );
        }finally {
            lock.writeLock().unlock();
        }
    }

    @Override
    public void onLoginFailed(String describe) {
        lock.writeLock().lock();
        try {
            state.onLoginFailed(describe);
        }finally {
            lock.writeLock().unlock();
        }
    }

    @Override
    public void onHeartbeat(long lastUpdateTime, String describe) {
        lock.writeLock().lock();
        try {
            state.onHeartbeat(lastUpdateTime,describe );
        }finally {
            lock.writeLock().unlock();
        }
    }


    @Override
    public void onTimeout(String describe) {
        lock.writeLock().lock();
        try {
            state.onTimeout(describe);
        }finally {
            lock.writeLock().unlock();
        }
    }

    @Override
    public String getStateName() {
        return null;
    }

    /**
     * 關閉鏈接
     */
    protected void closeChannle( String describe ){
        setState(new NoConnectedState(this),describe );
        //關閉此 channel
        this.channel.close();
    }


    @Override
    public String toString() {
        return "DeviceStateContext{" +
                " state=" + state.getStateName()  +
                ", channel=" + channel +
                ", deviceId='" + deviceId + '\'' +
                ", connectTime=" + connectTime +
                ", lastUpdateTime=" + lastUpdateTime +
                ", lock=" + lock +
                ", \nhistory=" + historyState +
                '}';
    }
}

下面是結合 netty 維護設備狀態。

**
設備狀態類的使用方法:
1.在設備鏈接上來的時候(channelActive) , new 出來並 調用 onConnecte() ,添加到 channle.attr 中
DeviceStateContext deviceStateContext = new DeviceStateContext(ctx.channel());
deviceStateContext.onConnect(System.currentTimeMillis());
2.在設備主動斷開鏈接的時候(channelInactive),從 channel 的 attr 中獲取出來並調用 onDisconnect()
3.發生異常的時候(exceptionCaught),從 channel 的 attr 中獲取出來並調用 onDisconnect()
4.在用戶超時的時候(userEventTriggered),從 channel 的 attr 中獲取出來並調用 onTimeout()
5.在有登錄成功的時候 調用 onLoginSucc() 在登錄失敗的時候調用 onLoginFailed()
6.在由普通的上行數據的時候調用 onHeartbeat()
**

設備狀態處理的 handler

package com.yhy;

import com.yhy.netty.ChannelAttribute;
import com.yhy.state.DeviceStateContext;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleStateEvent;


public class DeviceStateHandler extends SimpleChannelInboundHandler<String> {
    public static final ChannelAttribute<DeviceStateContext> session = new ChannelAttribute<>("state");

    //有數據可讀的時候觸發
    //登錄數據的格式 LOGIN:name,pass
    @Override
    public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        if( 0 == msg.length() ){
            return;
        }
        //處理消息
        System.out.println(getClass().getSimpleName() + "." + "channelRead0" + ctx.channel().remoteAddress() + ":" + msg);
        DeviceStateContext deviceStateContext = session.getAttributeValue(ctx);

        //是否是認證操作
        if( msg.startsWith("LOGIN") ){
            //登錄操作
            boolean result = login(ctx, msg);
            if( result ){
                //===========login ok,切換到已登錄狀態===============
                deviceStateContext.onLoginSucc("device-123",System.currentTimeMillis(),"設備認證通過");
                ctx.writeAndFlush("login ok\n");
            }else {
                //===========login false,切換到登錄失敗狀態==========
                deviceStateContext.onLoginFailed("設備認證失敗");
            }
        }else {
            //============狀態爲上行數據=============
            deviceStateContext.onHeartbeat(System.currentTimeMillis(),"設備上行了數據");
            //返回消息
            ctx.writeAndFlush("recvData ok\n");
        }
        System.out.println("channelRead0:"+deviceStateContext.toString());
    }


    /**
     * 空閒一段時間,就進行檢查 (當前時間-上次上行數據的時間) 如果大於設定的超時時間 設備狀態就就行一次 onTimeout
     * @param ctx
     * @param evt
     * @throws Exception
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        System.out.println(getClass().getSimpleName() + "." + "userEventTriggered" + ctx.channel().remoteAddress());
        if (evt instanceof IdleStateEvent) {
            DeviceStateContext deviceStateContext = session.getAttributeValue(ctx);
            long lastUpdateTime = deviceStateContext.getLastUpdateTime();
            long currentTimeMillis = System.currentTimeMillis();
            long intervalTime = currentTimeMillis - lastUpdateTime;

            if( intervalTime >10000 ){
                //==============發生超時,進入超時狀態==============
                deviceStateContext.onTimeout("設備發送了超時");
                System.out.println("userEventTriggered:"+deviceStateContext.toString());
            }
        }else {
            //不是超時事件,進行傳遞
            super.userEventTriggered(ctx,evt);
        }
    }

    //客戶端鏈接上來的時候觸發
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //鏈接成功
        DeviceStateContext deviceStateContext = new DeviceStateContext(ctx.channel(),true);
        //===========設置設備狀態爲 未登錄=================
        deviceStateContext.onConnect(System.currentTimeMillis(),"設備 active");
        //更新添加 state 屬性
        session.setAttribute(ctx,deviceStateContext);
        System.out.println("channelActive:"+deviceStateContext.toString());
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //================設置爲斷開================
        DeviceStateContext deviceStateContext = session.getAttributeValue(ctx);
        deviceStateContext.onDisconnect("設備 inactive");
        System.out.println("channelInactive:"+deviceStateContext.toString());
    }

    //異常的時候觸發
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //==============發生異常切換到斷開模式===============
        System.out.println("exceptionCaught:"+ cause.getMessage());
        DeviceStateContext deviceStateContext = session.getAttributeValue(ctx);
        deviceStateContext.onDisconnect("設備 exceptionCaught");
        System.out.println("exceptionCaught:"+deviceStateContext.toString());
    }


    private boolean login(ChannelHandlerContext ctx, String msg) {
        //獲取用戶名密碼 LOGIN:name,pass
        String info[] = msg.split(":");
        if( 2 != info.length ){
            return false;
        }
        String userAndPass = info[1];
        String info2[] = userAndPass.split(",");

        if( 2 != info2.length ){
            return false;
        }

        String user = info2[0];
        String pass = info2[1];

        //覈對用戶名密碼
        if( !user.equals("yhy") || !pass.equals("123") ){
            return false;
        }else {
            return true;
        }
    }
}

其他代碼
服務的啓動函數

package com.yhy;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.kqueue.KQueueEventLoopGroup;
import io.netty.channel.kqueue.KQueueServerSocketChannel;
import io.netty.util.concurrent.Future;

import java.util.Scanner;

public class LoginServer {
    private int PORT = 8080;
    //接收請求的 nio 池
    private EventLoopGroup bossGroup = new KQueueEventLoopGroup();
    //接收數據的 nio 池
    private EventLoopGroup workerGroup = new KQueueEventLoopGroup();


    public static void main( String args[] ){
        LoginServer loginServer = new LoginServer();
        try {
            loginServer.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Scanner in=new Scanner(System.in); //使用Scanner類定義對象
        in.next();

        loginServer.stop();
    }

    public void start() throws InterruptedException {
        ServerBootstrap b = new ServerBootstrap();
        //指定接收鏈接的 NioEventLoop,和接收數據的 NioEventLoop
        b.group(bossGroup, workerGroup);
        //指定server使用的 channel
        b.channel(KQueueServerSocketChannel.class);
        //初始化處理請求的編解碼,處理響應類等
        b.childHandler(new LoginServerInitializer());
        // 服務器綁定端口監聽
        b.bind(PORT).sync();
    }

    public void stop(){
        //異步關閉 EventLoop
        Future<?> future = bossGroup.shutdownGracefully();
        Future<?> future1 = workerGroup.shutdownGracefully();

        //等待關閉成功
        future.syncUninterruptibly();
        future1.syncUninterruptibly();
    }
}

netty 服務器初始化類,注意添加的 DeviceStateHandler 是我們的核心類。

package com.yhy;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateHandler;

import java.util.concurrent.TimeUnit;


public class LoginServerInitializer extends ChannelInitializer<SocketChannel>{
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        // 以("\n")爲結尾分割的 解碼器
        pipeline.addLast("framer",
                new DelimiterBasedFrameDecoder(2048, Delimiters.lineDelimiter()));
        //字符串編碼和解碼
        pipeline.addLast("decoder",new StringDecoder());
        pipeline.addLast("encoder",new StringEncoder());

        //檢測殭屍鏈接,超時沒有的登錄的斷開
        pipeline.addLast(new IdleStateHandler(0,0,10, TimeUnit.SECONDS));

        // 自己的邏輯Handler
        pipeline.addLast("deviceStateHandler",new DeviceStateHandler());
    }
}

源代碼:
https://gitee.com/yuhaiyang457288/netty-test
其中的 Login 模塊。

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