WebSocket连接总是断?可能是你打开方式不对

2026-05-01 10 0

上周五晚上,正当我准备美滋滋地过周末时,线上突然炸了。用户反馈聊天消息发不出去,实时数据不更新,客服电话被打爆。我赶紧上服务器看日志,好家伙,WebSocket连接数直接飙到十几万,内存占用爆炸。

这不是我第一次被WebSocket坑了,但绝对是死得最惨的一次。今天就把这些年踩过的坑整理一下,希望你们别重蹈覆辙。

第一个坑:以为WebSocket是长生不老的

很多人以为,建立WebSocket连接之后就可以躺着睡大觉了。朋友,你太天真了。

网络世界比你想象的脆弱多了。网络波动、负载均衡器超时、NAT会话过期、还有各种奇奇怪怪的网络设备,都可能导致连接悄无声息地断掉。用户还以为在聊天,实际上消息全发给了空气。

解决方案:心跳检测+自动重连。这俩是WebSocket的保命套装,缺一不可。

// 心跳检测示例
const HEARTBEAT_INTERVAL = 30000; // 30秒发一次心跳
const HEARTBEAT_TIMEOUT = 5000;   // 5秒没响应就判定为挂了

let heartbeatTimer = null;
let heartbeatTimeoutTimer = null;

function startHeartbeat() {
  clearInterval(heartbeatTimer);
  clearTimeout(heartbeatTimeoutTimer);
  
  heartbeatTimer = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify({ type: ping }));
      
      // 等待pong响应
      heartbeatTimeoutTimer = setTimeout(() => {
        console.warn("心跳超时,连接可能已死");
        ws.close();
      }, HEARTBEAT_TIMEOUT);
    }
  }, HEARTBEAT_INTERVAL);
}

ws.onmessage = (event) => {
  if (event.data === "pong") {
    clearTimeout(heartbeatTimeoutTimer);
  }
};

第二个坑:把所有消息都塞进一个连接

有些同学喜欢在一个WebSocket连接里传输所有类型的数据:聊天消息、实时通知、系统报警、文件传输......结果呢?一个大文件传半天,其他小消息全被堵死在队列里。

更骚的操作是,有人居然用WebSocket传GB级别的文件。我不知道该说你胆子大还是脑子有坑。

解决方案:分通道、按优先级。不同类型的数据走不同的连接,或者干脆用专门的协议。文件传输这种大块头,老老实实用TCP直连或者S3预签名URL吧。

第三个坑:不考虑后端水平扩展

单机WebSocket玩得溜,上了集群就抓瞎。用户连上了Server A,消息发到了Server B,两边谁也不认识谁。

我见过最离谱的方案是把所有WebSocket请求都路由到同一台机器。恭喜你,成功把分布式系统变成了单机系统,顺便把扩展性也一起埋葬了。

解决方案:Redis发布/订阅 + 消息队列。用户连接状态存Redis,消息通过消息队列广播,所有服务器节点都能收到。这样用户无论连到哪台机器,都能收到消息。

# Redis Pub/Sub 架构简图

用户A --WS--> Server A --publish--> Redis Channel:room_001
用户B --WS--> Server B --subscribe---/
用户C --WS--> Server C --subscribe---/

当用户A发消息时,Server A发布到Redis,所有订阅的服务器都能收到,然后转发给各自连接的用户

第四个坑:没有优雅关闭的概念

很多团队部署新版本时,直接kill -9把服务停掉。结果呢?用户手里的连接全部断开,正在填的表单、正在传输的文件,全部打水漂。用户一脸懵逼,投诉电话响个不停。

解决方案:平滑关闭四部曲。

  1. 先把负载均衡器从池子里摘掉,停止接收新连接
  2. 给所有连接发送关闭通知,让客户端开始重连
  3. 等待一段时间(比如30秒),让现有连接完成手头工作
  4. 最后才真正停掉服务
// 平滑关闭示例(Node.js)
process.on("SIGTERM", async () => {
  console.log("收到SIGTERM,开始平滑关闭...");
  
  // 1. 通知所有连接准备关闭
  wss.clients.forEach((client) => {
    client.send(JSON.stringify({ 
      type: "server_closing", 
      message: "服务器即将重启,请做好准备" 
    }));
    client.close();
  });
  
  // 2. 等待一段时间让连接关闭
  await new Promise(resolve => setTimeout(resolve, 30000));
  
  // 3. 真正退出
  process.exit(0);
});

第五个坑:忽视安全

WebSocket因为是持久连接,很容易成为攻击者的目标。连接数耗尽、恶意大量消息、跨站WebSocket劫持......这些问题不重视,分分钟被人按在地上摩擦。

解决方案:

  • 限制单个IP的最大连接数
  • 消息频率限制
  • 使用WSS(WebSocket Secure)
  • 验证Origin头
  • 对消息内容做校验和过滤

写在最后

WebSocket是个好技术,但它不是银弹。用好了是神器,用砸了是灾难。

我的建议是:能不用WebSocket就不用,如果你的场景可以用轮询解决,那就轮询。毕竟轮询虽然low,但胜在稳定、简单、好排查问题。等真正遇到实时性需求了,再上WebSocket。

记住,最好的架构不是最先进的那个,而是最适合当前业务规模和团队能力的那一个。别为了技术而技术。

好了,我去修bug了,周末什么的,不存在的。

相关文章

一次诡异的数据库死锁,帮你彻底搞懂事务隔离级别
为什么你的API总被吐槽?这份避坑指南让你少走三年弯路
你的接口不快,不是代码的问题——是你测量姿势错了
当我们在谈论高并发时,我们到底在谈什么?
懒得折腾?让小龙虾帮你一键部署AI工具,省心省力还省钱
懒得折腾?让小龙虾帮你一键部署AI工具,省心省力还省钱

发布评论