spring mvc 结合 websocket 与前端js 实现 心跳检测机制 并 断连重试

先推荐大家一个前端socket在线工具
https://www.idcd.com/tool/socket

一、前端实验代码

<!--仅供测试-->
<!-- websocket的配置 -->
<script>
    var path = 'localhost:89/xxxx';//基础路径
    var ws;//websocket实例
    var lockReconnect = false;//避免重复连接
    var ipRanIp=Math.random()*10000;//随机一个
    var wsUrl = getwsurl();
    createWebSocket(wsUrl);
    function getwsurl() {
        //作兼容性连接
        if ('WebSocket' in window) {
            return "ws://" + path + "/ws.action?uid=1&uname=2&clientIp=3";
        } else if ('MozWebSocket' in window) {
            return "ws://" + path + "/ws" + uid;
        } else {
            return "http://" + path + "/ws/sockjs" + uid;
        }
    }

    function createWebSocket(url) {
        try {
            ws = new WebSocket(url);
            initEventHandle();
        } catch (e) {
            reconnect(url);
        }
    }

    function initEventHandle() {
        ws.onclose = function () {
            console.info("连接关闭");
            // reconnect(wsUrl);
        };
        ws.onerror = function () {
            console.info("传输异常");
            reconnect(wsUrl);
        };
        ws.onopen = function () {
            //心跳检测重置
            heartCheck.reset().start();
        };
        ws.onmessage = function (event) {
            console.info(event.data);

            //如果获取到消息,心跳检测重置
            heartCheck.reset().start();
        }
    }

    function reconnect(url) {
        if(lockReconnect) return;
        lockReconnect = true;
        //没连接上会一直重连,设置延迟避免请求过多
        setTimeout(function () {
            console.info("尝试重连..." + new Date().format("yyyy-MM-dd hh:mm:ss"));
            createWebSocket(url);
            lockReconnect = false;
        }, 5000);
    }


    //心跳检测,每20s心跳一次
    var heartCheck = {
        timeout: 20000,
        timeoutObj: null,
        serverTimeoutObj: null,
        reset: function(){
            clearTimeout(this.timeoutObj);
            clearTimeout(this.serverTimeoutObj);
            return this;
        },
        start: function(){
            var self = this;
            this.timeoutObj = setTimeout(function(){
                //这里发送一个心跳,后端收到后,返回一个心跳消息,
                //onmessage拿到返回的心跳就说明连接正常
                ws.send("HeartBeat" + new Date().format("yyyy-MM-dd hh:mm:ss"));
                console.info("客户端发送心跳:" + new Date().format("yyyy-MM-dd hh:mm:ss"));
                self.serverTimeoutObj = setTimeout(function(){//如果超过一定时间还没重置,说明后端主动断开了
                    ws.close();//如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
                }, self.timeout)
            }, this.timeout)
        }
    }
    //js中格式化日期,调用的时候直接:new Date().format("yyyy-MM-dd hh:mm:ss")
    Date.prototype.format = function(fmt) {
        var o = {
            "M+" : this.getMonth()+1,                 //月份
            "d+" : this.getDate(),                    //日
            "h+" : this.getHours(),                   //小时
            "m+" : this.getMinutes(),                 //分
            "s+" : this.getSeconds(),                 //秒
            "q+" : Math.floor((this.getMonth()+3)/3), //季度
            "S"  : this.getMilliseconds()             //毫秒
        };
        if(/(y+)/.test(fmt)) {
            fmt=fmt.replace(RegExp.$1, (this.getFullYear()+"").substr(4 - RegExp.$1.length));
        }
        for(var k in o) {
            if(new RegExp("("+ k +")").test(fmt)){
                fmt = fmt.replace(RegExp.$1, (RegExp.$1.length==1) ? (o[k]) : (("00"+ o[k]).substr((""+ o[k]).length)));
            }
        }
        return fmt;
    }

 // 页面关闭,对应关闭WebSocket,防止server 端出现异常
 window.onbeforeunload = function(event) {
     ws.onclose =function(){};
     ws.close();
 }
</script>

二、服务端

1、先说maven依赖

(由于我的项目 spring mvc 版本也是 4.3.29.RELEASE ,所以这里我选取 4.3.29.RELEASE 的版本)

   <!--spring-websocket-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
            <version>4.3.29.RELEASE</version>
        </dependency>
        
   <!--websocket-api-->
        <dependency>
            <groupId>javax.websocket</groupId>
            <artifactId>javax.websocket-api</artifactId>
            <version>1.1</version>
