关于SpringBoot整合WebSocket
- 我们知道SpringBoot经典的语句就是约定大于配置,所以我们这里采用自动注入的方式进行添加webSocket
- 所以这里我们需要理解SpringBoot的自动注入原理,方便理解后进行别的功能添加。
我们介绍两种WebSocket进行整合
第一种不进行分组的方式进行连接订阅
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
@EnableWebSocketMessageBroker
public class WebSoketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
@Component
@ServerEndpoint("/webSocket/{uId}")
@Slf4j
public class WebSocketServerUtil {
private Session session;
private static CopyOnWriteArraySet<WebSocketServerUtil > webSocketSet = new CopyOnWriteArraySet<>();
private static ConcurrentHashMap<Long,WebSocketServerUtil > webSocketMap = new ConcurrentHashMap<>();
private Long uId = null;
@OnOpen
public void onOpen(Session session, @PathParam("uId") Long uId){
this.session = session;
this.uId = uId;
if(webSocketMap .containsKey(uId)){
webSocketMap .remove(uId);
webSocketMap .put(uId,this);
}else{
webSocketMap .put(uId,this);
webSocketSet.add(this);
}
log.info("【websocket消息】有新的连接,总数:{}",webSocketMap.size());
}
@OnClose
public void onClose(){
if(webSocketMap.containsKey(uId)){
webSocketMap.remove(uId);
//从set中删除
webSocketSet.remove(this);
}
log.info("【websocket消息】连接断开,总数:{}",webSocketSet.size());
}
@OnMessage
public void onMessage(String message){
log.info("【websocket消息】收到客户端发来的消息:{}",message);
}
public void sendMessage(String message){
try {
this.session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 发送自定义消息
*
* */
public static void sendInfo(String message,Long uId) throws Exception {
log.info("发送消息到:"+uId+",报文:"+message);
if(webSocketMap.containsKey(uId)){
webSocketMap.get(uId).sendMessage(message);
}else{
log.error("用户"+uId+",不在线!");
throw new Exception("连接已关闭,请刷新页面后重试");
}
}
}
后端推送消息
Long uId = new Long("1");
Map msgMap = new HashMap();
msgMap.put("step",1);
msgMap.put("type",2);
msgMap.put("msg","hello");
WebSocketServerUtil.sendInfo(JsonUtil.toJson(msgMap),uId);
前端JS
/**
* 初始化websocket连接
*/
function initWebSocket() {
let uId = 1;
var websocket = null;
if('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:8009/webSocket"+uId );
} else {
alert("该浏览器不支持websocket!");
}
websocket.onopen = function(event) {
console.log("建立连接");
websocket.send('Hello WebSockets!');
}
websocket.onclose = function(event) {
console.log('连接关闭')
reconnect(); //尝试重连websocket
}
//建立通信后,监听到后端的数据传递
websocket.onmessage = function(event) {
let data = JSON.parse(event.data);
//业务处理....
if(data.step == 1){
alert(data.msg);
}
}
websocket.onerror = function() {
// notify.warn("websocket通信发生错误!");
// initWebSocket()
}
window.onbeforeunload = function() {
websocket.close();
}
// 重连
function reconnect() {
console.log("正在重连");
// 进行重连
setTimeout(function () {
initWebSocket();
}, 1000);
}
第二种进行分组的方式进行连接订阅
这里我们采用了redisson,具体大家框架使用的是什么根据自己的框架进行调整
实现代码
<!-- websocket连接 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>sockjs-client</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>stomp-websocket</artifactId>
<version>2.3.3</version>
</dependency>
package com.ruoyi.framework.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* @Description WebSocket配置文件
* @Author GGBond
* @Date 2023-07-10 14:05
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic", "/all");
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/webServer").setAllowedOriginPatterns("*").withSockJS();
registry.addEndpoint("/queueServer").setAllowedOriginPatterns("*").withSockJS();//注册两个STOMP的endpoint,分别用于广播和点对点
}
}
package com.ruoyi.framework.config;
import com.ruoyi.framework.Interceptor.WebSocketInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
/**
* @Description 实现接口来配置Websocket请求的路径和拦截器。
* @Author GGBond
* @Date 2023-07-10 14:05
*/
@Configuration
@EnableWebSocket
public class WebSocketH5Config implements WebSocketConfigurer {
@Autowired
WebSocketHandler locusHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//handler是webSocket的核心,配置入口
registry.addHandler(locusHandler, "/myHandler/{destination}").setAllowedOrigins("*").addInterceptors(new WebSocketInterceptor());
}
}
package com.ruoyi.framework.core;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.utils.redis.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
@Service
@Slf4j
public class WebSocketService<T> extends BaseController {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private SimpMessagingTemplate template;
/**
* @param destination 发送的主题
* @param message 发送的消息
* @description: 发送到指定主题
* @author GGBond
*/
@Async(value = "threadPoolTaskExecutor")
public void sendTopicMessage(String destination, T message) {
template.convertAndSend("/topic/" + destination, message);
}
/**
* @param message 发送的消息
* @description: 发送到公共主题
* @author GGBond
*/
@Async(value = "threadPoolTaskExecutor")
public void sendTopicMessage(T message) {
template.convertAndSend("/topic/chat", message);
}
/**
* 给指定用户发送消息,并处理接收者不在线的情况
*
* @param receiver 消息接收者
* @param payload 消息正文
* @author GGBond
*/
@Async(value = "threadPoolTaskExecutor")
public void sendTopicMessageRedis(String receiver, T payload, Boolean flag) {
if (flag) {
template.convertAndSend("/topic/" + receiver, payload);
} else {
String listKey = Constants.WEBSOCKET_CODE_KEY + ":" + "/topic/" + receiver;
logger.info(MessageFormat.format("消息接收者{}还未建立WebSocket连接,消息【{}】将被存储到Redis的【{}】列表中", receiver, payload, listKey));
//存储消息到Redis中
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(payload);
RedisUtils.setCacheList(listKey, arrayList);
}
}
//给指定用户組发送消息
@Async(value = "threadPoolTaskExecutor")
public void sendTopicMessage(List<Long> userIds, T message) {
userIds.forEach(x -> {
template.convertAndSend("/topic/" + x, message);
});
}
//循环发送消息
public void sendTopicMessageList(List<Object> list, Long userIds) {
for (Object x : list) {
template.convertAndSend("/topic/" + userIds, x);
}
RedisUtils.deleteObject(Constants.WEBSOCKET_CODE_KEY + ":" + "/topic/" + userIds);
}
}
package com.ruoyi.framework.Interceptor;
import com.ruoyi.common.constant.Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.Map;
@Slf4j
public class WebSocketInterceptor implements HandshakeInterceptor {
//进入hander之前的拦截器
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
if (request instanceof ServletServerHttpRequest) {
String destination = request.getURI().toString().split("destination=")[1];
if (destination == null || "".equalsIgnoreCase(destination.trim())) return false;
log.info("当前session的destination={}", destination);
map.put(Constants.LOCUS_WEBSOCKET_DESTINATION, destination);
}
return true;
}
@Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
log.info("webSocket的afterHandshake拦截器!");
}
}
接下来比较重要,配置好我们的spring.factories文件,文件路径需要自己更换
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ruoyi.framework.core.WebSocketService,\
com.ruoyi.framework.config.WebSocketConfig
前端
这里用的是 SockJS。
npm install sockjs-client
npm install stompjs
import SockJS from 'sockjs-client';
import Stomp from 'stompjs';
export default {
data(){
return {
stompClient:'',
timer:'',
}
},
methods:{
initWebSocket() {
this.connection();
let that= this;
this.timer = setInterval(() => {
try {
that.stompClient.send("test");
} catch (err) {
console.log("断线了: " + err);
that.connection();
}
}, 5000);
},
connection() {
//后端的地址
let socket = new SockJS('http://10.10.91.4:8081/ws');
this.stompClient = Stomp.over(socket);
let headers = {
Authorization:''
}
this.stompClient.connect(headers,() => {
this.stompClient.subscribe('/topic/public', (msg) => {
console.log('广播成功')
console.log(msg);
},headers);
this.stompClient.send("/app/chat.addUser",
headers,
JSON.stringify({sender: '',chatType: 'JOIN'}),
)
}, (err) => {
console.log('失败')
console.log(err);
});
},
disconnect() {
if (this.stompClient) {
this.stompClient.disconnect();
}
},
},
mounted(){
this.initWebSocket();
},
beforeDestroy: function () {
this.disconnect();
clearInterval(this.timer);
}
}