文章目录
先推荐大家一个前端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();
}
}