Vue3 + Node.js 实现客服实时聊天系统(WebSocket + Socket.IO 详解)

发布于:2025-05-09 ⋅ 阅读:(24) ⋅ 点赞:(0)

Node.js 实现客服实时聊天系统(WebSocket + Socket.IO 详解)

一、为什么选择 WebSocket?

想象一下淘宝客服的聊天窗口:你发消息,客服立刻就能看到并回复。这种即时通讯效果是如何实现的呢?我们使用 Vue3 作为前端框架,Node.js 作为后端,通过 WebSocket+ Socket.IO 协议实现实时通信。

1.1 实时通信的痛点

传统 HTTP 协议就像打电话:客户端发起请求 → 服务器响应 → 挂断连接。要实现实时聊天需要频繁"拨号",这就是长轮询(不断发送请求问:“有新消息吗?”),既浪费资源又延迟高。

1.2 传统 HTTP 的局限性

传统 HTTP 协议 就像写信

  • 必须你先发请求,服务器才能回复

  • 每次都要重新建立连接

  • 服务器无法主动"推"消息给你

1.3 WebSocket 的优势

WebSocket 就像 打电话

  • 一次连接,持续通话
  • 双向实时通信
  • 低延迟,高效率
1.3 Socket.IO 的价值

原生 WebSocket 存在兼容性问题,Socket.IO 提供了:

  • 自动降级(不支持 WS 时回退到轮询)
  • 断线自动重连
  • 房间/命名空间管理
  • 简单的 API 设计

以下是传统HTTP、WebSocket和Socket.IO的对比表格,清晰展示它们的区别和特点:

特性 传统HTTP WebSocket Socket.IO
通信模式 单向通信(客户端发起) 全双工通信 全双工通信
连接方式 短连接(每次请求后断开) 长连接(一次连接持续通信) 长连接(自动管理连接)
实时性 低(依赖轮询) 高(实时推送) 高(实时推送)
资源消耗 高(重复建立连接和头部开销) 低(无重复头部) 低(优化传输)
兼容性 所有浏览器支持 现代浏览器支持 自动降级(不支持WebSocket时回退到轮询)
额外功能 基础通信 断线重连房间管理命名空间二进制传输ACK确认机制
比喻 写信(一来一回,每次重新寄信) 打电话(接通后持续通话) 智能对讲机(自动重连、多频道支持)
适用场景 静态资源获取、表单提交 实时聊天、股票行情 复杂实时应用(游戏、协同编辑、在线客服)
关键点总结:
  1. 传统HTTP:简单但效率低,无法主动推送。
  2. WebSocket:真正双向实时通信,但需处理兼容性和连接管理。
  3. Socket.IO:在WebSocket基础上封装,提供更健壮的解决方案,适合生产环境。

通过表格可以直观看出:Socket.IO是WebSocket的超集,解决了原生API的痛点,同时保留了所有优势。

二、深入解析实时聊天服务端实现(基于Socket.IO)

环境搭建

const http = require('http');
// 初始化Express应用
const app = express();
const server = http.createServer(app);
// 创建WebScoket服务器
const io = socketIo(server, {
  cors: {
    origin: "http://192.168.1.3:8080", // 你的前端地址
    origin: '*',
    methods: ['GET', 'POST']
  }
});
// ...
server.listen(3000, async () => {
    console.log(`Server is running on port 3000`);
});

接下来我会对我后端代码进行详细解析:

一、核心架构解析

1.1 用户连接管理
const userSocketMap = new Map(); // 用户ID到socket.id的映射
const userHeartbeats = new Map(); // 用户心跳检测

设计要点:

  • userSocketMap 维护用户ID与Socket实例的映射关系,实现快速查找
  • userHeartbeats 用于检测用户是否在线(心跳机制)
  • 双Map结构确保用户状态管理的可靠性
1.2 连接事件处理
io.on("connection", async (socket) => {
  // 所有连接逻辑在这里处理
});

生命周期:

  1. 客户端通过WebSocket连接服务端
  2. 服务端创建socket实例并触发connection事件
  3. 在回调中设置各种事件监听器

二、关键功能模块详解

2.1 用户登录认证
// 当客户端发送 'login' 事件时,触发这个回调函数
socket.on('login', ({ userId, csId }) => {
  // 参数验证:确保传入的参数是字符串类型
  userId = String(userId); // 将 userId 转换为字符串,统一类型
  csId = String(csId); // 将 csId 转换为字符串,表示要聊天的客户id

  // 存储关联关系:将用户信息与当前 socket 连接关联起来
  socket.userId = userId; // 将 userId 存储到当前 socket 对象中
  socket.csId = csId; // 将 csId 存储到当前 socket 对象中
  userSocketMap.set(userId, socket.id); // 在 userSocketMap 中存储 userId 和 socket.id 的映射关系

  // 加入房间:根据 csId 创建一个房间,用户加入该房间
  const room = `room-${csId}`; // 使用 csId 构造房间名称
  socket.join(room); // 让当前用户加入这个房间

  // 广播在线状态:通知所有客户端当前用户的在线状态
  io.emit('user_online', userId); // 发送 'user_online' 事件,通知用户上线
  io.emit('Online_user', Array.from(userSocketMap.entries())); // 发送 'Online_user' 事件,包含所有在线用户的信息
});

