WebSocket结合Socket.IO实现简易客服聊天系统全解析
一、技术选型对比
技术 优点 缺点 适用场景 原生WebSocket 浏览器原生支持,性能好 API较底层,需手动处理断线重连等逻辑 简单实时应用 Socket.IO 自动重连,房间管理,兼容性好 体积较大,有一定学习成本 复杂实时应用,生产环境
为什么选择Socket.IO?
内置断线自动重连 支持房间/命名空间管理 兼容老旧浏览器(自动降级) 丰富的API和社区支持
二、核心实现流程
2.1 系统架构图
前端浏览器
↑↓ HTTP/WebSocket
Node.js服务器(Socket.IO)
↑↓ 数据库(可选)
消息持久化存储
2.2 前后端交互时序图
前端
后端
所有前端
指定前端
建立WebSocket连接
发送login事件(用户名)
返回login_success
广播user_list
发送private_message
定向转发消息
断开连接
更新user_list
前端
后端
所有前端
指定前端
三、前端代码详解
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' , '大家好!' ) ;