HTTP 是请求-响应模式——客户端不开口,服务器不吭声。但如果你在做在线协作、实时行情推送或即时通讯,这种模式就完全不够用了。这时 WebSocket 登场——建立一条持久的双向通道,服务器随时可以主动推送数据。
握手:从 HTTP 升级到 WebSocket
WebSocket 连接建立依赖 HTTP 协议完成协议升级:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 客户端请求: GET /chat HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13
服务器响应: HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
|
状态码 101 表示协议切换成功。此后 TCP 连接上走的就是 WebSocket 数据帧了。
Node.js 服务端实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); const clients = new Map();
wss.on('connection', (ws, req) => { const clientId = req.headers['sec-websocket-key']; clients.set(clientId, ws); ws.on('message', (data) => { const msg = JSON.parse(data); clients.forEach((c, id) => { if (id !== clientId && c.readyState === WebSocket.OPEN) { c.send(JSON.stringify(msg)); } }); }); ws.on('close', () => clients.delete(clientId)); });
|
心跳保活
WebSocket 连接可能因网络波动而假死——双方都以为连接还在,但数据已经传不过去了。心跳机制是解决方案:
1 2 3 4 5 6 7 8 9 10 11 12
| const interval = setInterval(() => { wss.clients.forEach((ws) => { if (ws.isAlive === false) return ws.terminate(); ws.isAlive = false; ws.ping(); }); }, 30000);
wss.on('connection', (ws) => { ws.isAlive = true; ws.on('pong', () => { ws.isAlive = true; }); });
|
集群下的跨节点消息广播
单机时 clients Map 能管住所有连接。但如果你有 3 台服务器,用户 A 连节点 1,用户 B 连节点 2,它们之间怎么通信?用 Redis Pub/Sub 作为消息总线:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const Redis = require('ioredis'); const pub = new Redis(); const sub = new Redis();
sub.subscribe('ws:broadcast'); sub.on('message', (channel, msg) => { const { senderId, data } = JSON.parse(msg); clients.forEach((ws, id) => { if (id !== senderId) ws.send(data); }); });
function broadcast(data, senderId) { pub.publish('ws:broadcast', JSON.stringify({ senderId, data })); }
|
断线重连与指数退避
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class ReconnectingWS { constructor(url) { this.url = url; this.retries = 0; this.connect(); } connect() { const ws = new WebSocket(this.url); ws.onopen = () => { this.retries = 0; }; ws.onclose = () => { const delay = Math.min(1000 * Math.pow(2, this.retries++), 30000); setTimeout(() => this.connect(), delay); }; } }
|
指数退避让重连间隔从 1 秒开始、每次翻倍、最大 30 秒——既不会过于激进地冲击服务器,也不会等太久才恢复。
总结
WebSocket 本身不复杂,复杂的是围绕它的工程化。心跳保活、集群广播、断线重连、消息可靠性——把这些基础设施搭好后,实时通信就像 HTTP 一样顺手。