WebSocket架构重构:从分散管理到统一连接的实战经验
前言
在开发基于Vue 3 + WebSocket的实时通信系统时,我们遇到了一个看似简单但影响深远的问题:聊天消息能够实时推送,但通知消息却需要刷新页面才能显示。这个问题的根源在于WebSocket连接管理的架构设计缺陷。本文将详细记录从问题发现到架构重构的完整过程,希望能为遇到类似问题的开发者提供参考。
问题背景
系统架构概述
我们的系统、,包含以下核心功能:
- 实时聊天
- 实时通知
技术栈:
- 前端:Vue 3 + Vuetify + Pinia
- 后端:Spring Boot + WebSocket + STOMP
- 实时通信:WebSocket + SockJS
问题现象
系统上线后,用户反馈了一个奇怪的现象:
- ✅ 聊天消息:能够实时接收和发送
- ❌ 通知消息:需要刷新页面才能看到新通知
- ❌ 特定用户群体:管理员和不使用聊天功能的用户完全收不到实时通知
问题分析
初步排查
首先检查了后端的通知发送逻辑:
java
@Service
public class NotificationServiceImpl implements NotificationService {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Override
public void sendNotificationToUser(Long userId, NotificationDTO notification) {
try {
// 发送到用户特定的通知队列
messagingTemplate.convertAndSendToUser(
userId.toString(),
"/queue/notifications",
notification
);
log.info("通知已发送给用户 {}: {}", userId, notification.getTitle());
} catch (Exception e) {
log.error("发送通知失败,用户ID: {}, 错误: {}", userId, e.getMessage());
}
}
}
后端逻辑看起来没有问题,消息确实在发送。
前端WebSocket订阅检查
检查前端的消息处理逻辑:
javascript
// messageHandler.js
export const messageHandler = {
handleNotification(notification) {
console.log('收到通知:', notification);
// 添加到通知store
const notificationStore = useNotificationStore();
notificationStore.addNotification(notification);
// 显示全局通知
eventBus.emit('show-notification', {
title: notification.title,
text: notification.content,
// ... 其他配置
});
}
};
前端的消息处理逻辑也是正确的。
关键发现:WebSocket连接时机问题
深入调查后发现了问题的根源:WebSocket连接只在用户访问聊天功能时才建立!
原有的连接逻辑:
javascript
// 只有在ChatManagement.vue组件挂载时才连接WebSocket
onMounted(async () => {
await chatStore.connectWebSocket(); // 只有访问聊天页面才会执行
});
这导致了以下问题:
- 管理员通常不使用聊天功能,从未建立WebSocket连接
- 不聊天的用户无法接收实时通知
- 架构不合理:通知功能依赖于聊天功能的副作用
解决方案设计
架构重构目标
- 统一连接管理:所有WebSocket操作由统一的store管理
- 登录即连接:用户登录后自动建立WebSocket连接
- 智能重连:网络断开后自动重连,支持指数退避
- 功能解耦:聊天、通知等功能独立,不相互依赖
新架构设计
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ WebSocket │ │ Chat Store │ │ Notification │
│ Store │◄───┤ │ │ Store │
│ │ │ │ │ │
│ - 连接管理 │ │ - 消息处理 │ │ - 通知处理 │
│ - 重连策略 │ │ - 聊天状态 │ │ - 通知状态 │
│ - 订阅管理 │ └──────────────────┘ └─────────────────┘
└─────────────────┘
▲
│
┌────┴────┐
│ main.js │
│ 应用启动 │
└─────────┘
实施过程
第一步:创建统一的WebSocket Store
javascript
// store/websocket.js
import { defineStore } from 'pinia'
import { webSocketService } from '@/utils/websocket'
export const useWebSocketStore = defineStore('websocket', {
state: () => ({
connected: false,
connecting: false,
reconnectAttempts: 0,
maxReconnectAttempts: 5,
reconnectInterval: 1000, // 初始重连间隔1秒
maxReconnectInterval: 30000, // 最大重连间隔30秒
connectionHistory: []
}),
actions: {
async connect() {
if (this.connected || this.connecting) {
console.log('WebSocket已连接或正在连接中');
return;
}
try {
this.connecting = true;
console.log('开始建立WebSocket连接...');
await webSocketService.connect();
this.connected = true;
this.connecting = false;
this.reconnectAttempts = 0;
this.reconnectInterval = 1000; // 重置重连间隔
this.addConnectionHistory('connected');
console.log('WebSocket连接成功');
// 通知其他store连接状态变化
this.notifyConnectionChange(true);
} catch (error) {
console.error('WebSocket连接失败:', error);
this.connecting = false;
this.scheduleReconnect();
}
},
scheduleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('达到最大重连次数,停止重连');
return;
}
this.reconnectAttempts++;
const delay = Math.min(
this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1),
this.maxReconnectInterval
);
console.log(`${delay/1000}秒后进行第${this.reconnectAttempts}次重连...`);
setTimeout(() => {
this.connect();
}, delay);
},
// 通知其他store连接状态变化
notifyConnectionChange(connected) {
// 通知聊天store
const chatStore = useChatStore();
chatStore.onWebSocketConnectionChange(connected);
// 通知通知store
const notificationStore = useNotificationStore();
notificationStore.onWebSocketConnectionChange(connected);
}
}
});
第二步:重构Chat Store
移除WebSocket连接逻辑,专注于聊天功能:
javascript
// store/chat.js
export const useChatStore = defineStore('chat', {
actions: {
// 移除connectWebSocket方法,改为检查连接状态
checkWebSocketConnection() {
const websocketStore = useWebSocketStore();
if (!websocketStore.connected) {
console.log('WebSocket未连接,请求连接...');
websocketStore.connect();
}
return websocketStore.connected;
},
// WebSocket连接状态变化时的回调
onWebSocketConnectionChange(connected) {
this.wsConnected = connected;
if (connected) {
console.log('WebSocket已连接,聊天功能可用');
// 重新订阅聊天相关频道
this.subscribeToChannels();
} else {
console.log('WebSocket断开,切换到轮询模式');
// 启动轮询作为备用方案
this.startPollingIfNeeded();
}
}
}
});
第三步:应用启动时初始化WebSocket
javascript
// main.js
import { useWebSocketStore } from '@/store/websocket'
import { useUserStore } from '@/store/user'
const app = createApp(App)
// 应用启动后初始化WebSocket
app.mount('#app').$nextTick(async () => {
try {
const userStore = useUserStore()
const websocketStore = useWebSocketStore()
// 检查用户登录状态
await userStore.checkLoginStatus()
// 如果用户已登录,建立WebSocket连接
if (userStore.isLoggedIn) {
console.log('用户已登录,初始化WebSocket连接')
await websocketStore.connect()
}
} catch (error) {
console.error('应用初始化失败:', error)
}
})
第四步:用户登录/登出时管理连接
javascript
// store/user.js
export const useUserStore = defineStore('user', {
actions: {
async login(credentials) {
try {
const response = await authApi.login(credentials)
// ... 登录逻辑
// 登录成功后建立WebSocket连接
const websocketStore = useWebSocketStore()
await websocketStore.connect()
} catch (error) {
console.error('登录失败:', error)
throw error
}
},
async logout() {
try {
// 断开WebSocket连接
const websocketStore = useWebSocketStore()
websocketStore.disconnect()
// ... 登出逻辑
} catch (error) {
console.error('登出失败:', error)
}
}
}
})
第五步:修复组件中的调用
将所有组件中的 connectWebSocket()
调用替换为 checkWebSocketConnection()
:
javascript
// 修复前
await chatStore.connectWebSocket()
// 修复后
chatStore.checkWebSocketConnection()
涉及的文件:
ChatManagement.vue
ChatRoom.vue
GlobalNotification.vue
测试验证
测试场景
-
管理员登录测试
- 登录后立即建立WebSocket连接
- 能够实时接收竞标通知
-
网络断开重连测试
- 模拟网络断开
- 验证自动重连机制
- 验证指数退避策略
-
多标签页测试
- 同一用户多个标签页
- 验证连接共享和状态同步
-
功能独立性测试
- 不访问聊天页面也能收到通知
- 聊天功能不影响通知功能
测试结果
✅ 所有测试场景通过
✅ 实时通知功能正常
✅ 聊天功能不受影响
✅ 自动重连机制工作正常
性能优化
连接复用
javascript
// 避免重复连接
if (this.connected || this.connecting) {
return; // 直接返回,不重复建立连接
}
智能重连策略
javascript
// 指数退避算法
const delay = Math.min(
this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1),
this.maxReconnectInterval
);
内存管理
javascript
// 组件卸载时清理订阅
onUnmounted(() => {
// 不断开全局WebSocket连接,只清理组件特定的订阅
websocketStore.unsubscribeComponent(componentId);
});
经验总结
架构设计原则
- 单一职责:每个store只负责自己的业务逻辑
- 依赖倒置:高层模块不依赖低层模块的实现细节
- 开闭原则:对扩展开放,对修改封闭
最佳实践
- 统一管理:WebSocket连接应该在应用层统一管理
- 生命周期绑定:连接生命周期与用户登录状态绑定
- 错误处理:完善的重连机制和错误处理
- 状态同步:多个store之间的状态同步机制
常见陷阱
- 循环依赖:避免store之间的循环引用
- 重复连接:确保连接的唯一性
- 内存泄漏:及时清理事件监听器和订阅
- 状态不一致:确保所有相关组件的状态同步
后续优化方向
- 连接池管理:支持多个WebSocket连接
- 消息队列:离线消息的缓存和重发
- 性能监控:连接质量和消息延迟监控
- A/B测试:不同重连策略的效果对比
结语
这次WebSocket架构重构解决了一个看似简单但影响用户体验的关键问题。通过统一连接管理、智能重连策略和清晰的职责分离,我们不仅修复了通知功能,还为系统的后续扩展奠定了坚实的基础。
在实际开发中,架构设计的重要性往往在问题出现时才被重视。希望这次的经验分享能够帮助其他开发者在设计阶段就考虑到这些问题,避免后期的大规模重构。
关键要点回顾:
- WebSocket连接应该与用户登录状态绑定,而不是与特定功能绑定
- 统一的连接管理比分散的连接管理更可靠
- 完善的错误处理和重连机制是生产环境的必需品
- 清晰的架构设计能够避免功能间的不必要耦合
本文记录了一次真实的WebSocket架构重构经历,所有代码示例均来自实际项目。如果你在类似项目中遇到相关问题,欢迎交流讨论。