WebSocket 与实时功能实现指南:聊天、通知与实时更新

实时功能已经成为现代网站的常见需求——在线客服要即时对话、后台要有实时通知、文档要多人协作编辑、仪表盘数据要自动刷新。实现这些功能,最核心的技术就是 WebSocket。这篇文章从方案选型讲起,给出一套可以直接跑起来的前后端示例,再聊聊生产环境里容易踩的坑,避免“代码跑通了、上线就断线”的尴尬;文末附上官方文档链接,需要深挖协议细节时可以顺着继续读。

WebSocket vs SSE vs 轮询

特性 轮询 SSE WebSocket
通信方向 客户端→服务端 服务端→客户端 双向
实时性 取决于轮询间隔 即时 即时
连接开销 每次请求都有 保持长连接 保持长连接
浏览器支持 所有 除 IE 外 除 IE 外
适用场景 简单定时刷新 推送通知 双向交互

简单判断法:如果只是“服务端有新数据要推给前端”(新评论、订单状态、股价),SSE 就够,代码量只有 WebSocket 的一半;只有需要“客户端也随时发消息、服务端也随时回”(聊天、协作编辑、游戏),才值得上 WebSocket。轮询适合那些改动频率低、几分钟刷一次也无所谓的页面,图个省事。实际选型时,把“是否双向”作为第一判断标准,可以少走很多弯路。

WebSocket 服务端(Node.js + ws)

npm install ws
// server.js
const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });

const clients = new Set();

server.on('connection', (ws) => {
  clients.add(ws);
  console.log('Client connected. Total:', clients.size);

  // 接收消息
  ws.on('message', (message) => {
    const data = JSON.parse(message);
    
    // 广播给所有客户端
    clients.forEach(client => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(JSON.stringify({
          type: 'message',
          user: data.user,
          content: data.content,
          time: new Date().toISOString()
        }));
      }
    });
  });

  // 断开连接
  ws.on('close', () => {
    clients.delete(ws);
    console.log('Client disconnected. Total:', clients.size);
  });
});

console.log('WebSocket server running on port 8080');

这是一个最简的广播型聊天室服务端:每个连接加入一个 Set,收到消息就转发给所有在线客户端。生产环境里,这套逻辑还要加上用户身份校验、消息落库、心跳保活,甚至拆到多个进程做横向扩展。首次部署时建议先用一个房间或频道做隔离,避免所有消息都混进同一个广播里。鉴权也要在连接层做:服务端收到连接后先校验 token,未通过的连接直接关闭,比等消息进来再判权限更安全,也省资源。

WebSocket 客户端

// 连接到 WebSocket 服务
const ws = new WebSocket('wss://example.com/ws');

// 连接打开
ws.addEventListener('open', () => {
  console.log('Connected');
  
  // 发送消息
  ws.send(JSON.stringify({
    user: 'Alice',
    content: 'Hello everyone!'
  }));
});

// 接收消息
ws.addEventListener('message', (event) => {
  const data = JSON.parse(event.data);
  displayMessage(data.user, data.content);
});

// 连接关闭
ws.addEventListener('close', () => {
  console.log('Disconnected');
  // 自动重连(简单实现)
  setTimeout(() => {
    window.location.reload();
  }, 3000);
});

// 错误处理
ws.addEventListener('error', (error) => {
  console.error('WebSocket error:', error);
});

客户端代码有三个容易被忽略的点:一是要处理 close 并实现重连,移动端网络切换经常把连接掐断;二是消息要按 type 区分业务类型,别把控制消息和业务消息混在一起;三是 readyState 不是 OPENsend 会直接抛错,发送前要先判断。另外,页面切到后台时浏览器会暂停定时器,重连间隔和心跳都要考虑到这种“挂起”状态,避免回到前台后一瞬间触发大量重连。

SSE(Server-Sent Events)方案

如果只需要服务端推送(不需要客户端发送),SSE 是更简单的选择:

// 服务端(Node.js)
app.get('/events', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
  });

  // 每秒推送当前时间
  setInterval(() => {
    res.write(`data: ${JSON.stringify({ time: new Date().toISOString() })}\n\n`);
  }, 1000);
});
// 客户端
const eventSource = new EventSource('/events');
eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Server time:', data.time);
};

SSE 基于普通 HTTP,不需要额外协议和端口,配合 Nginx 反代也简单;它还自带断线重连,网络抖动恢复后能自动续上。缺点是单向——客户端想发数据还得走普通的 POST 请求。对“订单状态变化推给后台”这类场景,SSE 是最舒服的选择,前端一个 EventSource 就够用。写 SSE 时注意格式:data: 行之间要留空行作为消息分隔,需要多段数据可以用多个 data: 行拼成一条消息;响应头必须先设置 Content-Type: text/event-stream,否则浏览器不会按流式解析,直接当成普通请求处理。

部署注意事项

  1. Nginx 代理 WebSocket — 需要配置 Upgrade header,默认配置下空闲 60 秒就会断开,要把 proxy_read_timeout 调大并开启 HTTP/1.1;
  2. 自动重连机制 — WebSocket 连接可能因网络切换、代理超时意外断开,客户端要设计带退避的重连逻辑;
  3. 连接管理 — 记录在线用户数,限制单机连接数,防止连接泄漏拖垮进程;
  4. 扩展性 — 多服务器环境需使用 Redis Pub/Sub 转发消息,让不同节点上的客户端能互相通信。

实时功能的难点从来不在“能不能通”,而在“断了能不能自愈、人多了能不能扛住”。先把选型和基础示例跑通,再逐步加上心跳、鉴权和扩展,是更稳妥的推进方式。

参考:MDN WebSocket https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API、MDN Server-sent events https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events

16IDC 观察

如果你只需要服务端推送通知(如新评论、订单状态更新),SSE 比 WebSocket 更简单、兼容性更好,前端一个 EventSource 就搞定;如果需要双向交互(如在线客服、协作编辑),WebSocket 是唯一选择。多数网站其实并不需要全站实时——把最关键的几个场景做实时,其余保持常规请求,性价比最高。