前端 WebSocket 是一种在客户端(浏览器)和服务器之间建立持久性连接的通信协议,它允许双方进行全双工(full-duplex)通信,即数据可以同时在两个方向上发送。与传统的 HTTP 请求-响应模型不同,WebSocket 建立连接后,服务器可以主动向客户端推送数据,而无需客户端发起请求。这使得 WebSocket 非常适合实现实时应用,如聊天室、在线游戏、实时数据更新等。
WebSocket 的基本使用
在前端使用 WebSocket 非常简单,主要通过 WebSocket
构造函数和其提供的事件监听器及方法。
-
创建 WebSocket 实例:
jsconst ws = new WebSocket('ws://localhost:8080/ws'); // 对于加密连接,使用 wss:// // const ws = new WebSocket('wss://your-domain.com/ws');
ws://
是 WebSocket 的非加密协议。wss://
是 WebSocket 的加密协议(WebSocket Secure),基于 TLS/SSL,类似于 HTTPS。
-
事件监听:
WebSocket 实例会触发以下事件,用于处理连接状态和接收消息:
onopen
:连接成功建立时触发。onmessage
:从服务器接收到消息时触发。event.data
包含接收到的数据。onerror
:发生错误时触发。onclose
:连接关闭时触发。event.code
和event.reason
提供关闭原因。
-
发送消息:
使用
send()
方法向服务器发送数据。jsws.send('Hello Server!'); ws.send(JSON.stringify({ type: 'chat', message: 'Hi there!' }));
send()
方法可以发送字符串、Blob、ArrayBuffer 或 ArrayBufferView 类型的数据。
-
关闭连接:
使用
close()
方法关闭 WebSocket 连接。jsws.close(1000, 'Closing normally'); // 可选地提供状态码和原因
基本使用示例:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Basic Demo</title>
</head>
<body>
<h1>WebSocket 演示</h1>
<input type="text" id="messageInput" placeholder="输入消息">
<button id="sendButton">发送</button>
<div id="messages"></div>
<script>
const messageInput = document.getElementById('messageInput');
const sendButton = document.getElementById('sendButton');
const messagesDiv = document.getElementById('messages');
// 假设你的 WebSocket 服务器运行在 ws://localhost:8080/ws
// 你需要一个后端服务器来运行 WebSocket 服务
const ws = new WebSocket('ws://localhost:8080/ws');
ws.onopen = (event) => {
console.log('WebSocket 连接已建立!', event);
messagesDiv.innerHTML += '<p style="color: green;">连接成功!</p>';
};
ws.onmessage = (event) => {
console.log('收到消息:', event.data);
messagesDiv.innerHTML += `<p><strong>服务器:</strong> ${event.data}</p>`;
};
ws.onerror = (event) => {
console.error('WebSocket 错误:', event);
messagesDiv.innerHTML += '<p style="color: red;">连接错误!</p>';
};
ws.onclose = (event) => {
console.log('WebSocket 连接已关闭!', event.code, event.reason);
messagesDiv.innerHTML += `<p style="color: gray;">连接已关闭 (${event.code}): ${event.reason}</p>`;
// 在实际应用中,这里会触发重连机制
};
sendButton.addEventListener('click', () => {
const message = messageInput.value;
if (message && ws.readyState === WebSocket.OPEN) {
ws.send(message);
messagesDiv.innerHTML += `<p><strong>我:</strong> ${message}</p>`;
messageInput.value = '';
} else if (ws.readyState !== WebSocket.OPEN) {
messagesDiv.innerHTML += '<p style="color: orange;">连接未打开,无法发送消息。</p>';
}
});
</script>
</body>
</html>
WebSocket 的重连机制
WebSocket 连接可能因为多种原因中断,例如网络不稳定、服务器重启、客户端断网等。为了提供更好的用户体验,前端需要实现自动重连机制。
重连策略:
- 监听
onclose
事件: 当连接关闭时,onclose
事件会触发,这是启动重连的信号。 - 延迟重连: 立即重连可能会导致服务器压力过大,或在网络不稳定时陷入无限重连循环。通常采用指数退避 (Exponential Backoff) 策略,即每次重连失败后,等待时间逐渐增加。
- 最大重连次数/最大延迟: 设置一个重连次数上限或最大延迟时间,避免无休止的重连。
- 清除旧连接: 在尝试新连接之前,确保清理掉任何旧的、可能处于挂起状态的连接实例。
- 连接状态管理: 维护一个连接状态变量(例如
connecting
,connected
,disconnected
),以避免重复重连或在连接未建立时发送消息。
重连机制示例:
js
class WebSocketClient {
constructor(url, options = {}) {
this.url = url;
this.options = {
reconnectInterval: options.reconnectInterval || 1000, // 初始重连间隔
maxReconnectInterval: options.maxReconnectInterval || 30000, // 最大重连间隔
reconnectAttempts: options.reconnectAttempts || Infinity, // 最大重连尝试次数
debug: options.debug || false,
// ... 其他选项
};
this.ws = null;
this.reconnectTimer = null;
this.currentReconnectInterval = this.options.reconnectInterval;
this.reconnectCount = 0;
this.isConnected = false;
this.shouldReconnect = true; // 标志是否应该尝试重连
this.onOpenCallback = options.onOpen || (() => {});
this.onMessageCallback = options.onMessage || (() => {});
this.onErrorCallback = options.onError || (() => {});
this.onCloseCallback = options.onClose || (() => {});
this.connect();
}
log(...args) {
if (this.options.debug) {
console.log('[WebSocketClient]', ...args);
}
}
connect() {
this.log('尝试连接...');
this.ws = new WebSocket(this.url);
this.ws.onopen = (event) => {
this.log('连接已建立!');
this.isConnected = true;
this.reconnectCount = 0; // 重置重连计数
this.currentReconnectInterval = this.options.reconnectInterval; // 重置重连间隔
clearTimeout(this.reconnectTimer); // 清除任何待处理的重连定时器
this.onOpenCallback(event);
};
this.ws.onmessage = (event) => {
this.onMessageCallback(event);
};
this.ws.onerror = (event) => {
this.log('连接错误!', event);
this.onErrorCallback(event);
// 错误发生时,通常也会触发 onclose
};
this.ws.onclose = (event) => {
this.log('连接已关闭!', event.code, event.reason);
this.isConnected = false;
this.onCloseCallback(event);
if (this.shouldReconnect && event.code !== 1000) { // 1000 是正常关闭
this.reconnect();
}
};
}
reconnect() {
if (this.reconnectCount >= this.options.reconnectAttempts) {
this.log('达到最大重连尝试次数,停止重连。');
this.shouldReconnect = false; // 停止重连
return;
}
this.reconnectCount++;
this.currentReconnectInterval = Math.min(
this.currentReconnectInterval * 2, // 指数退避
this.options.maxReconnectInterval
);
this.log(`将在 ${this.currentReconnectInterval / 1000} 秒后尝试重连 (第 ${this.reconnectCount} 次)...`);
this.reconnectTimer = setTimeout(() => {
this.connect();
}, this.currentReconnectInterval);
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data);
} else {
this.log('连接未打开,无法发送消息。');
// 可以在这里将消息排队,待连接恢复后发送
}
}
close() {
this.shouldReconnect = false; // 用户主动关闭,不重连
clearTimeout(this.reconnectTimer);
if (this.ws) {
this.ws.close(1000, 'Client initiated close');
}
}
}
// 如何使用
const client = new WebSocketClient('ws://localhost:8080/ws', {
reconnectInterval: 500,
maxReconnectInterval: 10000,
reconnectAttempts: 10,
debug: true,
onOpen: () => {
console.log('自定义:连接已打开!');
client.send('Hello from client after reconnect!');
},
onMessage: (event) => {
console.log('自定义:收到消息:', event.data);
},
onError: (event) => {
console.error('自定义:发生错误!');
},
onClose: (event) => {
console.log('自定义:连接已关闭!');
}
});
// 模拟发送消息
setTimeout(() => {
client.send('Test message 1');
}, 2000);
// 模拟用户主动关闭连接
// setTimeout(() => {
// client.close();
// }, 10000);
WebSocket 的心跳检测(Ping/Pong)
即使 WebSocket 连接看起来是打开的,但由于网络代理、防火墙或服务器端超时设置,连接可能在不发送数据的情况下"死掉"而不会触发 onclose
事件。心跳检测就是用来解决这个问题,确保连接的活跃性。
心跳检测策略:
- 客户端发送 Ping: 客户端定时向服务器发送一个"心跳"消息(通常是简单的字符串如 "ping" 或特定的 JSON 结构)。
- 服务器响应 Pong: 服务器收到 Ping 后,应立即回复一个"Pong"消息。
- 超时检测: 客户端在发送 Ping 后启动一个定时器。如果在一定时间内没有收到 Pong 响应,就认为连接已死,主动关闭连接并触发重连机制。
- 清除定时器: 在连接建立 (
onopen
) 时启动心跳,在连接关闭 (onclose
) 或错误 (onerror
) 时清除心跳定时器。
心跳检测示例(集成到 WebSocketClient
类中):
js
class WebSocketClientWithHeartbeat {
constructor(url, options = {}) {
this.url = url;
this.options = {
reconnectInterval: options.reconnectInterval || 1000,
maxReconnectInterval: options.maxReconnectInterval || 30000,
reconnectAttempts: options.reconnectAttempts || Infinity,
pingInterval: options.pingInterval || 30000, // 客户端发送 ping 的间隔 (30秒)
pongTimeout: options.pongTimeout || 10000, // 接收 pong 的超时时间 (10秒)
debug: options.debug || false,
};
this.ws = null;
this.reconnectTimer = null;
this.currentReconnectInterval = this.options.reconnectInterval;
this.reconnectCount = 0;
this.isConnected = false;
this.shouldReconnect = true;
this.pingTimer = null; // 发送 ping 的定时器
this.pongTimeoutTimer = null; // 等待 pong 的超时定时器
this.onOpenCallback = options.onOpen || (() => {});
this.onMessageCallback = options.onMessage || (() => {});
this.onErrorCallback = options.onError || (() => {});
this.onCloseCallback = options.onClose || (() => {});
this.connect();
}
log(...args) {
if (this.options.debug) {
console.log('[WebSocketClientWithHeartbeat]', ...args);
}
}
connect() {
this.log('尝试连接...');
this.ws = new WebSocket(this.url);
this.ws.onopen = (event) => {
this.log('连接已建立!');
this.isConnected = true;
this.reconnectCount = 0;
this.currentReconnectInterval = this.options.reconnectInterval;
clearTimeout(this.reconnectTimer);
this.startHeartbeat(); // 连接成功后启动心跳
this.onOpenCallback(event);
};
this.ws.onmessage = (event) => {
// 如果服务器回复了 pong 消息,重置 pong 超时定时器
if (event.data === 'pong') {
this.log('收到 pong 响应。');
clearTimeout(this.pongTimeoutTimer);
this.pongTimeoutTimer = null; // 清除超时定时器
this.startPongTimeout(); // 重新启动 pong 超时检测
} else {
this.onMessageCallback(event);
}
};
this.ws.onerror = (event) => {
this.log('连接错误!', event);
this.onErrorCallback(event);
// 错误发生时,通常也会触发 onclose
};
this.ws.onclose = (event) => {
this.log('连接已关闭!', event.code, event.reason);
this.isConnected = false;
this.stopHeartbeat(); // 连接关闭时停止心跳
this.onCloseCallback(event);
if (this.shouldReconnect && event.code !== 1000) {
this.reconnect();
}
};
}
reconnect() {
if (this.reconnectCount >= this.options.reconnectAttempts) {
this.log('达到最大重连尝试次数,停止重连。');
this.shouldReconnect = false;
return;
}
this.reconnectCount++;
this.currentReconnectInterval = Math.min(
this.currentReconnectInterval * 2,
this.options.maxReconnectInterval
);
this.log(`将在 ${this.currentReconnectInterval / 1000} 秒后尝试重连 (第 ${this.reconnectCount} 次)...`);
this.reconnectTimer = setTimeout(() => {
this.connect();
}, this.currentReconnectInterval);
}
startHeartbeat() {
this.log('启动心跳检测...');
this.pingTimer = setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.log('发送 ping...');
this.ws.send('ping'); // 发送 ping 消息
this.startPongTimeout(); // 启动等待 pong 的超时定时器
}
}, this.options.pingInterval);
}
startPongTimeout() {
// 如果已经有等待 pong 的定时器,先清除
if (this.pongTimeoutTimer) {
clearTimeout(this.pongTimeoutTimer);
}
this.pongTimeoutTimer = setTimeout(() => {
this.log('未收到 pong 响应,连接可能已断开,主动关闭连接。');
if (this.ws) {
this.ws.close(1008, 'Pong timeout'); // 1008 是协议错误码,表示无法处理的消息
}
}, this.options.pongTimeout);
}
stopHeartbeat() {
this.log('停止心跳检测。');
clearInterval(this.pingTimer);
clearTimeout(this.pongTimeoutTimer);
this.pingTimer = null;
this.pongTimeoutTimer = null;
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data);
} else {
this.log('连接未打开,无法发送消息。');
}
}
close() {
this.shouldReconnect = false;
this.stopHeartbeat();
clearTimeout(this.reconnectTimer);
if (this.ws) {
this.ws.close(1000, 'Client initiated close');
}
}
}
// 如何使用
const clientWithHeartbeat = new WebSocketClientWithHeartbeat('ws://localhost:8080/ws', {
reconnectInterval: 500,
maxReconnectInterval: 10000,
reconnectAttempts: 10,
pingInterval: 5000, // 每5秒发送一次 ping
pongTimeout: 3000, // 3秒内未收到 pong 则认为断开
debug: true,
onOpen: () => {
console.log('自定义:连接已打开!');
},
onMessage: (event) => {
console.log('自定义:收到消息:', event.data);
},
onError: (event) => {
console.error('自定义:发生错误!');
},
onClose: (event) => {
console.log('自定义:连接已关闭!');
}
});
// 模拟发送消息
setTimeout(() => {
clientWithHeartbeat.send('Test message with heartbeat');
}, 2000);
服务器端心跳的配合:
请注意,心跳检测需要服务器端的配合。服务器需要:
- 接收到客户端的
ping
消息后,立即回复pong
消息。 - 服务器也可以主动向客户端发送
ping
消息,并等待客户端回复pong
,如果超时未收到,则服务器端关闭连接。
总结与最佳实践
- 封装: 将 WebSocket 的连接、重连、心跳等逻辑封装成一个独立的类或模块,方便复用和管理。
- 状态管理: 明确管理连接的各种状态(连接中、已连接、断开),避免在不正确的状态下执行操作。
- 错误处理: 仔细处理
onerror
事件,并根据错误类型决定是否重连。 - 消息队列: 在连接断开期间,可以考虑将待发送的消息存储在一个队列中,待连接恢复后再发送。
- 安全性: 始终使用
wss://
进行生产环境的 WebSocket 连接,以加密通信。服务器端也应进行来源(Origin)验证。 - 选择合适的库: 对于复杂的实时应用,考虑使用成熟的 WebSocket 库,如
Socket.IO
、SockJS
等。它们提供了更完善的重连、心跳、多路复用、房间管理等功能,并且有良好的跨浏览器兼容性。