WebSocket实战:构建实时消息推送系统

前言

HTTP请求的本质是"问一次答一次",但很多场景需要服务端主动推送数据------比如消息通知、实时数据大屏、在线协作编辑。WebSocket就是解决这类问题的最佳方案。

这篇文章带你从原理到落地,快速掌握WebSocket实战技巧。


一、WebSocket vs HTTP

复制代码
复制代码
HTTP(短连接):
  客户端发请求 → 服务端响应 → 连接断开
  缺点:无法服务端主动推送,轮询浪费资源

WebSocket(长连接):
  客户端发起握手 → 连接建立 → 双向实时通信
  优点:低延迟、低开销、全双工通信

适用场景:
  ✅ 聊天系统
  ✅ 实时通知
  ✅ 股票/行情数据
  ✅ 在线协作(文档、白板)
  ✅ 游戏状态同步

二、Node.js服务端实现

2.1 基础WebSocket服务

复制代码

javascript

复制代码
// server.js
const WebSocket = require('ws');
const http = require('http');

const server = http.createServer();
const wss = new WebSocket.Server({ server });

// 在线用户管理
const clients = new Map();

wss.on('connection', (ws, req) => {
  const userId = req.url.split('?userId=')[1];
  clients.set(userId, ws);
  console.log(`用户 ${userId} 已连接,当前在线:${clients.size}`);

  // 接收消息
  ws.on('message', (data) => {
    const message = JSON.parse(data);
    handleMessage(userId, message);
  });

  // 断开连接
  ws.on('close', () => {
    clients.delete(userId);
    console.log(`用户 ${userId} 已断开`);
  });

  // 错误处理
  ws.on('error', (err) => {
    console.error(`用户 ${userId} 连接异常:`, err.message);
    clients.delete(userId);
  });

  // 心跳检测
  ws.isAlive = true;
  ws.on('pong', () => { ws.isAlive = true; });
});

// 消息处理
function handleMessage(fromUser, message) {
  switch (message.type) {
    case 'private_msg':
      sendToUser(message.toUser, {
        type: 'private_msg',
        from: fromUser,
        content: message.content,
        time: Date.now()
      });
      break;

    case 'broadcast':
      broadcast({
        type: 'broadcast',
        from: fromUser,
        content: message.content
      }, fromUser);
      break;
  }
}

// 发送给指定用户
function sendToUser(userId, data) {
  const ws = clients.get(userId);
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(data));
  }
}

// 广播给所有在线用户
function broadcast(data, excludeUser = null) {
  clients.forEach((ws, userId) => {
    if (userId !== excludeUser && ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify(data));
    }
  });
}

// 心跳检测(每30秒)
const heartbeat = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (!ws.isAlive) return ws.terminate();
    ws.isAlive = false;
    ws.ping();
  });
}, 30000);

server.listen(8080, () => {
  console.log('WebSocket服务启动:ws://localhost:8080');
});

2.2 房间分组(聊天室场景)

复制代码

javascript

复制代码
// 房间管理
const rooms = new Map();

function joinRoom(userId, roomId) {
  if (!rooms.has(roomId)) {
    rooms.set(roomId, new Set());
  }
  rooms.get(roomId).add(userId);
}

function leaveRoom(userId, roomId) {
  const room = rooms.get(roomId);
  if (room) {
    room.delete(userId);
    if (room.size === 0) rooms.delete(roomId);
  }
}

function broadcastToRoom(roomId, data, excludeUser = null) {
  const room = rooms.get(roomId);
  if (!room) return;

  room.forEach((userId) => {
    if (userId !== excludeUser) {
      sendToUser(userId, data);
    }
  });
}

