Netty登录实现

594 阅读7分钟

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

转发自:

www.voidcn.com/article/p-c…