代码功能总结:

  1. 参数验证:确保传入的 userIdcsId 是字符串类型。
  2. 存储关联关系:将用户信息(userIdcsId)存储到当前 socket 对象中,并在 userSocketMap 中存储用户与 socket 的映射关系。
  3. 加入房间:根据 csId 创建一个房间,并让用户加入该房间。
  4. 广播在线状态:通过 io.emit 广播用户的在线状态,通知所有客户端当前用户的上线情况,并发送所有在线用户的信息。

关键点:

  • 强制类型转换确保数据一致性
  • 使用join()方法实现房间功能
  • 实时广播用户在线状态
2.2 房间成员管理
// 当客户端发送 'all_member' 事件时,触发这个回调函数
socket.on('all_member', async () => {
  // 根据当前用户的 csId 构造房间名称
  const room = `room-${socket.csId}`;

  // 获取房间内所有用户的 socket 实例
  const sockets = await io.in(room).fetchSockets();  // 使用 io.in(room).fetchSockets() 获取房间内的所有 socket 实例

  // 提取房间内所有用户的 userId
  const users = sockets.map(s => s.userId);  // 从每个 socket 实例中提取 userId,形成一个用户 ID 数组

  // 数据库查询优化:查询房间内用户的详细信息及未读消息数量
  const [results] = await pool.query(`
    SELECT u.id, u.role, u.username,  // 查询用户的基本信息:用户 ID、角色、用户名
           COUNT(m.id) AS message_count  // 查询未读消息的数量
    FROM users u
    LEFT JOIN messages m ON u.id = m.sender_id  // 关联消息表,找到发送给当前用户的消息
      AND m.receiver_id = ?  // 限定消息的接收者是当前用户
      AND m.read_at IS NULL  // 限定消息未被阅读
    WHERE u.id IN (?)  // 限定用户 ID 在房间内用户列表中
    GROUP BY u.id  // 按用户 ID 分组,确保每个用户只返回一条记录
  `, [socket.userId, users]);  // 查询参数:当前用户的 ID 和房间内用户 ID 列表

  // 将查询结果发送回客户端
  socket.emit('myUsersList', results);  // 发送 'myUsersList' 事件,将查询结果传递给客户端
});

代码功能总结:

  1. 获取房间信息
    • 根据当前用户的 csId 构造房间名称。
    • 使用 io.in(room).fetchSockets() 获取房间内所有用户的 socket 实例。
    • 从每个 socket 实例中提取 userId,形成一个用户 ID 数组。
  2. 数据库查询
    • 查询房间内用户的详细信息,包括用户的基本信息(idroleusername)。
    • 查询每个用户发送给当前用户且未被阅读的消息数量(message_count)。
    • 使用 LEFT JOIN 关联 messages 表,筛选出未读消息。
    • 使用 GROUP BY 确保每个用户只返回一条记录。
  3. 发送结果
    • 将查询结果通过 socket.emit 发送给当前用户,事件名称为 myUsersList

优化技巧:

  • 使用fetchSockets()获取房间内所有socket实例
  • 单次SQL查询获取用户信息+未读消息数
  • LEFT JOIN确保离线用户也能被查询到
2.3 私聊消息处理
// 当客户端发送 'private_message' 事件时,触发这个回调函数
socket.on("private_message", async (data) => {
  // 获取接收者的 socket.id
  const receiverSocketId = userSocketMap.get(String(data.receiverId)); // 从 userSocketMap 中根据接收者的 userId 获取对应的 socket.id

  // 实时消息推送:将消息发送给接收者
  if (receiverSocketId) { // 如果接收者在线(存在对应的 socket.id)
    io.to(receiverSocketId).emit('new_private_message', { // 向接收者的 socket 发送 'new_private_message' 事件
      senderId: data.senderId, // 发送者的 ID
      content: data.content, // 消息内容
      timestamp: new Date() // 消息发送的时间戳
    });
  }

  // 消息持久化:将消息存储到数据库中
  await pool.execute( // 使用数据库连接池执行 SQL 插入语句
    'INSERT INTO messages VALUES (?, ?, ?, ?)', // 插入消息到 messages 表
    [data.senderId, data.receiverId, data.content, new Date()] // 插入的值:发送者 ID、接收者 ID、消息内容、消息发送时间
  );
});

