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