搞实时通信,WebSocket 算是标配。但直接裸用原生 API 会有不少暗坑:连接断了不会自动重连,长时间闲置被代理或防火墙掐掉,消息一多处理逻辑就散得没法维护。这篇文章准备从零搭一个足够健壮的 WebSocket 客户端,涵盖重连、心跳、消息分发,再加几个生产环境常用的变体。
原生 WebSocket 的尴尬
下面这种写法估计很多人都用过:
const socket = new WebSocket('ws://localhost:8080');
socket.onopen = () => socket.send('Hello');
socket.onmessage = (e) => console.log(e.data);
socket.onclose = () => console.log('closed');
socket.onerror = (e) => console.error(e);
问题明显:
- 断开后悄无声息,没有任何重连尝试。
- 没有心跳,中间链路可能会悄悄回收连接。
onmessage里的if/else会随着消息类型增加迅速膨胀。- 错误处理基本等于没写。
所以我们需要自己封装一层。
基础连接类
先把重连、心跳和消息处理器包装起来。核心思路:用一个 WebSocketClient 类管理生命周期,通过 on(type, handler) 注册不同类型的消息回调,内部维护重连次数和心跳定时器。
class WebSocketClient {
constructor(url) {
this.url = url;
this.socket = null;
this.connected = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
this.messageHandlers = {};
this.heartbeatInterval = null;
}
connect() {
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
console.log('connected');
this.connected = true;
this.reconnectAttempts = 0;
this.startHeartbeat();
};
this.socket.onmessage = (event) => {
this.handleMessage(event.data);
};
this.socket.onclose = () => {
console.log('disconnected');
this.connected = false;
this.stopHeartbeat();
this.reconnect();
};
this.socket.onerror = (error) => {
console.error('error:', error);
};
}
reconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`reconnecting... ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
setTimeout(() => this.connect(), this.reconnectDelay * this.reconnectAttempts);
} else {
console.error('max reconnect attempts reached');
}
}
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
if (this.connected) {
this.send({ type: 'heartbeat' });
}
}, 30000);
}
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
send(data) {
if (this.connected) {
this.socket.send(JSON.stringify(data));
} else {
console.error('not connected');
}
}
on(type, handler) {
if (!this.messageHandlers[type]) {
this.messageHandlers[type] = [];
}
this.messageHandlers[type].push(handler);
}
handleMessage(data) {
try {
const message = JSON.parse(data);
const { type, payload } = message;
if (this.messageHandlers[type]) {
this.messageHandlers[type].forEach(handler => handler(payload));
}
} catch (error) {
console.error('parse error:', error);
}
}
disconnect() {
this.stopHeartbeat();
if (this.socket) {
this.socket.close();
}
}
}
使用起来也简单:
const ws = new WebSocketClient('ws://localhost:8080');
ws.connect();
ws.on('chat', (payload) => console.log('chat:', payload));
ws.on('notification', (payload) => console.log('notif:', payload));
ws.send({ type: 'chat', payload: { text: 'hi' } });
在 React 里集成
有了上面的类,放到 React 里只需要在 effect 中初始化,并记得卸载时断开。
import React, { useEffect, useCallback, useRef, useState } from 'react';
function WebSocketComponent() {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const wsRef = useRef(null);
useEffect(() => {
wsRef.current = new WebSocketClient('ws://localhost:8080');
wsRef.current.connect();
wsRef.current.on('chat', (payload) => {
setMessages(prev => [...prev, payload]);
});
return () => wsRef.current?.disconnect();
}, []);
const handleSend = useCallback(() => {
if (input.trim() && wsRef.current) {
wsRef.current.send({ type: 'chat', payload: { text: input, user: 'me' } });
setInput('');
}
}, [input]);
return (
<div>
<div className="messages">
{messages.map((m, i) => (
<div key={i}><strong>{m.user}:</strong> {m.text}</div>
))}
</div>
<div className="input-area">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
/>
<button onClick={handleSend}>Send</button>
</div>
</div>
);
}
常见扩展
实际项目里往往还需要一些额外机制,这几个是出现频率很高的。
带认证
把 token 拼在 URL 参数里是最直接的方式。
class AuthWebSocketClient extends WebSocketClient {
constructor(url, token) {
super(url);
this.token = token;
}
connect() {
this.socket = new WebSocket(`${this.url}?token=${this.token}`);
// 其他逻辑完全复用父类
}
}
指数退避重试
固定间隔重连在服务器压力大时不太友好,退避策略可以让重连间隔逐渐拉长。
class RetryWebSocketClient extends WebSocketClient {
constructor(url, options = {}) {
super(url);
this.maxReconnectAttempts = options.maxReconnectAttempts || 10;
this.reconnectDelay = options.reconnectDelay || 1000;
this.exponentialBackoff = options.exponentialBackoff ?? true;
}
reconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = this.exponentialBackoff
? this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1)
: this.reconnectDelay;
console.log(`reconnecting in ${delay}ms`);
setTimeout(() => this.connect(), delay);
}
}
}
消息队列
连接还没建立时,可以把要发的消息先存起来,连上后再一次性刷出去,避免丢消息。
class QueueWebSocketClient extends WebSocketClient {
constructor(url) {
super(url);
this.messageQueue = [];
}
connect() {
super.connect();
this.socket.onopen = () => {
console.log('connected');
this.connected = true;
this.reconnectAttempts = 0;
this.startHeartbeat();
this.flushQueue();
};
}
send(data) {
if (this.connected) {
this.socket.send(JSON.stringify(data));
} else {
this.messageQueue.push(data);
}
}
flushQueue() {
if (this.connected && this.messageQueue.length > 0) {
console.log(`flushing ${this.messageQueue.length} messages`);
this.messageQueue.forEach(m => this.socket.send(JSON.stringify(m)));
this.messageQueue = [];
}
}
}
服务端的一个简单例子
前端写得再稳,服务端也得配合。下面是一个基于 Node.js ws 库的服务,处理心跳并广播消息。
const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });
const clients = new Set();
server.on('connection', (socket) => {
console.log('client connected');
clients.add(socket);
socket.send(JSON.stringify({ type: 'system', payload: 'welcome' }));
socket.on('message', (data) => {
try {
const message = JSON.parse(data);
if (message.type === 'heartbeat') {
socket.send(JSON.stringify({ type: 'heartbeat' }));
return;
}
clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
});
} catch (e) {
console.error('invalid message', e);
}
});
socket.on('close', () => {
clients.delete(socket);
console.log('client disconnected');
});
socket.on('error', (err) => console.error('socket error', err));
});
前端多连接管理
如果一个页面需要连多个 WebSocket 端点,手动维护很烦,可以用一个单例管理器来统一创建和回收。
class WebSocketManager {
static instance = null;
constructor() {
this.clients = {};
}
static getInstance() {
if (!WebSocketManager.instance) {
WebSocketManager.instance = new WebSocketManager();
}
return WebSocketManager.instance;
}
createClient(name, url, options = {}) {
const client = new QueueWebSocketClient(url, options);
this.clients[name] = client;
client.connect();
return client;
}
getClient(name) {
return this.clients[name];
}
removeClient(name) {
this.clients[name]?.disconnect();
delete this.clients[name];
}
disconnectAll() {
Object.values(this.clients).forEach(c => c.disconnect());
this.clients = {};
}
}
还得注意的事
- 错误监控:可以给连接类加个错误计数器,连续报错超过阈值就主动断开避免资源泄露,同时上报日志。
- 不要为了用而用:WebSocket 适合需要服务端主动推送、频繁小数据交换的场景。如果只是偶尔拉一次服务端状态,轮询就够了,实现简单,排障也容易。
- 生产环境走 wss,证书配置和反向代理(如 Nginx)的支持都别忘了。
- 心跳间隔可以和服务端协商,别太短加重负载,太长起不到保活作用。
这些封装并不复杂,但能避免不少线上'连接断了、消息丢了'的尴尬。直接用第三方库当然也行,理解背后的这些机制,出问题的时候你才知道该往哪查。

