客户端异常断网断电,服务端该如何感知? 您所在的位置:网站首页 win10lsp协议异常断网了 客户端异常断网断电,服务端该如何感知?

客户端异常断网断电,服务端该如何感知?

#客户端异常断网断电,服务端该如何感知?| 来源: 网络整理| 查看: 265

本文只针对于web项目。开始想的是使用websocket进行长连接服务,但有个问题就来了,客户端异常断电、异常断网,比如说我现在把电脑炸了,网线掐了,服务端是不知道的,所以无法触发oncolse方法,通道就没办法关闭,该咋办呢?而且还有个缺点,如果用户过多,A用户向服务器发送10000次心跳,那么服务器也要回10000次,压力会很大。

解决方案

采用心跳机制解决。

客户端定时向服务端发送空消息(ping),服务端启动心跳检测,超过一定时间范围没有新的消息进来就默认为客户端已断线,服务端主动执行close()方法断开连接

Netty是一个非常强大的NIO通讯框架,支持多种通讯协议,并且在网络通讯领域有许多成熟的解决方案。

本文是借鉴了一个老哥的文章,但整合发现了问题,无法实现我需要的功能。于是自己对后端和前端代码进行了整改。大概说一下后端代码的意思把,其实也没啥说的。初始化netty,初始化信道,添加handler、设定端口、设定路由、开启监听类,大概就这些把。

具体看一下代码:

1.返回结果类

package org.springframework.security.web.netty; /** * 返回结果类 * @Author xiaoxin * @Date 2021/12/6 20:42 * @Version 1.0 */ public class CommonResult { private Integer code; private String msg; private T data; public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public T getData() { return data; } public void setData(T data) { this.data = data; } public CommonResult(Integer code, String msg) { this.code = code; this.msg = msg; } @Override public String toString() { return "CommonResult{" + "code=" + code + ", msg='" + msg + '\'' + ", data=" + data + '}'; } }

2.netty服务的相关变量和其他变量

