标签:WebSocket, Vue2, Node.js, ws, 心跳机制, 自动重连, 前后端通信
摘要
- ✅ 基础 WebSocket 连接
- ✅ 双向消息收发
- ✅ 心跳检测(防止连接超时)
- ✅ 客户端自动重连机制
- ✅ 错误处理与状态管理
- ✅ 可扩展的模块化设计
一、需求分析
1.1 项目背景
在现代 Web 应用中,实时性需求日益增长,如在线聊天、实时通知、数据监控等场景。传统的 HTTP 请求-响应模式无法满足低延迟、高并发的实时通信需求。WebSocket 提供了全双工通信通道,是实现实时功能的理想选择。
1.2 核心功能需求
功能 | 描述 |
---|---|
基础通信 | 前端 Vue2 页面与后端 Node.js 建立 WebSocket 连接,实现双向消息发送与接收 |
心跳机制 | 客户端定时向服务端发送心跳包,服务端响应,防止因网络空闲导致连接被中间代理或防火墙断开 |
自动重连 | 客户端检测到连接断开后,自动尝试重新连接,支持重试次数与间隔控制 |
连接状态管理 | 前端展示连接状态(连接中、已连接、断开、重连中) |
错误处理 | 捕获连接异常、消息解析错误等,并提供用户反馈 |
可扩展性 | 代码结构清晰,便于后续扩展更多业务逻辑(如鉴权、房间、广播等) |
1.3 非功能需求
- 稳定性:连接应尽可能保持稳定,断线能自动恢复
- 可维护性:前后端代码模块化,易于理解和维护
- 可测试性:提供简单 UI 验证功能
二、技术选型与介绍
2.1 技术栈
层级 | 技术 | 说明 |
---|---|---|
前端 | Vue2 + Vue CLI | 主流的前端框架,适合构建单页应用 |
前端通信 | 原生 WebSocket API | 浏览器原生支持,无需额外库 |
后端 | Node.js + ws |
轻量级、高性能的 WebSocket 库,社区活跃 |
包管理 | npm / yarn | 依赖管理 |
构建工具 | Webpack (via Vue CLI) | 前端打包 |
2.2 为什么选择 ws
?
ws
是 Node.js 中最流行的 WebSocket 实现之一,性能优秀,API 简洁。- 支持 WebSocket 协议的所有特性(包括子协议、扩展等)。
- 文档完善,社区支持良好。
- 轻量,不依赖 Express 等框架,可独立运行。
2.3 为什么使用原生 WebSocket 而非 Socket.IO?
虽然 Socket.IO
提供了自动重连、降级支持等便利功能,但本项目目标是:
- 深入理解 WebSocket 原理:手动实现心跳与重连,有助于掌握底层机制。
- 轻量化:避免引入大型库的开销,适合学习和轻量级场景。
- 可控性更强:自定义重连策略、心跳间隔等。
三、项目结构
bash
websocket-demo/
├── backend/
│ ├── server.js # WebSocket 服务端
│ └── package.json
├── frontend/
│ ├── src/
│ │ ├── main.js
│ │ ├── App.vue
│ │ ├── components/
│ │ │ └── WebSocketClient.vue # 核心 WebSocket 客户端组件
│ │ └── utils/
│ │ └── WebSocketService.js # 封装的 WebSocket 服务类
│ └── package.json
└── README.md
四、后端实现(Node.js + ws)
4.1 初始化项目
bash
mkdir backend && cd backend
npm init -y
npm install ws
4.2 编写 WebSocket 服务端 (server.js
)
javascript
const WebSocket = require('ws');
// 创建 WebSocket 服务器,监听 8080 端口
const wss = new WebSocket.Server({ port: 8080 });
// 存储所有客户端连接
const clients = new Set();
wss.on('connection', (ws) => {
console.log('New client connected');
clients.add(ws);
// 设置心跳响应
ws.isAlive = true; // 标记客户端是否存活
ws.on('pong', () => {
ws.isAlive = true;
});
// 监听客户端消息
ws.on('message', (data) => {
try {
const message = JSON.parse(data);
console.log('Received:', message);
// 广播消息给所有客户端(除发送者外)
clients.forEach((client) => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({
type: 'message',
data: message.data,
from: 'server',
timestamp: new Date().toISOString()
}));
}
});
} catch (err) {
console.error('Error parsing message:', err);
}
});
// 连接关闭
ws.on('close', () => {
console.log('Client disconnected');
clients.delete(ws);
});
// 连接错误
ws.on('error', (err) => {
console.error('Client error:', err);
});
});
// 心跳检测:每 30 秒检查一次客户端是否响应
const heartbeatInterval = setInterval(() => {
clients.forEach((ws) => {
if (!ws.isAlive) {
console.log('Client not responding, terminating connection');
return ws.terminate(); // 强制关闭无响应连接
}
ws.isAlive = false; // 下次心跳前未收到 pong 则标记为 false
ws.ping(() => {}); // 发送 ping,callback 可选
});
}, 30000); // 30 秒一次心跳检测
// 清理定时器
wss.on('close', () => {
clearInterval(heartbeatInterval);
});
console.log('WebSocket server is running on ws://localhost:8080');
4.3 后端核心逻辑说明
isAlive
标志:用于标记客户端是否存活。ping/pong
机制 :服务端发送ping
,客户端自动回复pong
,通过on('pong')
监听。- 心跳定时器 :每 30 秒遍历所有客户端,若未收到
pong
,则isAlive
为false
,强制关闭连接。 - 消息广播:收到消息后,解析并转发给其他在线客户端。
五、前端实现(Vue2)
5.1 初始化 Vue2 项目
bash
vue create frontend
# 选择 Vue 2 preset
cd frontend
5.2 创建 WebSocket 服务类 (src/utils/WebSocketService.js
)
这是核心封装,实现自动重连与状态管理。
javascript
class WebSocketService {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectInterval = 3000; // 重连间隔 3s
this.maxReconnectAttempts = 10; // 最大重连次数
this.reconnectAttempts = 0;
this.heartbeatInterval = null;
this.heartbeatTimeout = 45000; // 心跳超时时间(应大于服务端间隔)
this.onOpen = () => {};
this.onClose = () => {};
this.onMessage = () => {};
this.onError = () => {};
}
connect() {
if (this.ws) {
this.ws.close(); // 避免重复连接
}
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.reconnectAttempts = 0; // 重置重连计数
this.onOpen();
this.startHeartbeat();
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.onMessage(data);
} catch (err) {
console.error('Parse message error:', err);
this.onError(err);
}
};
this.ws.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason);
this.stopHeartbeat();
this.onClose(event);
// 自动重连逻辑
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`Reconnecting... attempt ${this.reconnectAttempts}`);
setTimeout(() => {
this.connect();
}, this.reconnectInterval);
} else {
console.warn('Max reconnect attempts reached');
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.onError(error);
};
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
} else {
console.warn('WebSocket is not open. ReadyState:', this.ws?.readyState);
}
}
close() {
if (this.ws) {
this.ws.close();
this.stopHeartbeat();
}
}
startHeartbeat() {
// 清除可能存在的旧定时器
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
// 每 25 秒发送一次心跳
this.heartbeatInterval = setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.send({ type: 'heartbeat', timestamp: Date.now() });
}
}, 25000);
}
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
// 设置回调函数(供 Vue 组件使用)
setCallbacks({ onOpen, onClose, onMessage, onError }) {
if (onOpen) this.onOpen = onOpen;
if (onClose) this.onClose = onClose;
if (onMessage) this.onMessage = onMessage;
if (onError) this.onError = onError;
}
}
// 单例模式导出
const socketService = new WebSocketService('ws://localhost:8080');
export default socketService;
5.3 创建 Vue 组件 (src/components/WebSocketClient.vue
)
vue
<template>
<div class="websocket-client">
<h2>WebSocket Client (Vue2)</h2>
<p>状态: <span :class="statusClass">{{ status }}</span></p>
<button @click="sendMessage" :disabled="status !== 'connected'">
发送消息
</button>
<button @click="manualClose" v-if="status === 'connected'">
手动断开
</button>
<button @click="reconnect" v-if="status === 'disconnected'">
重连
</button>
<div class="messages">
<h3>收到的消息:</h3>
<div v-for="(msg, index) in messages" :key="index" class="message">
[{{ msg.timestamp }}] {{ msg.data }}
</div>
</div>
</div>
</template>
<script>
import socketService from '@/utils/WebSocketService';
export default {
name: 'WebSocketClient',
data() {
return {
status: 'connecting', // connecting, connected, disconnected, reconnecting
messages: []
};
},
computed: {
statusClass() {
return {
'status-connecting': this.status === 'connecting',
'status-connected': this.status === 'connected',
'status-disconnected': this.status === 'disconnected',
'status-reconnecting': this.status === 'reconnecting'
};
}
},
methods: {
sendMessage() {
const msg = `Hello from Vue2 at ${new Date().toLocaleTimeString()}`;
socketService.send({ type: 'chat', data: msg });
},
manualClose() {
socketService.close();
this.status = 'disconnected';
},
reconnect() {
this.status = 'connecting';
socketService.connect();
}
},
created() {
// 设置 WebSocket 回调
socketService.setCallbacks({
onOpen: () => {
this.status = 'connected';
},
onClose: (event) => {
this.status = 'disconnected';
if (socketService.reconnectAttempts > 0) {
this.status = 'reconnecting';
}
},
onMessage: (data) => {
this.messages.push({
data: data.data,
timestamp: new Date().toLocaleTimeString()
});
},
onError: (error) => {
console.error('WebSocket error in component:', error);
}
});
// 初始化连接
socketService.connect();
},
beforeDestroy() {
// 组件销毁时关闭连接
socketService.close();
}
};
</script>
<style scoped>
.websocket-client {
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
max-width: 600px;
margin: 20px auto;
}
.status-connecting { color: #ff9800; }
.status-connected { color: #4caf50; }
.status-disconnected { color: #f44336; }
.status-reconnecting { color: #ff5722; }
button {
margin: 5px;
padding: 8px 16px;
cursor: pointer;
}
.messages {
margin-top: 20px;
max-height: 300px;
overflow-y: auto;
border: 1px solid #eee;
padding: 10px;
border-radius: 4px;
}
.message {
padding: 5px;
border-bottom: 1px solid #eee;
font-size: 14px;
}
</style>
5.4 在 App.vue
中使用组件
vue
<template>
<div id="app">
<WebSocketClient />
</div>
</template>
<script>
import WebSocketClient from './components/WebSocketClient.vue'
export default {
name: 'App',
components: {
WebSocketClient
}
}
</script>
六、运行项目
6.1 启动后端
bash
cd backend
node server.js
# 输出:WebSocket server is running on ws://localhost:8080
6.2 启动前端
bash
cd frontend
npm run serve
# 访问 http://localhost:8080
七、扩展与优化思路
7.1 安全性增强
- WSS (WebSocket Secure) :使用
wss://
替代ws://
,通过 HTTPS 传输。 - 身份验证 :在
on('connection')
时校验 Token 或 Session。 - 消息校验:对收到的消息进行 Schema 校验,防止恶意数据。
7.2 功能扩展
- 房间/频道:支持用户加入不同频道,实现群聊。
- 离线消息:结合数据库存储未送达消息。
- 消息持久化:使用 Redis 或数据库记录聊天历史。
- 服务端集群:使用 Redis Pub/Sub 实现多实例间消息同步。
7.3 性能优化
- 心跳间隔动态调整:根据网络状况自适应心跳频率。
- 消息压缩:对大消息使用 gzip 压缩。
- 连接池管理:限制最大连接数,防止资源耗尽。
7.4 前端优化
- 使用 Vuex 管理 WebSocket 状态:更适合复杂应用。
- TypeScript 支持:提升代码健壮性。
- WebSocket 封装为 Mixin 或 Composition API(Vue3)。
八、总结
本文通过一个完整的项目实践,详细展示了如何使用 Vue2 + Node.js + ws
构建一个具备心跳检测 和自动重连功能的 WebSocket 应用。我们从需求分析出发,合理选型,实现了前后端的完整通信逻辑,并封装了可复用的 WebSocket 服务类。
通过手动实现心跳与重连,我们深入理解了 WebSocket 的底层机制,避免了过度依赖第三方库,提升了系统的可控性与轻量化程度。
该项目可作为实时通信功能的起点,后续可根据实际业务需求进行扩展,如加入用户系统、消息加密、集群部署等。
参考资料
- MDN WebSocket API: developer.mozilla.org/zh-CN/docs/...
- Vue.js 官方文档: v2.vuejs.org/
- Node.js 官方文档: nodejs.org/
欢迎点赞、收藏、分享!如有疑问或建议,欢迎在评论区留言交流。