门店终端有时候收不到后台推送的价格变更通知,要手动重启终端才能恢复。查日志发现 WebSocket 连接表面上还在,但实际已经断了——中间经过了一层 Nginx 反向代理,空闲连接 60 秒就被断开,但终端不知道,还以为连接正常。
前后花了两天把心跳和重连机制加上,记录下来。
一、问题:TCP 断开了但应用层不知道
WebSocket 是长连接,但中间经过负载均衡或 Nginx,空闲连接超时会被断开。如果没有心跳机制,客户端不会知道连接已断,继续往"死连接"上发消息,消息全部丢失。
判断连接是否存活,不能只看 WebSocket.readyState === OPEN,要靠应用层心跳。
二、心跳:客户端定时发 ping,服务端回 pong
客户端每 30 秒发一个心跳消息,服务端收到后回 pong。如果连续 3 次没收到 pong,判定连接已断,主动重连:
// 客户端伪代码
let ws, heartbeatTimer, missedPongs = 0;
function connect() {
ws = new WebSocket('wss://你的站/ws');
ws.onopen = () => {
missedPongs = 0;
heartbeatTimer = setInterval(sendPing, 30000);
};
ws.onmessage = (event) => {
if (event.data === 'pong') {
missedPongs = 0; // 收到pong重置计数
} else {
handleBusinessMessage(event.data);
}
};
ws.onclose = () => {
clearInterval(heartbeatTimer);
setTimeout(connect, 3000); // 3秒后重连
};
}
function sendPing() {
missedPongs++;
if (missedPongs >= 3) {
ws.close(); // 连续3次没pong,主动断开触发重连
return;
}
ws.send('ping');
}
关键参数:心跳间隔 30 秒,Nginx 空闲超时一般是 60 秒,30 秒一次心跳能保持连接不被断开。missedPongs 连续 3 次(90 秒)没收到 pong 才判定断开,避免网络抖动误判。
三、Nginx WebSocket 代理配置
Nginx 代理 WebSocket 要加 Upgrade 头,否则握手会失败:
location /ws {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s; # 心跳保持连接,超时放宽到1小时
proxy_send_timeout 3600s;
}
proxy_read_timeout 默认 60 秒,我们的心跳间隔 30 秒,理论上不会超时。但 Nginx 版本不同默认值可能不一样,显式设到 3600 秒最保险。
四、消息补发:断线期间的消息不能丢
客户端重连后,要告诉服务端"我最后收到的消息 ID 是多少",服务端把断线期间错过的消息补发过来:
// 重连时带上最后收到的消息ID
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'resume',
lastMsgId: localStorage.getItem('lastMsgId') || 0
}));
};
// 收到消息后记录ID
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
localStorage.setItem('lastMsgId', msg.id);
handleBusinessMessage(msg);
};
服务端收到 resume 请求后,从 lastMsgId 之后开始推送历史消息。这样断线期间错过的消息不会丢。
五、踩坑清单(5 条实际踩过的)
- WebSocket.readyState === OPEN 不代表连接真的活着,必须用心跳确认。
- 心跳间隔要小于 Nginx/负载均衡的空闲超时,否则连接中间就被断了。
- 重连要加指数退避,别每秒疯狂重连,服务器恢复时会被冲垮。
- 消息 ID 要持久化到 localStorage,终端重启后重连也能补发历史消息。
- Nginx 代理 WebSocket 必须加 Upgrade 和 Connection 头,否则握手 400。
| 方案 | 断线检测 | 消息不丢 | 复杂度 |
|---|---|---|---|
| 无心跳 | 慢(数分钟) | 否 | 低 |
| 心跳+重连 | 快(90秒) | 否 | 中 |
| 心跳+重连+补发 | 快 | 是 | 中高 |
这次终端消息推送的稳定性改造是在唯顿收银系统的终端推送模块上做的,我主要负责加 WebSocket 心跳检测、写断线重连逻辑和消息补发那几段,改完后消息丢失率从每天十几条降到 0。
复盘
- WebSocket 长连接必须有心跳,readyState 不可信。
- 心跳间隔小于中间代理的空闲超时,重连加指数退避。
- 消息补发靠客户端带 lastMsgId,服务端补发断线期间的历史消息。
以上是个人实践记录,各平台具体功能以官方实时信息为准。
你们做 WebSocket 推送时,心跳间隔一般设多少?