WebSocket结合Socket.IO实现简易客服聊天系统全解析
 
一、技术选型对比
 
| 技术 | 优点 | 缺点 | 适用场景 | 
|---|
| 原生WebSocket | 浏览器原生支持,性能好 | API较底层,需手动处理断线重连等逻辑 | 简单实时应用 | 
| Socket.IO | 自动重连,房间管理,兼容性好 | 体积较大,有一定学习成本 | 复杂实时应用,生产环境 | 
 
为什么选择Socket.IO?
 
- 内置断线自动重连
- 支持房间/命名空间管理
- 兼容老旧浏览器(自动降级)
- 丰富的API和社区支持
二、核心实现流程
 
2.1 系统架构图
 
前端浏览器↑↓ HTTP/WebSocket
Node.js服务器(Socket.IO)↑↓ 数据库(可选)
消息持久化存储
 
2.2 前后端交互时序图
 
  
  
三、前端代码详解
 
3.1 核心功能实现
 
| 功能 | 代码片段 | 说明 | 
|---|
| 建立连接 | socket = io('http://localhost:3000') | 连接到指定服务器的Socket.IO服务 | 
| 用户登录 | socket.emit('login', username) | 发送登录事件,携带用户名 | 
| 接收消息 | socket.on('message', (data) => {...}) | 监听服务器推送的消息 | 
| 发送私聊 | socket.emit('private_message', {to, message}) | 发送私聊消息到指定用户 | 
| 用户列表更新 | socket.on('user_list', (users) => {...}) | 实时更新在线用户列表 | 
 
3.2 页面结构分析
 
<div class="container"><div id="login"><input type="text" id="username"><button onclick="login()">登录</button></div><div id="chat" style="display: none;"><h3>当前用户:<span id="currentUser"></span></h3><div><h4>在线用户</h4><ul id="userList"></ul></div><div id="messages"></div><div><select id="receiver"><option value="">选择接收人</option></select><input type="text" id="messageInput"><button onclick="sendMessage()">发送</button></div></div>
</div>
 
3.3 js功能实现
 
先声明全局变量:
 
let socket;       let currentUser;  
 
用户登录,即进入房间:
 
function login() {const username = document.getElementById('username').value.trim();if (!username) return;socket = io('http://localhost:3000');currentUser = username;socket.on('connect', () => {socket.emit('login', username);});socket.on('login_success', () => {document.getElementById('login').style.display = 'none';document.getElementById('chat').style.display = 'block';document.getElementById('currentUser').textContent = username;});socket.on('login_error', (err) => {alert(err);});socket.on('user_list', (users) => {const list = document.getElementById('userList');const receiver = document.getElementById('receiver');list.innerHTML = '';receiver.innerHTML = '<option value="">选择接收人</option>';users.forEach(user => {if (user !== currentUser) {const li = document.createElement('li');li.textContent = user;list.appendChild(li);const option = document.createElement('option');option.value = user;option.textContent = user;receiver.appendChild(option);}});});socket.on('message', (data) => {const messagesDiv = document.getElementById('messages');const msgEl = document.createElement('div');msgEl.className = `message ${data.self ? 'self' : ''}`;msgEl.textContent = `[${data.from} → ${data.to}]: ${data.message}`;messagesDiv.appendChild(msgEl);messagesDiv.scrollTop = messagesDiv.scrollHeight;});socket.on('error', (err) => {alert(err);});}
 
用户发送消息:
 
		function sendMessage() {const to = document.getElementById('receiver').value;const message = document.getElementById('messageInput').value.trim();if (!to || !message) return;socket.emit('private_message', {to,        message    });document.getElementById('messageInput').value = '';}
 
四、后端代码详解
 
为了实现1对1的聊天功能 to().emait('',data) ,我们需要知道你要发送的那个人的 socket.id ,这里就需要对每个登录的人与他的socket.id进行关联,因为 socket.id 前端不知道,所以后端需要创建一个new Map对象对用户的name进行映射,用户的name对应它的socket.id。在发消息的时候只需要知道要发的人的name即可进行get从map对象中获取对应的socket.id,从而实现一对一聊天
 
4.1 主要事件处理
 
| 事件类型 | 处理逻辑 | 业务作用 | 
|---|
| connection | 初始化socket连接,打印日志 | 新客户端连接处理入口 | 
| login | 检查用户名冲突,更新users映射,广播user_list | 用户身份认证和在线状态管理 | 
| private_message | 查找接收者socket.id,分别向发送方和接收方发送消息 | 实现私聊消息转发 | 
| disconnect | 从users映射中移除用户,广播更新后的user_list | 处理用户离线状态 | 
 
