WebSocket 服务端
问题
如何在服务端实现 WebSocket?连接管理、心跳检测、房间机制和集群广播是怎么做的?
面试速答版
Node.js 服务端 WebSocket 怎么实现? 两种主流选择:
ws库:底层、性能高、API 干净,适合自己实现协议(如游戏、IM 长连)。Socket.IO:生产首选,自带自动重连、心跳、房间、命名空间、HTTP 长轮询降级,浏览器兼容性强。- NestJS 用
@WebSocketGateway装饰器封装,开发体验好。
连接管理、心跳、房间怎么做?
- 连接管理:用
Map<userId, ws>维护在线用户表,断开时删除;超大规模迁到 Redis。 - 心跳检测:服务端定时(30s)发
ping,客户端回pong;超时未收到就terminate()关闭——防 NAT 超时和半开连接。 - 房间机制:Socket.IO 的
socket.join('room-1')+io.to('room-1').emit(),用于群聊、协同编辑场景。 - 鉴权:握手阶段在
connection事件里校验 Token(req.headers.authorization或 query 里的token),不通过直接ws.close()。
多实例集群广播怎么解决? 核心是「跨实例消息总线」:
- 问题:用户 A 连到实例 1,用户 B 连到实例 2,实例 1 直接
emit触达不到 B。 - 方案:用 Redis Pub/Sub 做跨实例广播——任一实例发消息先
publish到 Redis,所有实例subscribe后再推给本地连接。 - Socket.IO 有现成的
@socket.io/redis-adapter,一行配置搞定;自研ws服务要手写。 - 粘性会话:负载均衡需要
ip_hash或 sticky session,避免握手和升级请求落到不同实例。
答案
WebSocket 服务端架构
基础实现(ws 库)
ws-server.ts
import { WebSocket, WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
// 连接管理
const clients = new Map<string, WebSocket>();
wss.on('connection', (ws, req) => {
const userId = parseUserId(req);
clients.set(userId, ws);
// 心跳检测
let isAlive = true;
ws.on('pong', () => { isAlive = true; });
const heartbeat = setInterval(() => {
if (!isAlive) {
clients.delete(userId);
return ws.terminate();
}
isAlive = false;
ws.ping();
}, 30000);
// 消息处理
ws.on('message', (data) => {
const message = JSON.parse(data.toString());
handleMessage(userId, message);
});
ws.on('close', () => {
clearInterval(heartbeat);
clients.delete(userId);
});
});
// 发送消息给指定用户
function sendToUser(userId: string, data: unknown) {
const ws = clients.get(userId);
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(data));
}
}
// 广播给所有在线用户
function broadcast(data: unknown) {
const message = JSON.stringify(data);
clients.forEach((ws) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
}
});
}
集群广播(Redis Pub/Sub)
cluster-broadcast.ts
import Redis from 'ioredis';
const pub = new Redis();
const sub = new Redis();
// 当本机收到消息需要广播时
function clusterBroadcast(channel: string, data: unknown) {
pub.publish(channel, JSON.stringify(data));
}
// 每个实例订阅频道
sub.subscribe('chat:messages');
sub.on('message', (channel, message) => {
// 收到消息后广播给本机连接的客户端
const data = JSON.parse(message);
broadcast(data);
});
Socket.IO(生产推荐)
socketio-server.ts
import { Server } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
const io = new Server(server, {
cors: { origin: '*' },
});
// Redis 适配器(支持集群)
io.adapter(createAdapter(pubClient, subClient));
io.on('connection', (socket) => {
// 加入房间
socket.join(`user:${socket.data.userId}`);
// 房间消息
socket.on('join-room', (roomId) => {
socket.join(roomId);
});
socket.on('message', (data) => {
// 向房间内广播
io.to(data.roomId).emit('message', data);
});
});
// 向特定用户发消息
io.to(`user:${userId}`).emit('notification', data);
常见面试问题
Q1: 如何处理 WebSocket 断线重连?
答案:
服务端配合:
- 为每个连接分配唯一 ID
- 存储未送达消息(离线队列)
- 重连后根据 ID 恢复会话,推送离线消息
Q2: 多台服务器部署时,WebSocket 消息如何同步?
答案:
使用 Redis Pub/Sub 做跨实例广播。Socket.IO 有现成的 @socket.io/redis-adapter。
Q3: WebSocket 和 HTTP 长轮询怎么选?
答案:
- WebSocket:双向实时通信、高频消息(聊天、协同编辑)
- 长轮询:兼容性好、低频消息、简单场景
- SSE:服务端单向推送(通知、AI 流式输出)
Q4: 心跳检测的作用?
答案:
- 检测连接是否存活(客户端断网不会触发 close 事件)
- 防止中间设备(NAT、代理)关闭空闲连接
- 通常 30s 一次 ping/pong
相关链接
- WebSocket 与 SSE - 前端 WebSocket
- 设计实时通讯系统 - IM 系统设计
- 设计直播弹幕系统 - 弹幕系统