Vue3 + Node.js 实现客服实时聊天系统(WebSocket + Socket.IO 详解)
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确认机制等 |
比喻 | 写信(一来一回,每次重新寄信) | 打电话(接通后持续通话) | 智能对讲机(自动重连、多频道支持) |
适用场景 | 静态资源获取、表单提交 | 实时聊天、股票行情 | 复杂实时应用(游戏、协同编辑、在线客服) |
关键点总结:
- 传统HTTP:简单但效率低,无法主动推送。
- WebSocket:真正双向实时通信,但需处理兼容性和连接管理。
- 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) => {// 所有连接逻辑在这里处理
});
生命周期:
- 客户端通过WebSocket连接服务端
- 服务端创建socket实例并触发connection事件
- 在回调中设置各种事件监听器
二、关键功能模块详解
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' 事件,包含所有在线用户的信息
});
代码功能总结:
- 参数验证:确保传入的
userId
和csId
是字符串类型。 - 存储关联关系:将用户信息(
userId
和csId
)存储到当前 socket 对象中,并在userSocketMap
中存储用户与 socket 的映射关系。 - 加入房间:根据
csId
创建一个房间,并让用户加入该房间。 - 广播在线状态:通过
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 实例// 提取房间内所有用户的 userIdconst 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 uLEFT 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' 事件,将查询结果传递给客户端
});
代码功能总结:
- 获取房间信息:
- 根据当前用户的
csId
构造房间名称。 - 使用
io.in(room).fetchSockets()
获取房间内所有用户的 socket 实例。 - 从每个 socket 实例中提取
userId
,形成一个用户 ID 数组。
- 根据当前用户的
- 数据库查询:
- 查询房间内用户的详细信息,包括用户的基本信息(
id
、role
、username
)。 - 查询每个用户发送给当前用户且未被阅读的消息数量(
message_count
)。 - 使用
LEFT JOIN
关联messages
表,筛选出未读消息。 - 使用
GROUP BY
确保每个用户只返回一条记录。
- 查询房间内用户的详细信息,包括用户的基本信息(
- 发送结果:
- 将查询结果通过
socket.emit
发送给当前用户,事件名称为myUsersList
。
- 将查询结果通过
优化技巧:
- 使用
fetchSockets()
获取房间内所有socket实例 - 单次SQL查询获取用户信息+未读消息数
- LEFT JOIN确保离线用户也能被查询到
2.3 私聊消息处理
// 当客户端发送 'private_message' 事件时,触发这个回调函数
socket.on("private_message", async (data) => {// 获取接收者的 socket.idconst 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, // 发送者的 IDcontent: data.content, // 消息内容timestamp: new Date() // 消息发送的时间戳});}// 消息持久化:将消息存储到数据库中await pool.execute( // 使用数据库连接池执行 SQL 插入语句'INSERT INTO messages VALUES (?, ?, ?, ?)', // 插入消息到 messages 表[data.senderId, data.receiverId, data.content, new Date()] // 插入的值:发送者 ID、接收者 ID、消息内容、消息发送时间);
});
代码功能总结:
- 获取接收者的 socket.id:
- 从
userSocketMap
中根据接收者的userId
获取对应的socket.id
。
- 从
- 实时消息推送:
- 如果接收者在线(存在对应的
socket.id
),则使用io.to(receiverSocketId).emit
向接收者的 socket 发送new_private_message
事件,包含发送者的 ID、消息内容和时间戳。
- 如果接收者在线(存在对应的
- 消息持久化:
- 将消息存储到数据库中,插入到
messages
表中,记录发送者 ID、接收者 ID、消息内容和发送时间。
- 将消息存储到数据库中,插入到
消息流设计:
- 通过Map快速查找接收者socket
- 使用
io.to(socketId).emit()
实现点对点推送 - 异步存储到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 秒)执行一次定时检测
代码功能总结
- 心跳接收:
- 当客户端发送
heartbeat
事件时,更新userHeartbeats
中对应用户的心跳时间,记录为当前时间戳。
- 当客户端发送
- 定时检测:
- 使用
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);
调试技巧:
- 定期打印连接状态
- 输出完整的用户映射关系
- 生产环境可替换为日志系统
四、性能优化建议
-
Redis集成:
// 使用Redis存储映射关系 const redisClient = require('redis').createClient(); await redisClient.set(`user:${userId}:socket`, socket.id);
-
消息分片:
// 大消息分片处理 socket.on('message_chunk', (chunk) => {// 重组逻辑... });
-
负载均衡:
# 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()));
通过以上实现,您的聊天系统将具备:
- 完善的用户状态管理
- 可靠的私聊功能
- 高效的心跳机制
- 良好的可扩展性
建议在生产环境中添加:
- JWT认证
- 消息加密
- 限流防护
- 监控告警系统
相关文章:
Vue3 + Node.js 实现客服实时聊天系统(WebSocket + Socket.IO 详解)
Node.js 实现客服实时聊天系统(WebSocket Socket.IO 详解) 一、为什么选择 WebSocket? 想象一下淘宝客服的聊天窗口:你发消息,客服立刻就能看到并回复。这种即时通讯效果是如何实现的呢?我们使用 Vue3 作…...

