websocket vue跟后台的交互

复制代码
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>