import { getToken } from "@/utils/auth.js";
import "./stomp.umd.js"
import moment from "moment";
interface WsSubscription {
unsubscribe: () => void;
}
interface WsClient {
ws: any;
isReConnect: boolean;
connected: boolean;
subscribers: {
[topic: string]: {
[key: string]: { sub: WsSubscription, cb: Function };
};
};
}
const wsClientStore: { [path: string]: WsClient } = {}
const resolveMap: { [k: string]: Function[] } = {}
const runtimeHost = `${window.location.hostname}:8080`;
const devOverrideHost = "10.192.30.139:8080";
let ip = import.meta.env.PROD ? runtimeHost : devOverrideHost;
function logWs(wsPath: string, message: string) {
let wsLog = undefined
const totalNum = 100
const msg = moment().format("YYYY-MM-DD HH:mm:ss ") + message
let wsLogStr: any = localStorage.getItem('websocket-log')
if (wsLogStr == undefined) {
wsLog = {}
wsLog[wsPath] = []
} else {
wsLog = JSON.parse(wsLogStr)
}
if (wsLog[wsPath] == undefined) {
wsLog[wsPath] = []
}
if (wsLog[wsPath].length < totalNum) {
wsLog[wsPath].push(msg)
} else {
wsLog[wsPath].push(msg)
const len = wsLog[wsPath].length
wsLog[wsPath] = wsLog[wsPath].slice(len - totalNum, len)
}
localStorage.setItem('websocket-log', JSON.stringify(wsLog))
}
//订阅
export function subscribeWsTopic(wsPath: string, topic: string, cb: any, subscriptionKey?: string): {
subscriptionKey: string;
unsubscribe: () => void
} {
let url = `ws://${ip}${wsPath}`;
const key = subscriptionKey || `${topic}_${Math.random().toString(36).slice(2)}`;
const subscribe = (client: any, _topic: string, _cb: any) => {
if (!wsClientStore[wsPath]) {
wsClientStore[wsPath] = {
ws: client,
isReConnect: false,
connected: false,
subscribers: {}
};
}
if (!wsClientStore[wsPath].subscribers[_topic]) {
wsClientStore[wsPath].subscribers[_topic] = {};
}
const stompSubscription = client.subscribe(_topic, _cb);
wsClientStore[wsPath].subscribers[_topic][key] = {
sub: stompSubscription,
cb: _cb
};
console.log(`订阅成功: ${url}, topic: ${_topic}, key: ${key}`);
return {
subscriptionKey: key,
unsubscribe: () => {
stompSubscription.unsubscribe();
if (wsClientStore[wsPath]?.subscribers?.[_topic]?.[key]) {
delete wsClientStore[wsPath].subscribers[_topic][key];
}
}
};
};
if (wsClientStore[wsPath] && wsClientStore[wsPath].connected) {
return subscribe(wsClientStore[wsPath].ws, topic, cb);
}
if (!wsClientStore[wsPath]) {
resolveMap[wsPath] = [];
const client = new (window["StompJs"] as any).Client({
brokerURL: `${url}`,
connectHeaders: {
login: ' ',
passcode: ' ',
Authorization: getToken(),
},
debug: function (str: any) {
// console.log(str);
},
reconnectDelay: 5000,
heartbeatIncoming: 40000,
heartbeatOutgoing: 40000,
});
client.onConnect = () => {
if (wsClientStore[wsPath]?.isReConnect) {
logWs(wsPath, "重连成功");
if (wsClientStore[wsPath]?.subscribers) {
Object.entries(wsClientStore[wsPath].subscribers).forEach(([storedTopic, topicSubscribers]) => {
Object.entries(topicSubscribers).forEach(([subKey, subscriberInfo]) => {
const reSub = client.subscribe(storedTopic, subscriberInfo.cb);
wsClientStore[wsPath].subscribers[storedTopic][subKey].sub = reSub;
console.log(`重连后,topic:${storedTopic}重新订阅成功`);
});
});
}
wsClientStore[wsPath].connected = true;
console.log(`websocket:${url}与服务端重连成功`);
} else {
wsClientStore[wsPath].connected = true;
wsClientStore[wsPath].isReConnect = true;
resolveMap[wsPath].forEach((func: any) => func(null));
resolveMap[wsPath] = [];
console.log(`websocket:${url}与服务端连接成功`);
logWs(wsPath, "首连成功");
}
};
client.onDisconnect = (evt: any) => {
wsClientStore[wsPath].connected = false;
console.log(`websocket:${url}与服务端连接断开`, evt);
logWs(wsPath, "连接断开");
};
client.onStompError = (evt: any) => {
console.log(`websocket:${url}出现错误`, evt);
logWs(wsPath, "错误");
};
wsClientStore[wsPath] = {
ws: client,
isReConnect: false,
connected: false,
subscribers: {}
};
client.activate();
}
// 等待连接建立后再订阅
return new Promise((resolve) => {
if (wsClientStore[wsPath].connected) {
resolve(null);
} else {
resolveMap[wsPath].push(resolve);
}
}).then(() => {
return subscribe(wsClientStore[wsPath].ws, topic, cb);
}) as Promise<{ subscriptionKey: string; unsubscribe: () => void }>;
}
//取消订阅
export function unSubscribeWsTopic(wsPath: string, topic: string, subscriptionKey?: string | null) {
if (!wsClientStore[wsPath]?.subscribers?.[topic]) {
console.warn(`未找到路径 ${wsPath} 下的 topic: ${topic}`);
return;
}
if (subscriptionKey) {
// 根据具体的subscriptionKey取消订阅
if (wsClientStore[wsPath].subscribers[topic][subscriptionKey]) {
wsClientStore[wsPath].subscribers[topic][subscriptionKey].sub.unsubscribe();
delete wsClientStore[wsPath].subscribers[topic][subscriptionKey];
console.log(`websocket:${wsPath}下 topic:${topic}下 key:${subscriptionKey} 取消订阅成功`);
logWs(wsPath, `topic:${topic}下 key:${subscriptionKey}取消订阅成功`);
} else {
console.warn(`未找到路径 ${wsPath} 下 topic: ${topic} 的订阅 key: ${subscriptionKey}`);
}
} else {
// 取消该topic下的所有订阅
for (let key in wsClientStore[wsPath].subscribers[topic]) {
wsClientStore[wsPath].subscribers[topic][key].sub.unsubscribe();
delete wsClientStore[wsPath].subscribers[topic][key];
}
console.log(`websocket:${wsPath}下 topic:${topic}下 所有订阅取消成功`);
logWs(wsPath, `${topic}取消订阅成功`);
}
}
在需要使用的页面添加代码如下
const refreshPage = (msg: any) => {
let data = JSON.parse(msg.body)
// console.log("upgrade websocket return data:", data);
if (Object.keys(data).length > 0) {
handleSearch();
}
};
subscribeWsTopic('/websocket/caltta-vm/', '/topic/vm_ne_upgrade_topic', refreshPage);
onUnmounted(() => {
unSubscribeWsTopic('/websocket/caltta-vm/', '/topic/vm_ne_upgrade_topic')
})
后台需要发送到页面的内容
private void send2VUE(UpgradeTask upgradeTask){
SpringUtils.getBean(SimpMessagingTemplate.class).convertAndSend(WebSocketTopicConstant.NE_UPGRADE_STATUS, JSONUtil.toJsonStr(upgradeTask));
}
websocket配置
spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration= \
com.caltta.common.websocket.config.BrokerWebSocketConfig
主题
package com.caltta.common.websocket.constant;
import com.caltta.common.websocket.config.BrokerWebSocketConfig;
/**
* WebSocket所有topic
*/
public class WebSocketTopicConstant {
/**网元升级状态变化**/
public static final String NE_UPGRADE_STATUS = BrokerWebSocketConfig.TOPIC_PATH + "vm_ne_upgrade_topic";
}
package com.caltta.common.websocket.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;
@Configuration
@EnableWebSocketMessageBroker
public class BrokerWebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* stomp.js的订阅地址
* 示例: const client = new StompJs.Client({
* brokerURL: 'ws://localhost:8080/websocket/caltta-cm/',
* connectHeaders: {
* Authorization: 'Bearer abcedsdf...'
* },
* reconnectDelay: 5000,
* heartbeatIncoming: 120000,
* heartbeatOutgoing: 120000,
* });
*/
public static final String SUBSCRIBE_PATH = "/websocket/";
public static final String TOPIC_PATH = "/topic/";
private TaskScheduler messageBrokerTaskScheduler;
@Value("${spring.application.name}")
private String applicationName;
@Autowired
public void setMessageBrokerTaskScheduler(@Lazy TaskScheduler taskScheduler) {
this.messageBrokerTaskScheduler = taskScheduler;
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// /websocket/caltta-cm
registry.addEndpoint(SUBSCRIBE_PATH + applicationName + "/").setAllowedOrigins("*");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker(TOPIC_PATH)
.setHeartbeatValue(new long[]{120000, 120000})
.setTaskScheduler(this.messageBrokerTaskScheduler);
}
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
// 256KB
registration.setMessageSizeLimit(256 * 1024);
// 2MB
registration.setSendBufferSizeLimit(2 * 1024 * 1024);
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new CommonInboundChannelInterceptor());
}
}
package com.caltta.common.websocket.config;
import com.caltta.common.core.constant.CacheConstants;
import com.caltta.common.core.constant.TokenConstants;
import com.caltta.common.core.utils.JwtUtils;
import com.caltta.common.core.utils.SpringUtils;
import com.caltta.common.core.utils.StringUtils;
import com.caltta.common.redis.service.RedisService;
import io.jsonwebtoken.Claims;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
public class CommonInboundChannelInterceptor implements ChannelInterceptor {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
// access authentication header(s)
String token = accessor.getFirstNativeHeader(TokenConstants.AUTHENTICATION);
String realToken = getToken(token);
boolean result = this.authenticationHeader(realToken);
if(!result){
return null;
}else{
//accessor.setUser(user);
}
}
return message;
}
private String getToken(String token) {
// 如果前端设置了令牌前缀,则裁剪掉前缀
if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX)) {
token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
}
return token;
}
private boolean authenticationHeader(String token) {
if (StringUtils.isEmpty(token)) {
return false;
}
Claims claims = JwtUtils.parseToken(token);
if (claims == null) {
return false;
}
String userkey = JwtUtils.getUserKey(claims);
RedisService redisService = SpringUtils.getBean(com.caltta.common.redis.service.RedisService.class);
boolean islogin = redisService.hasKey(getTokenKey(userkey));
if (!islogin) {
return false;
}
String userid = JwtUtils.getUserId(claims);
String username = JwtUtils.getUserName(claims);
if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) {
return false;
}
return true;
}
private String getTokenKey(String token) {
return CacheConstants.LOGIN_TOKEN_KEY + token;
}
}
pom依赖
<dependencies>
<dependency>
<groupId>com.caltta</groupId>
<artifactId>caltta-common-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>com.caltta</groupId>
<artifactId>caltta-common-security</artifactId>
</dependency>
</dependencies>