强化学习PPO算法学习记录
1. 四个模型: Policy Model:我们想要训练的目标语言模型。我们一般用SFT阶段产出的SFT模型来对它做初始化。Reference Model:一般也用SFT阶段得到的SFT模型做初始化,在训练过程中,它的参数是冻结的。Ref模型的主要作用…...

从零开始:用PyTorch构建CIFAR-10图像分类模型达到接近1的准确率
为了增强代码可读性,代码均使用Chatgpt给每一行代码都加入了注释,方便大家在本文代码的基础上进行改进优化。 本文是搭建了一个稍微优化了一下的模型,训练200个epoch,准确率达到了99.74%,简单完成了一下CIFAR-10数据集…...
uni-app使用web-view组件APP实现返回上一页
一、功能概述 本案例实现了在Uniapp中内嵌H5网页并深度控制的三项核心功能: 隐藏指定特征的内链元素自定义导航栏返回逻辑Webview原生特性保留 二、代码解析 2.1 基础结构 <template><view><web-view :webview-styles"webviewStyles"…...
Apache Velocity代码生成简要介绍
Apache Velocity 概述 Apache Velocity 是一个基于 Java 的模板引擎,它允许将 Java 代码与 HTML、XML 或其他文本格式分离,实现视图与数据的解耦。在 Web 开发中,Velocity 常用于生成动态网页内容;在其他场景下,也可用…...

初学Python爬虫
文章目录 前言一、 爬虫的初识1.1 什么是爬虫1.2 爬虫的核心1.3 爬虫的用途1.4 爬虫分类1.5 爬虫带来的风险1.6. 反爬手段1.7 爬虫网络请求1.8 爬虫基本流程 二、urllib库初识2.1 http和https协议2.2 编码解码的使用2.3 urllib的基本使用2.4 一个类型六个方法2.5 下载网页数据2…...

【办公类-99-05】20250508 D刊物JPG合并PDF便于打印
背景需求 委员让我打印2024年2025年4月的D刊杂志,A4彩打,单面。 有很多JPG,一个个JPG图片打开,实在太麻烦了。 我需要把多个jpg图片合并成成为一个PDF,按顺序排列打印。 deepseek写Python代码 代码展示 D刊jpg图片合…...
高效C/C++之十:Coverity修复问题:尽量多使用 c++强制类型转化
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了: 这一篇我们开始讲: 高效C/C之十:Coverity修复问题:尽量多使用 c强制类型转化 目录 【关注我,后…...

相机的方向和位置
如何更好的控制相机按照我们需要来更好的观察我们需要的地貌呢? 使用 // setview瞬间到达指定位置,视角//生成position是天安门的位置var position Cesium.Cartesian3.fromDegrees(116.397428,39.90923,100)viewer.camera.setView({//指定相机位置destination: position, 在…...

suna界面实现原理分析(二):浏览器工具调用可视化
这是一个基于React的浏览器操作可视化调试组件,主要用于在AI开发工具中展示网页自动化操作过程(如导航、点击、表单填写等)的执行状态和结果。以下是关键技术组件和功能亮点的解析: 一、核心功能模块 浏览器操作状态可视化 • 实时…...

操作系统面试问题(4)
32.什么是操作系统 操作系统是一种管理硬件和软件的应用程序。也是运行在计算机中最重要的软件。它为硬件和软件提供了一种中间层,让我们无需关注硬件的实现,把心思花在软件应用上。 通常情况下,计算机上会运行着许多应用程序,它…...
websocketd 10秒教程
websocketd 参考地址:joewalnes/websocketd 官网地址:websocketd websocketd简述 websocketd是一个简单的websocket服务Server,运行在命令行方式下,可以通过websocketd和已经有程序进行交互。 现在,可以非常容易地构…...

C++ Dll创建与调用 查看dll函数 MFC 单对话框应用程序(EXE 工程)改为 DLL 工程
C Dll创建 一、添加 DllMain(必要) #include <fstream>void Log(const char* msg) {std::ofstream f("C:\\temp\\dll_log.txt", std::ios::app);f << msg << std::endl; }BOOL APIENTRY DllMain(HMODULE hModule, DWORD u…...

【prometheus+Grafana篇】基于Prometheus+Grafana实现Linux操作系统的监控与可视化
💫《博主主页》: 🔎 CSDN主页 🔎 IF Club社区主页 🔥《擅长领域》:擅长阿里云AnalyticDB for MySQL(分布式数据仓库)、Oracle、MySQL、Linux、prometheus监控;并对SQLserver、NoSQL(MongoDB)有了…...
小刚说C语言刷题—1004阶乘问题
1.题目描述 编程求 123⋯n 。 输入 输入一行,只有一个整数 n(1≤n≤10); 输出 输出只有一行(这意味着末尾有一个回车符号),包括 1 个整数。 样例 输入 5 输出 120 2.参考代码(C语言版) #include <stdio…...

