WebSocket连接异常问题分析与修复

PS:使用的是RuoYi-Vue-Plus:https://gitee.com/dromara/RuoYi-Vue-Plus,
对应的前端:ruoyi-plus-vben5
https://gitee.com/dapppp/ruoyi-plus-vben5

问题现象

用户登录时控制台出现"WebSocket消息已经订阅,跳过重复订阅"提示,导致WebSocket连接未成功建立。此问题仅当用户手动刷新浏览器后WebSocket才能重新连接成功,严重影响了实时消息功能的可用性。

根本原因分析

通过深入分析代码,我发现了以下几个关键问题:

1. 订阅状态管理异常

src/store/notify.ts 文件中,hasSubscribed 变量是模块级变量,不会在登录/登出时重置:

// 标记是否已经订阅了WebSocket消息
let hasSubscribed = false;

function startListeningMessage() {
  // 检查是否已经订阅
  if (hasSubscribed) {
    console.log('WebSocket消息已经订阅,跳过重复订阅');
    return;
  }
  // ...
  // 标记已经订阅
  hasSubscribed = true;
}

这导致即使token更新也不会重新建立WebSocket连接,因为 hasSubscribed 始终为true。

2. WebSocket单例模式问题

src/utils/message.ts 文件中,sharedWebsocket 变量同样是模块级的,在token更新后仍保持旧连接:

let sharedWebsocket: ReturnType<typeof useWebSocket> | undefined;

export function useWebSocketMessage() {
  if (!websocketEnable) {
    console.warn('当前未开启websocket.');
    return;
  }
  if (!sharedWebsocket) {
    sharedWebsocket = createWebsocket();
  }
  return sharedWebsocket;
}

3. 登录流程时序问题

src/store/auth.ts 文件中,登录成功后立即触发WebSocket连接,但此时token可能尚未完全设置(useChatStore为自己写的聊天模块):

// 登录成功后触发 WebSocket 连接
const notifyStore = useNotifyStore();
const chatStore = useChatStore();
notifyStore.startListeningMessage();
chatStore.connectWebSocket();

4. 连接初始化条件判断失误

useWebSocketMessage 函数仅在 sharedWebsocket 不存在时才创建新连接,未考虑token变化:

if (!sharedWebsocket) {
  sharedWebsocket = createWebsocket();
}

为什么手动刷新能解决问题?

  • 浏览器刷新会重新加载整个应用,包括所有模块级变量
  • 刷新后 hasSubscribedsharedWebsocket 变量会被重置为初始状态
  • 新的token会被正确用于建立WebSocket连接

修复方案

1. 修复 notify.ts 中的订阅逻辑

hasSubscribed 变量改为响应式变量,并在 $reset 方法中重置:

// 标记是否已经订阅了WebSocket消息
const hasSubscribed = ref(false);

function startListeningMessage() {
  // 检查是否已经订阅
  if (hasSubscribed.value) {
    console.log('WebSocket消息已经订阅,跳过重复订阅');
    return;
  }
  // ...
  // 标记已经订阅
  hasSubscribed.value = true;
}

function $reset() {
  // 重置订阅状态
  hasSubscribed.value = false;
  // notificationList.value = [];
}

2. 修复 message.ts 中的WebSocket管理

增加token变化检测和WebSocket连接重置机制:

// 记录上次使用的token
let lastToken: string | undefined;

export function useWebSocketMessage() {
  if (!websocketEnable) {
    console.warn('当前未开启websocket.');
    return;
  }
  
  // 检查token是否变化
  const accessStore = useAccessStore();
  const currentToken = accessStore.accessToken;
  
  // 如果没有连接或者token变化,创建新连接
  if (!sharedWebsocket || currentToken !== lastToken) {
    // 关闭旧连接
    if (sharedWebsocket) {
      console.log('Token变化,重新建立WebSocket连接');
      sharedWebsocket.close();
    }
    
    sharedWebsocket = createWebsocket();
    lastToken = currentToken;
  }
  
  return sharedWebsocket;
}

/**
 * 重置WebSocket连接
 */
export function resetWebSocket() {
  if (sharedWebsocket) {
    console.log('重置WebSocket连接');
    sharedWebsocket.close();
    sharedWebsocket = undefined;
    lastToken = undefined;
  }
}

3. 修复 auth.ts 中的登录流程

调整登录成功后WebSocket连接的触发时机,确保在token完全设置后再建立连接:

// 登录成功后触发 WebSocket 连接
// 确保在 token 完全设置后再建立连接
setTimeout(() => {
  console.log('登录成功,准备建立WebSocket连接');
  const notifyStore = useNotifyStore();
  const chatStore = useChatStore();
  
  // 重置之前的WebSocket连接
  resetWebSocket();
  // 重置订阅状态
  notifyStore.$reset();
  
  // 重新建立连接
  notifyStore.startListeningMessage();
  chatStore.connectWebSocket();
}, 100);

修复效果

  1. 登录时:不再出现"WebSocket消息已经订阅,跳过重复订阅"提示,连接能够成功建立
  2. 登出后再登录:WebSocket连接能够正确重新建立,无需手动刷新浏览器
  3. token变化时:自动检测token变化并重新建立连接
  4. 浏览器刷新:仍然能够正常连接,保持了原有的可靠性

技术原理

修复后的工作流程

  1. 登录流程:登录成功 → 设置token → 重置WebSocket连接 → 重置订阅状态 → 建立新连接
  2. token变化检测:每次调用 useWebSocketMessage 时检查token是否变化,变化则重新建立连接
  3. 状态管理:订阅状态现在存储在store中,会在登录/登出时正确重置

关键技术点

  1. 响应式状态管理:使用Vue 3的 ref 实现状态的响应式管理
  2. 单例模式优化:在单例模式中增加token变化检测,确保连接的时效性
  3. 异步时序控制:使用 setTimeout 确保token完全设置后再建立连接
  4. 状态重置机制:在关键节点重置连接和订阅状态,确保系统的一致性

总结

WebSocket连接异常问题的根本原因在于模块级变量的状态管理不当和连接初始化条件判断失误。通过将订阅状态改为响应式变量、增加token变化检测、优化登录流程时序,我们成功解决了这个问题,确保了WebSocket连接的可靠性和实时消息功能的可用性。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