4.2 环境配置
 
const http = require('http');       
const socketio = require('socket.io'); 
const server = http.createServer((req, res) => {res.writeHead(200, { 'Content-Type': 'text/html' });res.end('<h1>Chat Server</h1>');
}).listen(3000, () => {console.log("Server running at http://localhost:3000");
});
const io = socketio(server, {cors: {origin: "*", methods: ["GET", "POST"] }
});
 
4.3 用户信息管理
 
const users = new Map();
 
4.4 具体功能实现
 
io.on('connection', (socket) => {console.log(`现在登录的用户socketid: ${socket.id}`);socket.on('login', (username) => {if (users.has(username)) {socket.emit('login_error', '当前用户名已经被注册');return;}users.set(username, socket.id); socket.username = username;     io.emit('user_list', Array.from(users.keys()));socket.emit('login_success');});socket.on('private_message', ({ to, message }) => {const targetSocketId = users.get(to);if (!targetSocketId) {socket.emit('error', '用户未找到');console.log("用户未找到");return;}socket.emit('message', {from: socket.username, to: to,               message: message,     self: true            });socket.to(targetSocketId).emit('message', {from: socket.username,to: to,message: message,self: false           });});socket.on('disconnect', () => {if (socket.username) {users.delete(socket.username);io.emit('user_list', Array.from(users.keys()));console.log(`用户断开连接: ${socket.username}`);}});
});
 
五、.on和.emit方法解惑
 
5.1 Socket.io 前端视角的 on 和 emit 方法对比
 
从前端(客户端)的角度来看,socket.on() 和 socket.emit() 的使用方式与后端类似,但角色和典型场景有所不同。以下是前端视角的对比表格:
 
| 特性 | socket.on('event', callback)(前端) | socket.emit('event', data)(前端) | 
|---|
| 用途 | 监听服务器发送的事件 | 向服务器发送事件 | 
| 方向 | 接收服务器数据 | 向服务器发送数据 | 
| 回调函数 | 有,处理服务器推送的数据 | 无,只发送数据 | 
| 作用范围 | 监听特定服务器事件 | 向服务器发送特定事件 | 
| 是否等待响应 | 被动接收服务器消息 | 主动向服务器发起请求 | 
| 典型使用场景 | 接收通知、更新、广播消息等 | 发送用户操作、请求数据等 | 
 
**socket.on('event', callback) **
 
- 用于监听服务器发送的事件
- 当服务器触发对应事件时,回调函数会执行
- 典型前端使用场景:  
- 示例:
socket.on('newMessage', (message) => {console.log('收到新消息:', message);
});
socket.on('userJoined', (username) => {console.log(`${username} 加入了聊天室`);
});
 
socket.emit('event', data)
 
- 用于向服务器发送事件和数据
- 可以带参数发送给服务器
- 典型前端使用场景:  
- 示例:
socket.emit('sendMessage', {text: '你好!',room: 'general'
});
socket.emit('joinRoom', {roomId: '123',userId: 'user456'
});
 
5.2 Socket.io 的 on 和 emit 方法对比
 
是的,socket.on() 和 socket.emit() 在 Socket.io 中有不同的用途。下面是一个对比表格:
 
| 特性 | socket.on('event', callback) | socket.emit('event', data) | 
|---|
| 用途 | 注册/监听事件 | 触发/发送事件 | 
| 方向 | 接收数据 | 发送数据 | 
| 回调函数 | 有,处理接收的数据 | 无,只发送数据 | 
| 作用范围 | 监听特定事件 | 向特定目标发送事件 | 
| 是否等待响应 | 被动等待 | 主动触发 | 
| 典型使用场景 | 服务器监听客户端请求 | 客户端或服务器发送消息 | 
 
socket.on('event', callback)
 
- 用于注册事件监听器,等待接收特定事件
- 当对应的事件被触发时,回调函数会被执行
- 示例:
socket.on('login', (credentials) => {console.log('收到登录请求:', credentials);
});
 
socket.emit('event', data)
 
- 用于触发事件并发送数据
- 可以向特定客户端或所有客户端广播消息
- 示例:
socket.emit('login', { username: 'user', password: 'pass' });
io.emit('message', '大家好!');