前端 socket.io-client
注意事项
shell
# 关于 io 实例,创建连接和创建命名空间的概念不要混淆
const socket = io("http://localhost:3000/chat");
以上代码的作用有两个功能:
前提是 需要判断是否已经建立 socket 连接
未连接的情况下:一是创建 http://localhost:3000 连接通道,二是创建 /chat 命名空间
已连接的情况下:则省略一,只进入二
后面还可以写多个 io 实例代码:const game= io('/game')
后续的实例不会再创建连接,在已有的连接基础上创建一个子通道,只是创建对应的命名空间
如果不写域名,直接写 const game= io('/game') 且只有这一个实例
那么,Socket.IO 会自动补全域名和端口,默认连接当前网站的域名 + 当前端口,再创建一个 socket 连接,再创建一个 /game 命名空间
域名/端口是否相同还有不同的说法
域名 / 端口不同 → 新建连接
io('a.com')
io('b.com')
→ 两条连接
域名 / 端口相同 → 复用连接
io('a.com')
io('a.com/game')
→ 同一条连接,不同命名空间
命名空间 ≠ 房间
命名空间:连接上的子通道
房间:命名空间里的小组
# 总结
1. 不同域名 = 不同连接
2. 相同域名 = 复用连接 + 多命名空间
3. io('/game') 不写域名 = 连接当前网站
4. 连接是物理通道
5. 命名空间是逻辑通道
6. 房间是命名空间下的小组核心 API
shell
socket.io-client 库的核心是围绕 io 函数、Manager 和 Socket 这三个层次构建的。
io 用于创建连接并获取 Socket 实例;Socket 则用于事件收发,是进行数据通信的核心对象。
Manager 负责底层连接和重连逻辑;
# io
io(url, options) 连接到指定 url 的服务器,options 可配置重连、认证等参数
常用配置选项 (Options)
auth Object {} 在连接时发送给服务器的认证数据(如 { token: '...' })。
autoConnect Boolean true 若设为 false,需要手动调用 .connect() 才会发起连接。
reconnection Boolean true 是否启用自动重连。
reconnectionAttempts Number Infinity 最大重连尝试次数。
reconnectionDelay Number 1000 初始重连延迟时间(毫秒),reconnectionDelayMax 可设上限。
timeout Number 20000 连接超时时间(毫秒)。
transports Array ['polling', 'websocket'] 指定要使用的传输方式及其顺序。
query Object {} 作为查询参数附加到连接 URL 上的额外数据。
# 管理器 (Manager)
动态读取或修改 Manager 的配置选项
.reconnection([value])
.reconnectionAttempts([value])
.reconnectionDelay([value])
.reconnectionDelayMax([value])
.timeout([value])
核心控制方法
.open(callback) / .connect(callback):如果初始化时设置了 autoConnect: false,则需要调用此方法手动建立连接。
.socket(nsp, options):为指定的命名空间(Namespace, nsp)创建一个新的 Socket 实例
监听事件
error: 发生连接错误时触发。
reconnect: 成功重新连接后触发。
reconnect_attempt: 每次尝试重新连接时触发。
reconnect_error: 重新连接尝试失败时触发。
reconnect_failed: 重连尝试次数耗尽(reconnectionAttempts)后触发。
ping: 接收到服务器发来的 ping 包时触发,常用于计算网络延迟。
# Socket 实例的属性和方法
核心属性
.id:当前 Socket 会话的唯一标识符(String)。在 connect 事件触发后才有效。
.connected:返回一个布尔值,表示当前是否已连接。
.disconnected:返回一个布尔值,表示当前是否已断开连接。
核心方法
.on(eventName, callback):为指定事件(如服务器的自定义消息)添加监听器。
.once(eventName, callback):添加一个只触发一次的监听器,触发后会自动移除。
.off([eventName], [listener]):移除事件监听器。若不传参数,则移除所有事件的所有监听器。
.emit(eventName, [...args], [ack]):向服务器发送一个事件。可以通过回调函数 ack 接收服务器的确认(Acknowledgment)。
.send([...args], [ack]):.emit('message', ...) 的别名,用于发送消息类型的事件。
.connect() / .open():如果创建 Socket 时设置了 autoConnect: false,调用此方法手动连接。
.compress(value):设置一个压缩标志,用于下一个发送的数据包。例如 socket.compress(false).emit('large', data) 可用于禁止压缩大量数据。
监听事件
connect:成功连接到服务器时触发。
disconnect:与服务器断开连接时触发。
connect_error:连接失败时触发。
reconnect:断线后成功重连时触发。
reconnecting:正在尝试重连时触发。
reconnect_attempt:开始一次新的重连尝试时触发。
reconnect_error:重连尝试失败时触发。
reconnect_failed:重连次数耗尽,最终失败时触发。简单场景
1、基础连接与收发消息
js
import io from "socket.io-client";
const socket = io('https://your-http-server.com');
socket.on('connect', () => console.log('已连接'));
socket.on('news', (data) => console.log('收到新闻:', data));
socket.emit('user action', { type: 'click' });2、认证与手动连接
js
const socket = io({
auth: { token: 'your-jwt-token' },
autoConnect: false // 先不自动连接
});
socket.connect(); // 手动连接3、命名空间与房间
js
const adminSocket = io('/admin'); // 连接到 /admin 命名空间
const roomSocket = io('https://your-server.com/room/123'); // 连接到特定房间自动重连
js
let ws;
let lockReconnect = false; // 🔒 加锁:防止重复重连
let reconnectTimer; // ⏱ 延迟定时器
// 创建连接
function connect() {
ws = new WebSocket("ws://localhost:3000");
ws.onopen = () => {
console.log("连接成功");
};
// 断开 → 重连
ws.onclose = () => {
reconnect();
};
// 报错 → 重连
ws.onerror = () => {
reconnect();
};
}
// 重连函数(核心!包含 延迟 + 锁)
function reconnect() {
// 🔒 加锁:如果正在重连,直接返回,不重复执行
if (lockReconnect) return;
lockReconnect = true; // 上锁
// ⏱ 加延迟:3 秒后再重连(不会疯狂重连)
reconnectTimer = setTimeout(() => {
connect(); // 重新连接
lockReconnect = false; // 解锁
}, 3000); // 延迟 3 秒
}
// 启动
connect();复杂场景
shell
场景类型 关键技术点 适用场景
确认机制 Acknowledgment 订单、支付、重要操作
心跳检测 定时 ping/pong 弱网环境、移动端
房间管理 Namespaces + Rooms 聊天室、游戏、多租户
离线队列 消息缓存 + 重试 即时通讯、数据上报
文件传输 分块上传 + 进度 大文件上传、音视频
数据节流 限频发送 鼠标轨迹、传感器数据
二进制传输 Blob/ArrayBuffer 音视频、截图、文件
订阅模式 动态订阅/取消 股票行情、新闻推送
多路复用 Manager + Namespace 多模块应用
错误处理 降级 + 重连策略 生产环境必备1、带确认机制的消息(Acknowledgment)
适用于需要确保服务器已接收并处理消息的场景,比如支付、订单创建等。
js
// 发送订单创建请求,等待服务器确认
socket.emit('create order', { productId: 123, quantity: 2 }, (response) => {
if (response.status === 'success') {
console.log('订单创建成功,订单号:', response.orderId);
} else {
console.error('订单创建失败:', response.error);
}
});
// 服务器端处理(Node.js 示例)
socket.on('create order', (orderData, callback) => {
// 处理订单逻辑...
if (success) {
callback({ status: 'success', orderId: 'ORD-12345' });
} else {
callback({ status: 'error', error: '库存不足' });
}
});2、心跳检测与自动重连优化
适用于网络不稳定的环境,如移动端、弱网场景。
js
let lastHeartbeat = Date.now();
let heartbeatInterval;
const socket = io('https://your-server.com', {
reconnection: true,
reconnectionAttempts: 10, // 最多重连10次
reconnectionDelay: 1000, // 初始延迟1秒
reconnectionDelayMax: 5000, // 最大延迟5秒
timeout: 10000 // 连接超时10秒
});
// 监听服务器心跳
socket.on('heartbeat', () => {
lastHeartbeat = Date.now();
});
// 客户端主动发送心跳(可选)
setInterval(() => {
if (socket.connected) {
socket.emit('client heartbeat');
}
}, 30000);
// 检测连接是否假死(长时间未收到心跳)
setInterval(() => {
if (socket.connected && (Date.now() - lastHeartbeat) > 60000) {
console.warn('连接可能假死,手动断开重连');
socket.disconnect();
socket.connect();
}
}, 10000);3、房间(Rooms)与命名空间(Namespaces)管理
适用于多频道聊天、多游戏房间、多租户系统等场景。
js
// 主命名空间 - 全局通知
const mainSocket = io('/');
mainSocket.emit('join room', 'global-notifications');
mainSocket.on('global announcement', (msg) => {
console.log('全局公告:', msg);
});
// 游戏房间命名空间 - 实时对战
const gameSocket = io('/game', {
auth: { userId: 'user123' }
});
// 加入指定房间
gameSocket.emit('join game room', 'room-abc123');
// 监听房间内事件
gameSocket.on('player joined', (playerInfo) => {
console.log(`${playerInfo.name} 加入了房间`);
});
gameSocket.on('game state update', (gameState) => {
updateGameUI(gameState);
});
// 发送游戏操作
gameSocket.emit('player action', {
roomId: 'room-abc123',
action: 'move',
data: { x: 100, y: 200 }
});
// 离开房间
function leaveRoom(roomId) {
gameSocket.emit('leave room', roomId);
}4、消息缓存与离线队列
适用于需要保证消息可靠送达的场景,即使网络断开也要缓存消息。
js
class ReliableSocket {
constructor(url, options) {
this.socket = io(url, options);
this.messageQueue = [];
this.isConnected = false;
this.socket.on('connect', () => {
this.isConnected = true;
this.flushQueue(); // 连接成功后发送缓存的消息
});
this.socket.on('disconnect', () => {
this.isConnected = false;
});
}
// 可靠发送:如果离线则缓存
emitReliable(event, data, maxRetries = 3) {
const message = {
id: Date.now() + '-' + Math.random(),
event,
data,
retries: 0,
maxRetries
};
if (this.isConnected) {
this.sendWithRetry(message);
} else {
console.log('离线状态,缓存消息:', message);
this.messageQueue.push(message);
}
}
sendWithRetry(message) {
this.socket.emit(message.event, message.data, (ack) => {
if (ack && ack.success) {
console.log('消息发送成功:', message.id);
} else if (message.retries < message.maxRetries) {
message.retries++;
console.log(`重试发送消息 ${message.id},第 ${message.retries} 次`);
setTimeout(() => this.sendWithRetry(message), 1000 * message.retries);
} else {
console.error('消息发送失败:', message);
this.handleFailedMessage(message);
}
});
}
flushQueue() {
console.log(`发送 ${this.messageQueue.length} 条缓存消息`);
while (this.messageQueue.length > 0) {
const msg = this.messageQueue.shift();
this.sendWithRetry(msg);
}
}
handleFailedMessage(message) {
// 将失败消息保存到 localStorage 供下次启动时重试
const failed = JSON.parse(localStorage.getItem('failedMessages') || '[]');
failed.push(message);
localStorage.setItem('failedMessages', JSON.stringify(failed));
}
}
// 使用示例
const reliableSocket = new ReliableSocket('https://your-server.com');
reliableSocket.emitReliable('important data', { userId: 123, content: '重要内容' });5、进度上传与文件传输
适用于大文件上传、实时进度展示等场景。
js
function uploadFileWithProgress(file) {
const chunkSize = 64 * 1024; // 64KB 分块
let offset = 0;
const totalChunks = Math.ceil(file.size / chunkSize);
function sendNextChunk() {
const chunk = file.slice(offset, offset + chunkSize);
const reader = new FileReader();
reader.onload = (e) => {
const chunkData = {
fileName: file.name,
chunkIndex: Math.floor(offset / chunkSize),
totalChunks: totalChunks,
data: e.target.result,
fileId: uploadId
};
// 发送分块数据
socket.emit('upload chunk', chunkData, (ack) => {
if (ack && ack.received) {
offset += chunkSize;
const progress = Math.floor((offset / file.size) * 100);
// 触发进度事件
socket.emit('upload progress', {
fileId: uploadId,
progress: progress
});
if (offset < file.size) {
sendNextChunk(); // 继续发送下一块
} else {
console.log('文件上传完成!');
socket.emit('upload complete', { fileId: uploadId });
}
}
});
};
reader.readAsDataURL(chunk);
}
const uploadId = Date.now() + '-' + file.name;
sendNextChunk();
}
// 上传进度监听
socket.on('upload progress', (data) => {
updateProgressBar(data.progress);
console.log(`上传进度: ${data.progress}%`);
});6、实时数据流与节流
适用于常见数据(如鼠标移动、股票行情),需要节流避免性能问题。
js
let lastEmit = 0;
const THROTTLE_MS = 100; // 每100毫秒最多发送一次
// 鼠标移动轨迹跟踪
document.addEventListener('mousemove', (e) => {
const now = Date.now();
if (now - lastEmit >= THROTTLE_MS) {
lastEmit = now;
socket.emit('mouse move', {
x: e.clientX,
y: e.clientY,
timestamp: now
});
}
});
// 或者使用 requestAnimationFrame 优化
let animationFrameId;
let lastPosition = { x: 0, y: 0 };
document.addEventListener('mousemove', (e) => {
lastPosition = { x: e.clientX, y: e.clientY };
if (!animationFrameId) {
animationFrameId = requestAnimationFrame(() => {
socket.emit('mouse move', lastPosition);
animationFrameId = null;
});
}
});7、二进制数据高效传输
适用于音视频流、图片、文件等二进制数据。
js
// 发送音频流
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
const mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
// 直接发送 Blob 数据
socket.emit('audio data', event.data);
}
};
mediaRecorder.start(1000); // 每秒发送一次
});
// 接收二进制数据并播放
socket.on('audio data', (audioBlob) => {
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
audio.play();
});
// 发送 Canvas 截图(二进制)
function sendScreenshot() {
const canvas = document.getElementById('game-canvas');
canvas.toBlob((blob) => {
socket.emit('screenshot', blob);
}, 'image/jpeg', 0.8);
}
setInterval(sendScreenshot, 5000); // 每5秒发送一次截图8、动态订阅/取消订阅模式
适用于用户可以灵活开关不同数据流的场景。
js
class DataSubscriptionManager {
constructor(socket) {
this.socket = socket;
this.subscriptions = new Map(); // 存储当前订阅
}
subscribe(topic, callback) {
if (!this.subscriptions.has(topic)) {
this.subscriptions.set(topic, new Set());
// 第一次订阅时,通知服务器
this.socket.emit('subscribe', topic);
// 为该 topic 添加全局监听器
this.socket.on(topic, (data) => {
const callbacks = this.subscriptions.get(topic);
if (callbacks) {
callbacks.forEach(cb => cb(data));
}
});
}
this.subscriptions.get(topic).add(callback);
console.log(`订阅了 ${topic},当前订阅者数: ${this.subscriptions.get(topic).size}`);
// 返回取消订阅函数
return () => this.unsubscribe(topic, callback);
}
unsubscribe(topic, callback) {
const callbacks = this.subscriptions.get(topic);
if (callbacks) {
callbacks.delete(callback);
if (callbacks.size === 0) {
// 最后一个订阅者取消时,通知服务器
this.socket.emit('unsubscribe', topic);
this.socket.off(topic);
this.subscriptions.delete(topic);
console.log(`取消订阅 ${topic}`);
}
}
}
}
// 使用示例
const subManager = new DataSubscriptionManager(socket);
// 订阅股票行情
const unsubscribeStock = subManager.subscribe('stock:APPL', (data) => {
console.log(`苹果股价: $${data.price}`);
});
// 订阅天气信息
subManager.subscribe('weather:beijing', (weather) => {
console.log(`北京天气: ${weather.temp}°C`);
});
// 5分钟后取消股票订阅
setTimeout(() => {
unsubscribeStock();
console.log('已取消股票订阅');
}, 5 * 60 * 1000);9、连接池与多路复用优化
适用于一个页面需要连接多个服务的场景,节省资源。
js
// 不好的做法:创建多个独立连接
// const socket1 = io('https://api1.example.com');
// const socket2 = io('https://api2.example.com');
// 好的做法:使用同一连接的不同命名空间
const manager = io.Manager('https://api.example.com', {
reconnection: true,
transports: ['websocket'] // 强制使用 WebSocket
});
// 多个命名空间共享同一个底层连接
const chatSocket = manager.socket('/chat');
const gameSocket = manager.socket('/game');
const notifySocket = manager.socket('/notify');
chatSocket.on('message', (msg) => console.log('聊天消息:', msg));
gameSocket.on('score', (score) => console.log('游戏分数:', score));
notifySocket.on('alert', (alert) => console.log('系统通知:', alert));
// 手动控制连接(节省带宽)
chatSocket.connect();
gameSocket.connect();
notifySocket.connect();
// 暂时不需要聊天功能时,可以断开但保持其他
function disableChat() {
chatSocket.disconnect();
// gameSocket 和 notifySocket 仍保持连接
}10、错误处理与优雅降级
适用于生产环境的完整错误处理方案。
js
class RobustSocket {
constructor(url, options) {
this.url = url;
this.options = {
reconnection: true,
reconnectionAttempts: 5,
timeout: 10000,
...options
};
this.connect();
}
connect() {
this.socket = io(this.url, this.options);
this.setupEventHandlers();
}
setupEventHandlers() {
this.socket.on('connect', () => {
console.log('✅ 连接成功,ID:', this.socket.id);
this.showToast('连接成功', 'success');
});
this.socket.on('connect_error', (error) => {
console.error('❌ 连接失败:', error.message);
this.showToast('连接服务器失败', 'error');
// 尝试降级方案
if (error.message.includes('websocket')) {
console.log('WebSocket 失败,尝试长轮询...');
this.socket.io.opts.transports = ['polling', 'websocket'];
}
});
this.socket.on('disconnect', (reason) => {
console.warn('⚠️ 断开连接:', reason);
if (reason === 'io server disconnect') {
// 服务器主动断开,手动重连
this.socket.connect();
}
// 其他原因(如网络问题),自动重连机制会处理
});
this.socket.on('reconnect_attempt', (attempt) => {
console.log(`🔄 重连尝试 ${attempt}`);
this.showToast(`正在重连... (${attempt}/${this.options.reconnectionAttempts})`, 'info');
});
this.socket.on('reconnect', (attempt) => {
console.log(`✅ 重连成功 (第 ${attempt} 次尝试)`);
this.showToast('重新连接成功', 'success');
});
this.socket.on('reconnect_failed', () => {
console.error('❌ 重连失败,已达最大尝试次数');
this.showToast('无法连接到服务器,请刷新页面', 'error');
this.enterOfflineMode();
});
this.socket.on('error', (error) => {
console.error('Socket 错误:', error);
// 记录错误到监控系统
this.logErrorToService(error);
});
}
showToast(message, type) {
// 实现 UI 提示
console.log(`[${type}] ${message}`);
}
enterOfflineMode() {
// 进入离线模式,保存用户操作到本地
console.log('进入离线模式');
}
logErrorToService(error) {
// 发送到错误监控服务(如 Sentry)
if (window.Sentry) {
Sentry.captureException(error);
}
}
emit(event, data, callback) {
if (this.socket && this.socket.connected) {
this.socket.emit(event, data, callback);
} else {
console.warn(`离线状态,无法发送事件: ${event}`);
// 缓存到本地
this.cacheOfflineEvent(event, data);
}
}
cacheOfflineEvent(event, data) {
const cache = JSON.parse(localStorage.getItem('offlineEvents') || '[]');
cache.push({ event, data, timestamp: Date.now() });
localStorage.setItem('offlineEvents', JSON.stringify(cache));
}
}