引言
WebSocket 是实现实时双向通信的核心技术,广泛应用于在线游戏、即时通讯、股票行情等场景。然而,WebSocket 连接的稳定性面临多种挑战:网络抖动、浏览器关闭、服务器重启、断电断网等。如何正确识别连接断开、设计可靠的心跳机制、实现优雅的断线重连,是每个实时应用开发者必须掌握的技能。本文将深入分析各种断开场景,并提供完整的处理方案。
WebSocket 断开场景分析
断开场景分类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| ┌─────────────────────────────────────────────────────────────────────┐ │ WebSocket 断开场景分类 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 客户端主动断开 │ │ ├── 正常关闭浏览器(触发 onClose) │ │ ├── 杀掉浏览器进程(触发 onClose + onError) │ │ ├── 关闭页面/刷新(触发 onClose) │ │ └── 调用 ws.close()(触发 onClose) │ │ │ │ 客户端异常断开 │ │ ├── 断电断网(TCP 假死,需心跳检测) │ │ ├── 网络切换(WiFi → 4G) │ │ ├── 网络超时(路由不可达) │ │ └── 防火墙拦截 │ │ │ │ 服务端主动断开 │ │ ├── 正常重启服务(触发客户端 onClose) │ │ ├── 调用 ws.terminate()(触发客户端 onClose + onError) │ │ └── 负载均衡器断开 │ │ │ │ 服务端异常断开 │ │ ├── 进程崩溃(kill -9) │ │ ├── 服务器断电 │ │ └── 网络故障 │ │ │ └─────────────────────────────────────────────────────────────────────┘
|
客户端断开场景处理
场景一:正常关闭浏览器
当用户正常关闭浏览器或关闭页面时,浏览器会发送 WebSocket 关闭帧,触发优雅的 TCP 四次挥手。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const WebSocket = require('ws');
wss.on('connection', (ws, req) => { const clientId = generateClientId(); console.log(`客户端 ${clientId} 连接建立`);
ws.on('close', (code, reason) => { console.log(`客户端 ${clientId} 连接关闭,代码: ${code},原因: ${reason}`); cleanupClient(clientId); }); });
|
| 关闭代码 |
含义 |
说明 |
1000 |
正常关闭 |
会话完成 |
1001 |
终端离开 |
浏览器导航离开 |
1005 |
无状态码 |
连接异常中断 |
1006 |
异常关闭 |
连接意外终止 |
场景二:杀掉浏览器进程
当浏览器进程被强制终止(如任务管理器结束进程),操作系统会关闭进程的所有网络端口,触发 TCP RST 包。
1 2 3 4 5 6 7 8 9 10 11
| ws.on('close', (code, reason) => { console.log(`连接关闭: ${code}`); cleanupClient(clientId); });
ws.on('error', (error) => { console.error(`连接错误: ${error.message}`); });
|
处理要点:
- 强制关闭会同时触发
error 和 close 事件
error 事件处理中不要重复执行清理逻辑
- 依赖
close 事件进行最终的资源释放
场景三:客户端断电断网
这是最难处理的场景。由于 TCP 连接的断开信息无法到达服务端,服务端会维持一个”僵尸连接”(TCP 假死)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| ┌─────────────────────────────────────────────────────────────────────┐ │ TCP 假死问题 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 客户端 网络层 服务端 │ │ │ │ │ │ │ │ ◄──── 断电/断网 ────► │ │ │ │ │ │ │ │ │ × 客户端消失 │ 服务端不知情 │ │ │ │ │ │ │ │ ◄── 服务端继续发送 ──│ │ │ │ 数据包 │ │ │ │ │ │ │ │ ── 无 ACK 响应 ──► │ │ │ │ (多次重试) │ │ │ │ │ │ │ │ 经过超时时间后 │ │ │ │ 服务端才感知断开 │ │ │ │ │ 问题:默认 TCP 超时时间很长(几分钟到几小时) │ │ 解决:应用层心跳机制 │ │ │ └─────────────────────────────────────────────────────────────────────┘
|
服务端断开场景处理
场景一:正常重启服务
服务端主动关闭连接时,会发送关闭帧,客户端收到后触发 onClose。
1 2 3 4 5 6 7 8 9 10 11
| const ws = new WebSocket('wss://example.com/ws');
ws.onclose = (event) => { console.log(`连接关闭,代码: ${event.code}`);
if (event.code === 1001) { console.log('服务器正在重启,5秒后重连'); setTimeout(reconnect, 5000); } };
|
场景二:服务端进程崩溃
服务端进程被 kill -9 强制终止时,与客户端杀掉浏览器类似,会触发异常关闭。
1 2 3 4 5 6 7 8 9 10 11
| ws.onerror = (error) => { console.error('WebSocket 错误:', error); };
ws.onclose = (event) => { if (event.code === 1006) { console.log('异常关闭,可能是服务端崩溃'); reconnect(); } };
|
心跳机制设计
为什么需要心跳
| 问题 |
原因 |
心跳解决方案 |
| TCP 假死 |
物理断网时 TCP 不会立即感知 |
定期发送心跳包 |
| NAT 超时 |
路由器会清理不活跃的连接映射 |
保持连接活跃 |
| 代理超时 |
Nginx/负载均衡器有读超时 |
防止被中间件断开 |
| 及时发现故障 |
无法区分慢网络和真断开 |
超时未收到心跳则判定断开 |
心跳方案设计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| ┌─────────────────────────────────────────────────────────────────────┐ │ WebSocket 心跳机制架构 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 客户端 服务端 │ │ │ │ │ │ │───────── ping ─────────────────►│ 1. 客户端定期发送 ping │ │ │ │ │ │ │◄──────── pong ──────────────────│ 2. 服务端回复 pong │ │ │ │ │ │ │───────── ping ─────────────────►│ 3. 间隔 heartBeatInterval │ │ │ (未收到响应) │ │ │ │ │ │ │ │ 超时 heartBeatTimeout │ │ │ │ │ │ │ │ 判定连接断开 ──► 触发重连 │ │ │ │ │ 参数建议: │ │ • heartBeatInterval: 10-30 秒(视场景调整) │ │ • heartBeatTimeout: 2-3 倍 interval │ │ │ └─────────────────────────────────────────────────────────────────────┘
|
客户端心跳实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
| class WebSocketClient { constructor(url) { this.url = url; this.ws = null; this.heartBeatInterval = 30000; this.heartBeatTimeout = 90000; this.reconnectInterval = 5000; this.heartBeatTimer = null; this.heartBeatTimeoutTimer = null; this.reconnectTimer = null; this.isManualClose = false; }
connect() { this.ws = new WebSocket(this.url);
this.ws.onopen = () => { console.log('WebSocket 连接成功'); this.startHeartBeat(); };
this.ws.onmessage = (event) => { const data = JSON.parse(event.data);
if (data.type === 'pong') { this.resetHeartBeatTimeout(); return; }
this.handleMessage(data); };
this.ws.onclose = (event) => { console.log(`连接关闭: ${event.code}`); this.stopHeartBeat();
if (!this.isManualClose) { this.scheduleReconnect(); } };
this.ws.onerror = (error) => { console.error('WebSocket 错误:', error); }; }
startHeartBeat() { this.heartBeatTimer = setInterval(() => { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'ping', time: Date.now() })); this.setHeartBeatTimeout(); } }, this.heartBeatInterval); }
setHeartBeatTimeout() { this.heartBeatTimeoutTimer = setTimeout(() => { console.error('心跳超时,连接可能已断开'); this.ws.close(); }, this.heartBeatTimeout); }
resetHeartBeatTimeout() { if (this.heartBeatTimeoutTimer) { clearTimeout(this.heartBeatTimeoutTimer); this.heartBeatTimeoutTimer = null; } }
stopHeartBeat() { if (this.heartBeatTimer) { clearInterval(this.heartBeatTimer); this.heartBeatTimer = null; } this.resetHeartBeatTimeout(); }
scheduleReconnect() { console.log(`${this.reconnectInterval}ms 后尝试重连`); this.reconnectTimer = setTimeout(() => { this.connect(); }, this.reconnectInterval); }
close() { this.isManualClose = true; this.stopHeartBeat(); if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); } this.ws.close(); }
handleMessage(data) { console.log('收到消息:', data); } }
const client = new WebSocketClient('wss://example.com/ws'); client.connect();
|
服务端心跳实现(Node.js)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| const WebSocket = require('ws');
class WebSocketServer { constructor(port) { this.wss = new WebSocket.Server({ port }); this.clients = new Map(); this.heartBeatInterval = 30000; this.heartBeatTimeout = 90000;
this.init(); }
init() { this.wss.on('connection', (ws, req) => { const clientId = this.generateClientId(); console.log(`客户端 ${clientId} 连接`);
this.clients.set(clientId, { ws, lastPing: Date.now() });
ws.on('message', (data) => { try { const msg = JSON.parse(data);
if (msg.type === 'ping') { ws.send(JSON.stringify({ type: 'pong', time: msg.time }));
const client = this.clients.get(clientId); if (client) { client.lastPing = Date.now(); } } else { this.handleMessage(clientId, msg); } } catch (e) { console.error('消息解析错误:', e); } });
ws.on('close', () => { console.log(`客户端 ${clientId} 断开`); this.clients.delete(clientId); });
ws.on('error', (error) => { console.error(`客户端 ${clientId} 错误:`, error); }); });
this.startHeartBeatCheck(); }
startHeartBeatCheck() { setInterval(() => { const now = Date.now();
for (const [clientId, client] of this.clients) { const elapsed = now - client.lastPing;
if (elapsed > this.heartBeatTimeout) { console.log(`客户端 ${clientId} 心跳超时,强制断开`); client.ws.terminate(); this.clients.delete(clientId); } } }, this.heartBeatInterval); }
generateClientId() { return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; }
handleMessage(clientId, msg) { console.log(`来自 ${clientId} 的消息:`, msg); } }
new WebSocketServer(8080);
|
断线重连策略
指数退避重连
简单的固定间隔重连在网络故障时可能给服务器造成压力,推荐使用指数退避策略:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class ReconnectStrategy { constructor() { this.baseInterval = 1000; this.maxInterval = 30000; this.maxRetries = 10; this.retryCount = 0; }
getNextInterval() { if (this.retryCount >= this.maxRetries) { return -1; }
const interval = Math.min( this.baseInterval * Math.pow(2, this.retryCount), this.maxInterval );
this.retryCount++; return interval; }
reset() { this.retryCount = 0; } }
|
完整重连逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| class ResilientWebSocket { constructor(url) { this.url = url; this.ws = null; this.reconnectStrategy = new ReconnectStrategy(); this.messageQueue = []; this.isReconnecting = false; }
connect() { this.ws = new WebSocket(this.url);
this.ws.onopen = () => { console.log('连接成功'); this.reconnectStrategy.reset(); this.flushMessageQueue(); };
this.ws.onclose = () => { this.attemptReconnect(); }; }
attemptReconnect() { if (this.isReconnecting) return; this.isReconnecting = true;
const interval = this.reconnectStrategy.getNextInterval();
if (interval === -1) { console.error('重试次数用尽,停止重连'); return; }
console.log(`${interval}ms 后重连...`);
setTimeout(() => { this.isReconnecting = false; this.connect(); }, interval); }
send(data) { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(data)); } else { this.messageQueue.push(data); } }
flushMessageQueue() { while (this.messageQueue.length > 0) { const msg = this.messageQueue.shift(); this.ws.send(JSON.stringify(msg)); } } }
|
心跳参数选择建议
| 场景 |
心跳间隔 |
超时时间 |
说明 |
| 即时通讯 |
10-15s |
30-45s |
需要快速感知断线 |
| 在线游戏 |
5-10s |
15-30s |
实时性要求高 |
| 股票行情 |
15-30s |
60-90s |
平衡实时性和开销 |
| IoT 设备 |
60-300s |
300-600s |
节省电量和流量 |
实验数据参考
| 心跳间隔 |
断电后感知时间 |
网络开销 |
适用场景 |
| 1s |
~167s |
高 |
高频交易 |
| 1min |
~6min 40s |
中 |
普通应用 |
| 10min |
~11-15min |
低 |
低频场景 |
注意:以上数据受操作系统 TCP 超时设置影响,不同平台可能有差异。
总结
WebSocket 连接断开处理的核心要点:
- 断开场景识别:正常关闭触发
onClose,异常断开同时触发 onError + onClose,断电断网产生 TCP 假死
- 心跳机制:客户端定期发送 ping,服务端回复 pong,超时未响应则判定断开
- TCP 假死处理:依赖应用层心跳,不能仅靠 TCP 连接状态
- 优雅重连:使用指数退避策略,避免对服务端造成冲击
- 消息队列:断线期间缓存消息,重连后补发
- 参数调优:根据业务场景选择合适的心跳间隔和超时时间
- 资源清理:连接断开后及时释放内存、关闭定时器、删除映射
通过完善的心跳机制和断线重连策略,可以构建高可用的实时通信系统,为用户提供稳定的连接体验。