netty 超时,登录,心跳,状态模式等解问题

在使用 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 模块。

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值