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 一样顺手。