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 不是 OPEN 时 send 会直接抛错,发送前要先判断。另外,页面切到后台时浏览器会暂停定时器,重连间隔和心跳都要考虑到这种“挂起”状态,避免回到前台后一瞬间触发大量重连。
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,否则浏览器不会按流式解析,直接当成普通请求处理。
部署注意事项
- Nginx 代理 WebSocket — 需要配置 Upgrade header,默认配置下空闲 60 秒就会断开,要把
proxy_read_timeout调大并开启 HTTP/1.1; - 自动重连机制 — WebSocket 连接可能因网络切换、代理超时意外断开,客户端要设计带退避的重连逻辑;
- 连接管理 — 记录在线用户数,限制单机连接数,防止连接泄漏拖垮进程;
- 扩展性 — 多服务器环境需使用 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 是唯一选择。多数网站其实并不需要全站实时——把最关键的几个场景做实时,其余保持常规请求,性价比最高。