CurrentHashMap的整体系统介绍及Java内存模型(JVM)介绍
当我们提到ConurrentHashMap时,先想到的就是HashMap不是线程安全的: 在多个线程共同操作HashMap时,会出现一个数据不一致的问题。 ConcurrentHashMap是HashMap的线程安全版本。 它通过在相应的方法上加锁,来保证多线程情况下的…...

spring ai alibaba 使用 SystemPromptTemplate 很方便的集成 系统提示词
系统提示词可以是.st 文件了,便于修改和维护 1提示词内容: 你是一个有用的AI助手。 你是一个帮助人们查找信息的人工智能助手。 您的名字是{name} 你应该用你的名字和{voice}的风格回复用户的请求。 每一次回答的时候都要增加一个65字以内的标题形如:【…...
@PostConstruct @PreDestroy
PostConstruct 是 Java EE(现 Jakarta EE)中的一个注解,用于标记一个方法在对象初始化完成后立即执行。它在 Spring 框架、Java Web 应用等场景中广泛使用,主要用于资源初始化、依赖注入完成后的配置等操作。 1. 基本作用 执行时…...

网络的搭建
1、rpm rpm -ivh 2、yum仓库(rpm包):网络源 ----》网站 本地源 ----》/dev/sr0 光盘映像文件 3、源码安装 源码安装(编译) 1、获取源码 2、检测环境生成Ma…...

C++学习之类和对象_1
1. 面向过程与面向对象 C语言是面向过程的,注重过程,通过调用函数解决问题。 比如做番茄炒蛋:买番茄和鸡蛋->洗番茄和打鸡蛋->先炒蛋->把蛋放碟子上->炒番茄->再把蛋倒回锅里->加调料->出锅 而C是面向对象的ÿ…...

YOLOv12云端GPU谷歌免费版训练模型
1.效果 2.打开 https://colab.research.google.com/?utm_sourcescs-index 3.上传代码 4.解压 !unzip /content/yolov12-main.zip -d /content/yolov12-main 5.进入yolov12-main目录 %cd /content/yolov12-main/yolov12-main 6.安装依赖库 !pip install -r requirements.…...

OpenCV进阶操作:图像直方图、直方图均衡化
文章目录 一、图像直方图二、图像直方图的作用三、使用matplotlib方法绘制直方图2.使用opencv的方法绘制直方图(划分16个小的子亮度区间)3、绘制彩色图像的直方图 四、直方图均衡化1、绘制原图的直方图2、绘制经过直方图均衡化后的图片的直方图3、自适应…...

基环树(模板) 2876. 有向图访问计数
对于基环树,我们可以通过拓扑排序去掉所有的树枝,只剩下环,题目中可能会有多个基环树 思路:我们先利用拓扑排序将树枝去掉,然后求出每个基环树,之后反向dfs求得所有树枝的长度即可 class Solution { publi…...

【物联网】基于树莓派的物联网开发【1】——初识树莓派
使用背景 物联网开发从0到1研究,以树莓派为基础 场景介绍 系统学习Linux、Python、WEB全栈、各种传感器和硬件 接下来程序猫将带领大家进军物联网世界,从0开始入门研究树莓派。 认识树莓派 正面图示: 1:树莓派简介 树莓派…...
Qt读写XML文档
XML 结构与概念简介 XML(可扩展标记语言) 是一种用于存储和传输结构化数据的标记语言。其核心特性包括: 1、树状结构:XML 数据以层次化的树形结构组织,包含一个根元素(Root Element)ÿ…...

学习Python的第一天之网络爬虫
30岁程序员学习Python的第一天:网络爬虫 Requests库 1、requests库安装 windows系统通过管理员打开cmd,运行pip install requests!测试案例: 2、Requests库的两个重要对象 Response对象Resoponse对象包含服务器返回的所有信息ÿ…...
前端展示后端返回的图片流
一、请求 重点:添加responseType: “blob”, // Vue2组件中请求示例 methods: {fetchImage() {return axios.get(/api/getImage, {params: { id: 123 },responseType: blob // 关键配置(重点,必须配置)});} }或 export function…...
65.微服务保姆教程 (八) 微服务开发与治理实战
微服务开发与治理实战:搭建一个简单的微服务系统 在这个实战中,我们将使用以下技术栈来搭建一个简单的微服务系统: 注册中心和配置中心:使用 Nacos。服务开发框架:使用 Spring Boot。服务间通信:使用 Feign。API 网关:使用 Spring Cloud Gateway。依赖管理工具:使用 M…...
AI服务器通常会运用在哪些场景当中?
人工智能行业作为现代科技的杰出代表,在多个领域当中发展其强大的应用能力和价值,随之,AI服务器也在各个行业中日益显现出来,为各个行业提供了强大的计算能力和处理能力,帮助企业处理复杂的大规模数据,本文…...

linux下的Redis的编译安装与配置
配合做开发经常会用到redis,整理下编译安装配置过程,仅供参考! --------------------------------------Redis的安装与配置-------------------------------------- 下载 wget https://download.redis.io/releases/redis-6.2.6.tar.gz tar…...