代码功能总结:

  1. 获取接收者的 socket.id
    • userSocketMap 中根据接收者的 userId 获取对应的 socket.id
  2. 实时消息推送
    • 如果接收者在线(存在对应的 socket.id),则使用 io.to(receiverSocketId).emit 向接收者的 socket 发送 new_private_message 事件,包含发送者的 ID、消息内容和时间戳。
  3. 消息持久化
    • 将消息存储到数据库中,插入到 messages 表中,记录发送者 ID、接收者 ID、消息内容和发送时间。

消息流设计:

  1. 通过Map快速查找接收者socket
  2. 使用io.to(socketId).emit()实现点对点推送
  3. 异步存储到MySQL确保数据不丢失
2.4 断连处理机制
socket.on('disconnect', () => {
  userSocketMap.delete(socket.userId);
  io.emit('user_offline', socket.userId);
  io.emit('update_member_list');
});

容错设计:

  • 及时清理映射关系防止内存泄漏
  • 广播离线事件通知所有客户端
  • 触发成员列表更新

三、高级功能实现

3.1 心跳检测系统
// 心跳接收:客户端发送心跳信号时,更新用户的心跳时间
socket.on('heartbeat', () => {
  userHeartbeats.set(socket.userId, Date.now()); // 将当前用户的心跳时间更新为当前时间戳
});

// 定时检测:每隔一段时间检查用户是否离线
setInterval(() => {
  const now = Date.now(); // 获取当前时间戳
  for (const [userId, lastTime] of userHeartbeats) { // 遍历 userHeartbeats 中的每个用户及其最后心跳时间
    if (now - lastTime > 4000) { // 如果当前时间与最后心跳时间的差值超过 4000 毫秒(4 秒)
      // 清理离线用户
      userSocketMap.delete(userId); // 从 userSocketMap 中删除该用户,表示用户已离线
      io.emit('user_offline', userId); // 广播 'user_offline' 事件,通知所有客户端该用户已离线
    }
  }
}, 2000); // 每隔 2000 毫秒(2 秒)执行一次定时检测

代码功能总结

  1. 心跳接收
    • 当客户端发送 heartbeat 事件时,更新 userHeartbeats 中对应用户的心跳时间,记录为当前时间戳。
  2. 定时检测
    • 使用 setInterval 每隔 2 秒执行一次检测。
    • 遍历 userHeartbeats 中的每个用户及其最后心跳时间。
    • 如果当前时间与最后心跳时间的差值超过 4 秒,认为用户已离线。
    • userSocketMap 中删除该用户,并广播 user_offline 事件,通知所有客户端该用户已离线。

关键点解释

  • 心跳机制:客户端定期发送心跳信号(heartbeat 事件),服务器记录每次心跳的时间。如果超过一定时间(4 秒)没有收到心跳,认为用户离线。
  • 定时检测:每隔 2 秒检查一次,确保及时清理离线用户并通知其他客户端。

心跳参数建议:

  • 客户端每2秒发送一次心跳
  • 服务端4秒未收到视为离线
  • 检测间隔应小于超时时间
3.2 调试信息输出
setInterval(() => {
  console.log('\n当前连接状态:');
  console.log('用户映射:', Array.from(userSocketMap.entries()));
  
  io.sockets.forEach(socket => {
    console.log(`SocketID: ${socket.id}, User: ${socket.userId}`);
  });
}, 30000);

调试技巧:

  • 定期打印连接状态
  • 输出完整的用户映射关系
  • 生产环境可替换为日志系统

四、性能优化建议

  1. Redis集成

    // 使用Redis存储映射关系
    const redisClient = require('redis').createClient();
    await redisClient.set(`user:${userId}:socket`, socket.id);
    
  2. 消息分片

    // 大消息分片处理
    socket.on('message_chunk', (chunk) => {
      // 重组逻辑...
    });
    
  3. 负载均衡

    # Nginx配置
    location /socket.io/ {
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
      proxy_pass http://socket_nodes;
    }
    

五、常见问题解决方案

问题1:Map内存泄漏

  • 解决方案:双重清理(disconnect + 心跳检测)

问题2:消息顺序错乱

  • 解决方案:客户端添加消息序列号

问题3:跨节点通信

  • 解决方案:使用Redis适配器
    npm install @socket.io/redis-adapter
    
    const { createAdapter } = require("@socket.io/redis-adapter");
    io.adapter(createAdapter(redisClient, redisClient.duplicate()));
    

通过以上实现,您的聊天系统将具备:

  • 完善的用户状态管理
  • 可靠的私聊功能
  • 高效的心跳机制
  • 良好的可扩展性

建议在生产环境中添加:

  1. JWT认证
  2. 消息加密
  3. 限流防护
  4. 监控告警系统

网站公告

今日签到

点亮在社区的每一天
去签到