// 消息处理中加入房间逻辑
function handleMessage(fromUser, message) {
  switch (message.type) {
    case 'join_room':
      joinRoom(fromUser, message.roomId);
      broadcastToRoom(message.roomId, {
        type: 'system',
        content: `用户 ${fromUser} 加入了房间`
      });
      break;

    case 'room_msg':
      broadcastToRoom(message.roomId, {
        type: 'room_msg',
        from: fromUser,
        content: message.content,
        time: Date.now()
      }, fromUser);
      break;

    case 'leave_room':
      leaveRoom(fromUser, message.roomId);
      break;
  }
}

三、前端客户端实现

3.1 封装WebSocket客户端

复制代码

javascript

复制代码
// websocket-client.js
class WSClient {
  constructor(url) {
    this.url = url;
    this.ws = null;
    this.listeners = new Map();
    this.reconnectDelay = 3000;
    this.maxReconnect = 5;
    this.reconnectCount = 0;
  }

  connect(userId) {
    this.ws = new WebSocket(`${this.url}?userId=${userId}`);

    this.ws.onopen = () => {
      console.log('连接成功');
      this.reconnectCount = 0;
      this.emit('connected');
    };

    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      this.emit(message.type, message);
    };

    this.ws.onclose = () => {
      console.log('连接断开,尝试重连...');
      this.emit('disconnected');
      this.reconnect(userId);
    };

    this.ws.onerror = (err) => {
      console.error('连接异常:', err);
    };
  }

  // 自动重连
  reconnect(userId) {
    if (this.reconnectCount >= this.maxReconnect) {
      console.error('重连次数超限,请刷新页面');
      return;
    }
    this.reconnectCount++;
    setTimeout(() => this.connect(userId), this.reconnectDelay);
  }

  // 发送消息
  send(type, data) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({ type, ...data }));
    }
  }

  // 事件监听
  on(event, callback) {
    this.listeners.set(event, callback);
  }

  emit(event, data) {
    const callback = this.listeners.get(event);
    if (callback) callback(data);
  }

  disconnect() {
    this.ws?.close();
  }
}

// 使用示例
const ws = new WSClient('ws://localhost:8080');
ws.connect('user_001');

ws.on('connected', () => {
  // 加入聊天室
  ws.send('join_room', { roomId: 'room_001' });
});

ws.on('room_msg', (msg) => {
  console.log(`${msg.from}: ${msg.content}`);
});

ws.on('private_msg', (msg) => {
  console.log(`私信来自 ${msg.from}: ${msg.content}`);
});

// 发送消息
ws.send('room_msg', {
  roomId: 'room_001',
  content: '大家好!'
});

四、生产环境注意事项

4.1 Nginx代理WebSocket

复制代码

nginx

复制代码
// websocket-client.js
class WSClient {
  constructor(url) {
    this.url = url;
    this.ws = null;
    this.listeners = new Map();
    this.reconnectDelay = 3000;
    this.maxReconnect = 5;
    this.reconnectCount = 0;
  }

  connect(userId) {
    this.ws = new WebSocket(`${this.url}?userId=${userId}`);

    this.ws.onopen = () => {
      console.log('连接成功');
      this.reconnectCount = 0;
      this.emit('connected');
    };

    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      this.emit(message.type, message);
    };

    this.ws.onclose = () => {
      console.log('连接断开,尝试重连...');
      this.emit('disconnected');
      this.reconnect(userId);
    };

    this.ws.onerror = (err) => {
      console.error('连接异常:', err);
    };
  }

  // 自动重连
  reconnect(userId) {
    if (this.reconnectCount >= this.maxReconnect) {
      console.error('重连次数超限,请刷新页面');
      return;
    }
    this.reconnectCount++;
    setTimeout(() => this.connect(userId), this.reconnectDelay);
  }

  // 发送消息
  send(type, data) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({ type, ...data }));
    }
  }

  // 事件监听
  on(event, callback) {
    this.listeners.set(event, callback);
  }

  emit(event, data) {
    const callback = this.listeners.get(event);
    if (callback) callback(data);
  }

  disconnect() {
    this.ws?.close();
  }
}