<!--这个参数可以解决报错:ClassCastException: org.apache.tomcat.websocket.server.WsServerContainer cannot be cast to javax.websocket.server.ServerContainer-->
            <scope>provided</scope>
        </dependency>
报错:ClassCastException: org.apache.tomcat.websocket.server.WsServerContainer cannot be cast to javax.websocket.server.ServerContainer
    <dependency>
            <groupId>javax.websocket</groupId>
            <artifactId>javax.websocket-api</artifactId>
            <version>1.1</version>
<!--这个参数可以解决报错:ClassCastException: org.apache.tomcat.websocket.server.WsServerContainer cannot be cast to javax.websocket.server.ServerContainer-->
            <scope>provided</scope>
        </dependency>
2、线程池 (可选) :
package cn.dbsec.task;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@EnableAsync
public class BeanConfig {

    /**
     * #websocket通信线程池配置专用,直接单核,未来可拓展*****************
     * #核心数
     * websocket.corePoolSize=1
     * #最大数
     * websocket.maximumPoolSize=1
     * #最大线程超时回收(秒)
     * websocket.keepAliveTime=5
     * #队列长
     * websocket.queueMax=200
     */

    //基础参数
    private int corePoolSize=2;//最小活跃线程数

    private int maximumPoolSize=2;//最大活跃线程数

    private int queueMax=2000;//空闲回收时间(秒)
	
    private int keepAliveTime=6;//空闲回收时间(秒)

    @Bean
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数
        executor.setCorePoolSize(corePoolSize);
        // 设置最大线程数
        executor.setMaxPoolSize(maximumPoolSize);
        // 设置队列容量
        executor.setQueueCapacity(queueMax);
        // 设置线程活跃时间(秒)
        executor.setKeepAliveSeconds(keepAliveTime);
        // 设置默认线程名称
        executor.setThreadNamePrefix("socket_thread-");
        // 设置拒绝策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 等待所有任务结束后再关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        return executor;
    }
}

3、拦截器实现 HandShakeInterceptor.cladd
package cn.dbsec.system.websocket;

import java.util.Map;

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

/**
 * 每次建立连接都会进行握手,这个拦截器是用于处理握手前后的预处理工作
 * @author ly
 *
 */
public class HandShakeInterceptor implements HandshakeInterceptor {

    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        System.out.println("将要进入WebSocket预处理过程");
        return true;
    }
 
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
    	System.out.println("WebSocket预处理中....");
    	System.out.println("webSocket预处理完毕");
    }
}
4、连接状态捕获 MyWebSocketHandler.class
package cn.dbsec.system.websocket;

import java.text.SimpleDateFormat;
import java.util.*;

import cn.dbsec.dbaa.pojo.system.WebProvingEntity;
import cn.dbsec.system.dao.WbProvingDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;

/**
 * 用于处理WebSocket的消息,这里是真正握手进行的处理
 * @author lei.yu
 */
@Component
public class MyWebSocketHandler implements WebSocketHandler {
	
	/*存放多个用户的List,解决多客户端访问的多线程问题,但是在实际测试过程中
	 * 并不时线程安全的,在将用户从List移除后,当服务端向客户端推送数据时会报错
	 * 因为在发送消息的方法里应该被移除的Session消息却进入了发送消息的环节,在执
	 * 行getBasicRemote().sendText(clientInfoJson)就会产生异常!!!一定注意,
	 * 
	 * 解决方案:不能通过线程安全的集合来保存Session解决。而应该保存整个类,并通过
	 * CopyOnWriteArraySet容器来操作。
	 */
	private volatile static List<WebSocketSession> users = new ArrayList<>();
	
