物聯網交流羣: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 模塊。