// 使用示例
const ws = new WSClient('ws://localhost:8080');
ws.connect('user_001');

ws.on('connected', () => {
  // 加入聊天室
  ws.send('join_room', { roomId: 'room_001' });
});

ws.on('room_msg', (msg) => {
  console.log(`${msg.from}: ${msg.content}`);
});

ws.on('private_msg', (msg) => {
  console.log(`私信来自 ${msg.from}: ${msg.content}`);
});

// 发送消息
ws.send('room_msg', {
  roomId: 'room_001',
  content: '大家好!'
});

4.2 水平扩展(多节点问题)

复制代码
复制代码
问题:多台服务器时,用户A在Node1,用户B在Node2
      A给B发消息,Node1找不到B的连接!

解决方案:使用Redis Pub/Sub同步消息

Node1 收到A的消息
  → 发布到Redis频道
  → Node2 订阅到消息
  → Node2 找到B的连接并推送
复制代码

javascript

复制代码
const redis = require('redis');

const pub = redis.createClient();
const sub = redis.createClient();

// 订阅跨节点消息
sub.subscribe('ws_messages');
sub.on('message', (channel, data) => {
  const { toUser, message } = JSON.parse(data);
  // 本节点有该用户则推送
  const ws = clients.get(toUser);
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(message));
  }
});

// 发送时发布到Redis
function sendToUser(userId, data) {
  const ws = clients.get(userId);
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(data));
  } else {
    // 本节点没有该用户,广播给其他节点
    pub.publish('ws_messages', JSON.stringify({
      toUser: userId,
      message: data
    }));
  }
}

五、团队与工具

我们团队在做实时协作功能时,产品、设计、前后端工程师每天都需要开跨时区的需求讨论会。会议中偶尔有英文表述不清楚的地方,我们会借助**同言翻译(Transync AI)**的实时语音翻译功能及时消除歧义,让沟通效率大幅提升。


六、性能优化要点

复制代码
复制代码
✅ 消息体积:尽量精简JSON结构,避免冗余字段
✅ 心跳机制:防止连接假死,推荐30s间隔
✅ 断线重连:指数退避策略,避免瞬间涌入
✅ 消息队列:高并发时先入队再推送
✅ 连接数限制:单节点建议不超过5万并发连接
✅ 消息压缩:大消息启用permessage-deflate压缩

总结

WebSocket是实时通信的首选方案,掌握核心要点:

  • 服务端:连接管理 + 消息路由 + 心跳检测
  • 客户端:自动重连 + 事件监听封装
  • 生产环境:Nginx代理 + Redis集群同步
  • 性能优化:控制消息体积 + 限制连接数

从一个简单的聊天室开始,逐步演进为支撑百万并发的实时系统,WebSocket值得每位后端工程师深入掌握。


相关推荐
认真的小羽❅2 小时前
SSE服务器推送事件原理深度解析与实战应用
java·网络
果果燕3 小时前
网络编程第一天学习笔记(重点:UDP 协议)
网络
非凡ghost3 小时前
proDAD ReSpeedr:专业视频变速编辑的利器
java·网络·windows·python·音视频·软件需求
路溪非溪3 小时前
Linux下iw工具的使用总结
linux·网络·arm开发·驱动开发
大榕树信息科技3 小时前
高效动环监控赋能机房环境智能管理与数据可视化
大数据·网络·数据库·人工智能·信息可视化
GlobalInfo3 小时前
全球户外WiFi智能插头市场份额、规模、技术研究报告2026
大数据·网络·人工智能
姜太小白3 小时前
【SQLServer】SQL Server 2022 连接证书错误解决
网络·数据库·sqlserver
゛anqiaoyun3 小时前
WebSocket告警无声音
网络·websocket·网络协议
Ka1Yan4 小时前
RPC核心原理:组件与调用流程
网络·网络协议·rpc