	private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    /**
     * 成功建立连接后(这个连接此时处于待使用的状态),将会进入此方法,类似于@OnOpen这个注解,触发页面上的onopen方法
     */
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        System.out.println("进入真正的握手类:MyWebSocketHandler,WebSocket连接建立成功");
        //获取客户端IP
        String clientIp = session.getRemoteAddress().getAddress().getHostAddress();
        //处理上线的后续业务
        //this.setSocketLog(session,"add");
        /*
		 * 这边根据自己的需求进行消息的推送,这里是每搁4s向在线的客户端进行数据推送
		 * 这里实际不能做到每个客户端的都4s推送一次,因为每次来一个客户端都触发这个方法
		 */
		//暂时不需要向前台去验证,如需验证,请自己添加线程池
		/*threadPoolTaskExecutor.execute(()->{
			while(true) {
				for(WebSocketSession user:users) {//向每个在线的客户端推送消息,4秒推送一次
					try {
						synchronized (user){
							user.sendMessage(new TextMessage("服务器推送消息,试试是否客户端还在:" + sdf.format(new Date())));
						}
					} catch (IOException e) {
						e.printStackTrace();
					}
				}

				try {//服务器每5秒向每个在线的客户端推送消息
					Thread.currentThread().sleep(30000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		});*/
    	users.add(session);
    	System.out.println("在线用户" + users.size() + "人:" + users);
    }
 
    /**
     * 消息处理,在客户端通过Websocket API发送的消息会经过这里,然后进行相应的处理,相当与@OnMessage注解
     */
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
    	System.out.println("接收到客户端:" + session.getId() + "发送的消息:" + message.getPayload().toString());
    	//这里一定要对客户端的心跳作回应动作,不然会不断重连
    	session.sendMessage(new TextMessage("服务器的心跳回应-HeartBeat" + sdf.format(new Date())));
    }

    /**
     * 处理来自WebSocket消息传输的错误,类似与@OnError注解
     */
    public void handleTransportError(WebSocketSession session,Throwable exception) throws Exception {
        //一定要移除
        users.remove(session);
        //处理上线的后续业务
//        this.setSocketLog(session,"del");
        System.out.println("客户端" + session.getId() + "传输异常");
    }
 
    /**
     * 关闭连接后或者发生传输错误时将会调用该方法,尽管会话session可能此时仍然未关闭,但是不建议在此处给客户端发消息,因为
     * 极有可能会发送失败,类似于@OnClose
     */
    public void afterConnectionClosed(WebSocketSession session,CloseStatus closeStatus) throws Exception {
        //断开连接,清除数据表中的用户的那条数据即可
        //this.setSocketLog(session,"del");
        //这里一定要移除,不然传输会报错
        users.remove(session);
        session.close();
        System.out.println("Websocket客户端" + session.getId() + "已经关闭");
    }
 
    /**
     * 表示是否让WebSocket支持处理大文件的拆分处理,默认为false
     */
    public boolean supportsPartialMessages() {
    	//不需要进行大文件的拆分处理
        return false;
    }

    /**
     * 获取socket中的参数数据,把参数拆解出来
     */
    private Map setParamToMap(String query){
        //获取 ? 后面的参数
        Map<String, Object> mapRes = new HashMap<>();
        //拆解参数
        String[] params = query.split("&");
        Map paramMap = new HashMap<>();
        for (String param : params) {
            String[] keyValue = param.split("=");
            mapRes.put(keyValue[0], keyValue[1]);
        }
        return mapRes;
    }

}
5、对指定请求进行拦截 WebSocketConfig.class

这里要着重说明一下两个配置点:

1、由于websocket 请求其实也是 http 请求,所以该请求也是会经过我们 spring mvc 的 servlet 进行转发、捕获,所以这里需要看一下你捕获时候的规则配置,比如我的就是匹配 “ .action ” , 如图:
在这里插入图片描述
所以我们配置socket拦截路径时,也必须以 .action 结尾,不然前端调用时就会出现 404 :
在这里插入图片描述
2、客户端连接时的作用域问题:
这里我配置为所有,正常情况下,你需要配置为你客户端的 IP ,这样才能保证网络安全性。
在这里插入图片描述
源码:

package cn.dbsec.system.websocket;

import javax.annotation.Resource;

import org.springframework.stereotype.Component;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

/**
 * 配置WebSocket
 * @author ly
 */
@Component
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{

    @Resource
    MyWebSocketHandler handler;
 
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    	
    	//配置对指定的url进行拦截,暂时设置作用域为所有
        registry.addHandler(handler, "/ws.action").addInterceptors(new HandShakeInterceptor()).setAllowedOrigins("*");
 
        //允许客户端使用SokcetJs
        registry.addHandler(handler, "/ws/sockjs.action").addInterceptors(new HandShakeInterceptor()).withSockJS();
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

会飞的小蜗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值