package org.springframework.security.web.netty; import java.util.concurrent.ConcurrentHashMap; /** * @describe Netty服务相关的全局变量 * @Author xiaoxin * @Date 2021/12/6 19:26 * @Version 1.0 */ public class Constant { /**端口*/ public static final int PROT = 9502; /**请求头数据*/ public static final String NONCE = "nonce"; public static final String USER_ID = "userId"; public static final String SYS_TIME = "systime"; public static final String NONCE_VALUE = "12345"; public static final String OPEN_API_KEY = "openapikey"; public static final String OPEN_API_KEY_VALUE = "a702b8e6147"; /**key为channelId value为uid 存储在Map中*/ public static ConcurrentHashMap uidMap = new ConcurrentHashMap(); /**测试域环境地址 正式*/ // public static final String LOGOUT_URL = "http://10.248.68.123:7081/gateway/auth/logout"; /**测试域环境地址 测试*/ public static final String LOGOUT_URL = "http://10.248.68.123:8080/gateway/auth/logout"; }

3.时间工具类

package org.springframework.security.web.netty; import java.text.SimpleDateFormat; import java.util.Date; /** * @describe 时间工具类 * @Author xiaoxin * @Date 2021/12/7 17:19 * @Version 1.0 */ public class DateUtil { /** * 获取年月日时分秒的纯数字拼接 * 例如:2021-12-07 17:28:32 * 20211207172832 * @return */ public static String getTime() { Date d = new Date(); SimpleDateFormat sbf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println(sbf.format(d)); String format = sbf.format(d); String str = format .replaceAll("-", "") .replaceAll(":","") .replaceAll(" ",""); return str; } }

4.心跳超时处理类

package org.springframework.security.web.netty; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.timeout.IdleState; import io.netty.handler.timeout.IdleStateEvent; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; /** * @Author xiaoxin * @Date 2021/12/4 23:26 * @Version 1.0 */ @Slf4j public class HeartBeatInspect extends ChannelInboundHandlerAdapter{ @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { //超时事件 if (evt instanceof IdleStateEvent) { log.info("心跳检测超时"); IdleStateEvent idleEvent = (IdleStateEvent) evt; //读 if (idleEvent.state() == IdleState.READER_IDLE) { //关闭通道连接 ctx.channel().close(); //根据信道id获取到用户id,然后根据用户id进行令牌失效 String userId = Constant.uidMap.get(ctx.channel().id().toString()).toString(); System.gc(); log.info("心跳检测超时,即将销毁令牌,用户id:{}" + userId); //token令牌失效操作 RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.add(Constant.SYS_TIME, DateUtil.getTime()); headers.add(Constant.OPEN_API_KEY, Constant.OPEN_API_KEY_VALUE); headers.add(Constant.NONCE, Constant.NONCE_VALUE); headers.add(Constant.USER_ID, userId); HttpEntity requestEntity = new HttpEntity(null, headers); ResponseEntity exchange = restTemplate.exchange(Constant.LOGOUT_URL, HttpMethod.GET, requestEntity, CommonResult.class); log.info("{}",exchange); //写 } else if (idleEvent.state() == IdleState.WRITER_IDLE) { //全部 } else if (idleEvent.state() == IdleState.ALL_IDLE) { } } super.userEventTriggered(ctx, evt); } }

5.websocketHanler,当用户登陆成功后,此类会监听到

package org.springframework.security.web.netty; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import lombok.extern.slf4j.Slf4j; /** * @Author xiaoxin * @Date 2021/12/4 23:26 * @Version 1.0 */ @Slf4j public class WebSocketHandler extends SimpleChannelInboundHandler { /** * 当一个 client 连接到 server 时,Java 底层 NIO 的 ServerSocketChannel 就会有一个 SelectionKey.OP_ACCEPT * 的事件就绪,接着就会调用到 NioServerSocketChannel 的 doReadMessages(),然后通过ChannelPipeline调用到channelRead方法 * channelRead * @param ctx * @param msg * @throws Exception */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof FullHttpRequest) { //拦截请求地址,获取地址上的uid值,并存入Map集合中 FullHttpRequest fh = (FullHttpRequest) msg; String uid = fh.uri().substring(fh.uri().lastIndexOf("/") + 1); Constant.uidMap.put(ctx.channel().id().toString(), uid); log.info("信道id:{}",ctx.channel().id()); log.info("用户连接:{}",uid); // uri改为 /ws fh.setUri("/ws"); } super.channelRead(ctx, msg); } /** * 由于项目架构原因,前端框架在点击导航栏模块、刷新网页都会全部加载js,刷新网页则会 * 触发信道失效操作,测试更改后无果,所以在信道失效后删除了直接删除当前信道,原本是 * 只在心跳超时后才会删除信道。 * 每刷新一次就会创立一个信道,刷新次数过多会造成过多不必要的信道,相当于垃圾,当然 * 信道和其他对象也是一样的,当在一定时间未使用信道,则会被销毁 * @param ctx * @throws Exception */ @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { //获取Uid String uid = Constant.uidMap.get(ctx.channel().id().toString()).toString(); log.info("该用户已断线:{}",uid); Constant.uidMap.remove(ctx.channel().id().toString()); System.gc(); } @Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception { } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { } @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { } @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { } }

6.netty启动类

package org.springframework.security.web.netty; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import org.springframework.stereotype.Component; /** * @Author xiaoxin * @Date 2021/12/4 23:24 * @Version 1.0 */ @Component public class WebSocketServer { public void start(int port){ EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap //设置主从线程组 .group(bossGroup, workerGroup) //设置nio双向通道 .channel(NioServerSocketChannel.class) //子处理器,用于处理workerGroup .childHandler(new WebSocketServerInitializer()); //用于启动server,同时启动方式为同步 ChannelFuture channelFuture = bootstrap.bind(port).sync(); //监听关闭的channel,设置同步方式 channelFuture.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } }

6.初始化信道,负责添加各种handler,注意添加顺序,这里包括了对心跳的监控时间的设定

package org.springframework.security.web.netty; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.handler.timeout.IdleStateHandler; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import java.util.concurrent.TimeUnit; /** * @describe WebSocket初始化类 * @Author xiaoxin * @Date 2021/12/4 23:25 * @Version 1.0 */ @Configuration @EnableWebSocket public class WebSocketServerInitializer extends ChannelInitializer { @Override protected void initChannel(SocketChannel ch) throws Exception { //获取流水线 ChannelPipeline pipeline = ch.pipeline(); //websocket基于http协议,所以要有http编解码器 pipeline.addLast(new HttpServerCodec()); //对写大数据的支持 pipeline.addLast(new ChunkedWriteHandler()); //对httpMessage进行整合,聚合成FullHttpRequest或FullHttpResponse pipeline.addLast(new HttpObjectAggregator(1024 * 64)); //心跳检测,读超时时间设置为40s,0表示不监控 ch.pipeline().addLast(new IdleStateHandler(40, 0, 0, TimeUnit.SECONDS)); //心跳超时处理事件 ch.pipeline().addLast(new HeartBeatInspect()); //自定义handler pipeline.addLast(new WebSocketHandler()); //websocket指定给客户端连接访问的路由:/ws,ws是只针对于http,wss是https pipeline.addLast(new WebSocketServerProtocolHandler("/ws")); } }

7.最后一步,监听tomcat启动和销毁,因为是ssm项目,如果你在tomcat中启动一个其他的服务,默认都是使用主线程启动的,比如8080,所以需要另外起个线程去启动netty,要不然会启动失败。在tomcat启动后,启动netty服务。

package org.springframework.security.web.netty; /** * @Author xiaoxin * @Date 2021/12/7 16:25 * @Version 1.0 * @describe 监听tomcat启动与销毁 */ import lombok.extern.slf4j.Slf4j; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; @Slf4j public class NettyListener implements ServletContextListener{ /** * tomcat项目中启动netty,因为都是默认使用主线程启动的,如果一起启动,则只会启动一个服务 * 防止启动失败,在tomcat启动完成后,再使用其他线程启动netty * @param sce */ @Override public void contextInitialized(ServletContextEvent sce) { log.info("Tomcat初始化开始-------------------------"); new Thread(){ @Override public void run(){ try { new WebSocketServer().start(Constant.PROT); } catch (Exception e) { e.printStackTrace(); } } }.start(); log.info("Tomcat初始化结束-------------------------"); } @Override public void contextDestroyed(ServletContextEvent sce) { log.info("Tomcat销毁-----------------------------"); } }

8.前端代码,js代码

        前端代码解释:

                1.在登陆成功后调用此js代码

                2.创建wobsocket对象,new WebSocket(),其中的url则是服务器的地址,例如           127.0.0.1、192.168.122.001、或者其他,本地测试的话一般都是什么127.0.0.1或者localhost,如果部署项目到线上报错WebSocket connection to 'ws://127.0.0.1:8888/failed的话,你需要看下你的路径有没有错,服务器是否有nginx,有没有配置代理?如果本地是可以的,是通的,就可以认为是服务器的问题。

                3.页面静止20分钟自动关闭连接服务,一般正常的都是超过20分钟不操作,就关闭连接,这里做的操作不是关闭连接,而是停止发送信息,服务器是监听心跳的,如果在30-40秒内没有收到信息,则调用销毁token操作,你再次刷新页面的时候,token失效了,直接会跳转登录页

                4.判断网络状态,有人说handlerRemoved方法中其实是有对网络作出判断的,这里我是没办法解决的,因为在进行测试的时候,发现了一个问题,new WebSocket()当服务器开启了8080端口后,前端建立连接去访问,然后这个时候你断网了,然后看浏览器的console打印出的信息,你会发现还在继续发送信息,明明断网了,后来问了运维,即便是没有外网也是可以发送信息的,走的是内网。那这样的话只能去判断网络状态了。

//测试域-测试环境nginx代理路径 var url = "ws://10.248.68.123:端口/ws/"; //测试域-正式环境nginx代理路径 // var url = "ws://10.248.68.123:端口/ws/"; var timeCheck = false var timers = null window.onload = function (){ window.CHAT = { socket: null, init: function () { console.log("Execution is about to begin WebSocket Server") if (window.WebSocket) { //nginx代理地址,测试域-测试环境 CHAT.socket = new WebSocket(url + getUserId()); CHAT.socket.onopen = function () { console.log("Websocket Now Open!!!!"); CHAT.heartBeat('1'); console.log("Websocket Creating a Success..."); }, CHAT.socket.onclose = function () { console.log("连接关闭..."); clearInterval(timers) }, CHAT.socket.onerror = function () { console.log("发生错误..."); }, CHAT.socket.onmessage = function (e) { console.log("接收到消息" + e.data); } } else { alert("浏览器不支持websocket协议..."); } }, heartBeat: function (val) { if (CHAT.socket) { timers = setInterval(function () { console.log('Send HearBeat Server......') CHAT.socket.send(val); }, 10000); } } }; CHAT.init(); } //页面静止20分钟自动关闭连接服务,也就是用户在任何一个界面超过指定时间不操作,则调用 //clearInterval,这个方法的意思就是停止给服务器发送信息 var timeOut = null var timeOut = setTimeout(function (){ console.log("Nothing for 20 minutes! Close the service......") clearInterval(timers) },20*1000*60) //判断网络是否连接,if条件是IE浏览器,IE浏览器对于判断网络状态的话,是比较好用的 //例如断网后,他不会立马触发,而是有一定的重连机制,在一定时间重连,网络中断不会触发 if(isIE()){ var EventUtil = { addHandler: function (element, type, handler) { if(element.addEventListener) { element.addEventListener(type, handler, false); }else if (element.attachEvent) { element.attachEvent("on" + type, handler); }else { element["on" + type] = handler; } } }; EventUtil.addHandler(window, "online", function () { console.log("连上网了!") timeCheck = false }); EventUtil.addHandler(window, "offline", function () { console.log("网络不给力,请检查网络设置!") timeCheck = true setTimeout(function() { if(timeCheck){ clearInterval(timers) } }, 10000) }); //else条件是其他的浏览器,很灵敏,哪怕出现网络波动就会触发已断网 //所以加了setTimeout方法,触发断网后等待10秒,如果网络再次连接,则不会触发clearInterval //如果没有连接,则触发clearInterval }else { const netWorkDownlink = navigator.connection.downlink; if (navigator.connection && navigator.connection.onchange === null) { navigator.connection.onchange=function () { if (netWorkDownlink !== navigator.connection.downlink || navigator.connection.rtt === 0) { //网络断开 console.log('已断网') timeCheck = true setTimeout(function () { if(timeCheck){ clearInterval(timers) } }, 10000) } else if (netWorkDownlink === navigator.connection.downlink && navigator.connection.rtt !== 0) { //连接到网络 timeCheck = false console.log('网络已连接') } } } }

最后:     1.这样子做有个好处,A用户发送10000条信息给服务端,服务端是不用回复信息的,减轻服务器压力     2.可以判断出客户端是否处于异常状态,异常断网断电,也做了类似重连的功能     3.关于WebSocketHandler方法中的handlerRemoved方法,之前在网上查询,说是此方法对于网络断开       是做了判断的,说是在linunx中配置ipv4的配置,我没有去配。但有个疑问,我这个断网都能访问,发送       信息,走的是内网,是如何判断的?如何触发的?如有大佬知道,指点一下     4.关于创建信道,然后用户断网了,销毁tokne之后,其实我之前是担心创建信道过多,造成很多垃圾,       其实这个问题不必担心,设计之初会考虑的,像对象一样,时间久了,会进行类似于gc的操作     5.记得,一定要配置代理!!!!!WebSocket connection to 'ws://127.0.0.1:8888/failed,就是代理问题    6.感觉感知客户端断网这里,做的好像不是那么合理,但我对不合理的部分做出了优化。希望有经验的老哥提示一下。

  7.记得netty服务一定要另外起线程去启动,上面方法中有说到

  8.去咨询了上次交流的那个老哥,他说域名加端口就可以了呀,客户端断网了,咋还会给服务端发信息呢?后来才知道,问了下运维的,如果是使用外网的话,会很不安全,所以需要走内网。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有