跳到主要内容

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 断线重连?

答案

服务端配合:

  1. 为每个连接分配唯一 ID
  2. 存储未送达消息(离线队列)
  3. 重连后根据 ID 恢复会话,推送离线消息

Q2: 多台服务器部署时,WebSocket 消息如何同步?

答案

使用 Redis Pub/Sub 做跨实例广播。Socket.IO 有现成的 @socket.io/redis-adapter

Q3: WebSocket 和 HTTP 长轮询怎么选?

答案

  • WebSocket:双向实时通信、高频消息(聊天、协同编辑)
  • 长轮询:兼容性好、低频消息、简单场景
  • SSE:服务端单向推送(通知、AI 流式输出)

Q4: 心跳检测的作用?

答案

  1. 检测连接是否存活(客户端断网不会触发 close 事件)
  2. 防止中间设备(NAT、代理)关闭空闲连接
  3. 通常 30s 一次 ping/pong

相关链接