为什么需要 WebSocket
传统 HTTP 是“请求-响应”模型:客户端问,服务器答,连接就结束。服务器无法主动给客户端推消息。在即时聊天、实时协作、股票行情、在线游戏这类场景,如果用 HTTP 轮询,要么延迟高、要么浪费带宽。
WebSocket 解决了这个问题:建立一次 TCP 连接后,服务器和客户端可以随时双向收发数据,且帧头开销极小(2-10 字节),非常适合高频小消息。
WebSocket 协议速览
WebSocket 连接建立的过程是“HTTP 升级”:
Upgrade: websocket 头的 HTTP 请求。101 Switching Protocols。GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13数据以“帧”传输,有文本帧、二进制帧、ping/pong 心跳帧、关闭帧。
服务端:用 ws 库搭建
Node.js 生态里,ws 是最轻量高性能的 WebSocket 库,Socket.IO 则在它之上加了房间、自动重连、降级等功能。这里先用原生 ws 理解底层。
const { WebSocketServer } = require("ws");
const wss = new WebSocketServer({ port: 8080 });
// 房间模型:用 Map 维护房间名 -> 客户端集合
const rooms = new Map();
function joinRoom(ws, room) {
if (!rooms.has(room)) rooms.set(room, new Set());
rooms.get(room).add(ws);
ws.room = room;
}
function broadcast(room, message, sender) {
const members = rooms.get(room);
if (!members) return;
const payload = JSON.stringify(message);
for (const client of members) {
if (client !== sender && client.readyState === ws.OPEN) {
client.send(payload);
}
}
}
wss.on("connection", (ws, req) => {
ws.isAlive = true;
ws.on("pong", () => { ws.isAlive = true; });
ws.on("message", (raw) => {
const msg = JSON.parse(raw.toString());
if (msg.type === "join") {
joinRoom(ws, msg.room);
broadcast(msg.room, { type: "system", text: "有人加入了房间" }, ws);
} else if (msg.type === "chat") {
broadcast(ws.room, { type: "chat", user: msg.user, text: msg.text }, ws);
}
});
ws.on("close", () => {
if (ws.room && rooms.has(ws.room)) {
rooms.get(ws.room).delete(ws);
}
});
});这段代码实现了:加入房间、群内广播、断开清理。核心是用内存 Map 维护“房间->连接集合”的映射。
心跳检测:清理死连接
TCP 连接可能因为网络异常而“半开”——客户端已断,服务器却不知道,连接对象一直占内存。必须靠心跳定期探测。
// 每 30 秒检查一次所有连接
const interval = setInterval(() => {
wss.clients.forEach((ws) => {
if (!ws.isAlive) return ws.terminate();
ws.isAlive = false;
ws.ping(); // 客户端收到 ping 会自动回 pong
});
}, 30000);
wss.on("close", () => clearInterval(interval));ws 库在收到对端 pong 时会触发 pong 事件,我们据此把 isAlive 重置为 true。一轮检查后仍为 false 的连接,说明对端无响应,直接 terminate。
客户端实现
浏览器原生支持 WebSocket:
const ws = new WebSocket("ws://localhost:8080/chat");
const messages = document.getElementById("messages");
ws.onopen = () => {
ws.send(JSON.stringify({ type: "join", room: "room1", user: "Tom" }));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
const el = document.createElement("div");
el.textContent = msg.user ? `${msg.user}: ${msg.text}` : msg.text;
messages.appendChild(el);
};
function send() {
const input = document.getElementById("input");
ws.send(JSON.stringify({ type: "chat", user: "Tom", text: input.value }));
input.value = "";
}自动重连
网络抖动会导致断连,客户端必须有重连机制,并配合指数退避避免雪崩。
class ReconnectingWebSocket {
constructor(url) {
this.url = url;
this.retry = 0;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.retry = 0;
console.log("已连接");
};
this.ws.onclose = () => {
const delay = Math.min(1000 * 2 ** this.retry, 30000);
this.retry++;
console.log(`${delay}ms 后重连...`);
setTimeout(() => this.connect(), delay);
};
this.ws.onmessage = (e) => this.onmessage && this.onmessage(e);
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data);
}
}
}用 Socket.IO 简化开发
如果不想手写房间、广播、重连,Socket.IO 把这些都封装好了:
// 服务端
const io = require("socket.io")(3000, { cors: { origin: "*" } });
io.on("connection", (socket) => {
socket.on("join", (room) => {
socket.join(room);
socket.to(room).emit("message", "有人加入了房间");
});
socket.on("chat", ({ room, user, text }) => {
io.to(room).emit("message", { user, text });
});
});
// 客户端
const socket = io("http://localhost:3000");
socket.emit("join", "room1");
socket.on("message", (msg) => console.log(msg));socket.to(room).emit 广播给房间内除自己外的所有人,io.to(room).emit 广播给房间所有人。Socket.IO 还自带断线重连、消息缓冲、ACK 机制。
生产化要点
Upgrade 和 Connection 头透传。# Nginx WebSocket 代理配置
location /chat {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}proxy_read_timeout 必须调大,否则 Nginx 默认 60 秒无数据就会断开长连接。
跨实例广播:Redis 适配器
const { createAdapter } = require("@socket.io/redis-adapter");
const { createClient } = require("redis");
const pubClient = createClient({ url: "redis://localhost:6379" });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));接入 Redis 适配器后,io.to(room).emit 会通过 Redis Pub/Sub 同步到所有实例,任一实例上的客户端都能收到,从而实现水平扩展。
小结
WebSocket 用一次握手换取全双工长连接,是实时通信的基石。构建聊天应用的核心要素:房间模型(Map 维护连接集合)、心跳清理死连接、广播与私聊、客户端自动重连。小项目用 ws 足够,复杂业务用 Socket.IO 省心,多实例扩展时接 Redis 适配器。生产环境别忘了鉴权、背压和消息可靠性。理解了这些,你就掌握了实时应用的基础设施,无论是聊天、协作还是推送,都游刃有余。
💬 评论区 (0)
暂无评论,快来抢沙发吧!