WebSocket实战:从踩坑到稳定实时通信

开篇:一张WebSocket连接状态流转图,展示从握手到关闭的完整生命周期。

先看基础:一个能用的WebSocket长什么样

很多人写WebSocket就喜欢搞个new WebSocket(url)就跑,然后美滋滋地以为万事大吉。但实际生产环境要考虑的东西多了去了。

为什么要这样写:下面的代码是我在项目里打磨过的初始化方案,包含了错误处理和状态管理。

javascript
class WebSocketClient {
constructor(url, options = {}) {
this.url = url;
this.options = {
reconnectInterval: 3000, // 重连间隔
maxReconnectAttempts: 10, // 最大重连次数
heartbeatInterval: 30000, // 心跳间隔
...options
};

this.ws = null;
this.reconnectCount = 0;
this.heartbeatTimer = null;
this.isManualClose = false; // 标记是否手动关闭

this.connect();
}

connect() {
try {
this.ws = new WebSocket(this.url);

this.ws.onopen = () => {
console.log(
[WebSocket] 连接成功);
this.reconnectCount = 0;
this.startHeartbeat();
};

this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleMessage(data);
} catch (e) {
console.error('[WebSocket] 消息解析失败:', e);
}
};

this.ws.onclose = (event) => {
console.log(
[WebSocket] 连接关闭, code: ${event.code});
this.stopHeartbeat();

if (!this.isManualClose) {
this.reconnect();
}
};

this.ws.onerror = (error) =>

<

p> {
console.error('[WebSocket] 连接错误:', error);
// onerror之后会触发onclose,所以这里只做日志
};

} catch (error) {
console.error('[WebSocket] 创建失败:', error);
this.reconnect();
}
}
}
`

这个设计真的反人类的地方在于:断开连接的code码多得要命,而且不同浏览器还不一样。比如1000是正常关闭,1006是异常断开,但你永远不知道用户那边会弹出什么鬼code。

另一个坑:断线重连的无限循环

有个用户骂我说:“你们的聊天功能让我手机发烫!”排查后发现是断线重连写了bug,在断网情况下疯狂重连,直接把CPU拉满。

为什么要这样写:重连必须加指数退避,不然就是自杀式攻击。

`javascript
class WebSocketClient {
// ... 接上面的代码

reconnect() {
if (this.reconnectCount >= this.options.maxReconnectAttempts) {
console.error('[WebSocket] 重连次数已达上限');
this.onMaxReconnectFailed && this.onMaxReconnectFailed();
return;
}

// 指数退避:每次重连间隔翻倍,但不超过30秒
const delay = Math.min(
this.options.reconnectInterval * Math.pow(2, this.reconnectCount),
30000
);

console.log([WebSocket] 第${this.reconnectCount + 1}次重连, 等待${delay}ms);

this.reconnectCount++;

// 用setTimeout避免阻塞
setTimeout(() => {
if (document.visibilityState !== 'hidden') { // 页面可见时才重连
this.connect();
} else {
// 页面不可见时,监听visibilitychange
const handler = () => {
if (document.visibilityState === 'visible') {
document.removeEventListener('visibilitychange', handler);
this.connect();
}
};
document.addEventListener('visibilitychange', handler);
}
}, delay);
}

// 心跳检测:防止假连接
sta

rtHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
// 发送ping消息
this.ws.send(JSON.stringify({ type: 'ping' }));
} else {
this.stopHeartbeat();
this.reconnect();
}
}, this.options.heartbeatInterval);
}

stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}

// 手动关闭,不要触发重连
close() {
this.isManualClose = true;
this.stopHeartbeat();
if (this.ws) {
this.ws.close(1000, '手动关闭');
}
}
}
`

消息处理:按类型分派才优雅

很多人收到消息就一个if-else写到天荒地老。我见过一个项目,单个onmessage函数里嵌套了7层switch,看得我头皮发麻。

为什么要这样写:用事件系统解耦,消息来了按类型分派,维护起来爽到飞起。

`javascript
class WebSocketClient {
constructor() {
this.handlers = new Map();
this.messageQueue = []; // 等待队列
}

// 注册消息处理器
on(type, handler) {
if (!this.handlers.has(type)) {
this.handlers.set(type, []);
}
this.handlers.get(type).push(handler);
}

// 移除消息处理器
off(type, handler) {
const handlers = this.handlers.get(type);
if (handlers) {
this.handlers.set(type, handlers.filter(h => h !== handler));
}
}

handleMessage(data) {
const { type, payload } = data;

// 处理ping/pong
if (type === 'pong') return;

const handlers = this.handlers.get(type);
if (handlers) {
handlers.forEach(handler => {
try {
handler(payload);
} catch (e) {
console.error(
[WebSocket] 处理器执行失败, type: ${type}, e);
}
});
} else {
console.warn(
[WebSocket] 未注册的处理器类型: ${type});
}
}
}

// 使用示例
const ws = new WebSocketClient('wss://api.example.com/ws');

ws.on('chat.message', (msg) => {
updateChatUI(msg);
});

ws.on('system.notification', (notification) => {
showNotification(notification.title, notification.content);
});
`

性能优化:从3.2秒降到0.8秒的骚操作

我曾经负责过一个弹幕系统,刚开始用WebSocket直接推原始数据,1000人同时在线就卡成PPT。后来优化了三个地方:

1. 二进制传输代替JSON

WebSocket支持ArrayBuffer,可以把数据用二进制格式传输,体积直接减半。

`javascript
// 发送端:用MessagePack序列化
import { encode } from '@msgpack/msgpack';

class WebSocketClient {
send(type, payload) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
const message = encode({ type, payload, timestamp: Date.now() });
this.ws.send(message);
} else {
// 连接不可用时,加入队列
this.messageQueue.push({ type, payload });
}
}
}
`

2. 消息去重和节流

相同类型的消息在短时间内合并,别让UI频繁刷新。

`javascript
class MessageThrottler {
constructor(delay = 100) {
this.cache = new Map();
this.delay = delay;
}

add(key, data) {
const existing = this.cache.get(key);
if (existing) {
clearTimeout(existing.timer);
Object.assign(existing.data, data); // 合并数据
} else {
this.cache.set(key, { data, timer: null });
}

this.cache.get(key).timer = setTimeout(() => {
this.flush(key);
}, this.delay);
}

flush(key) {
const item = this.cache.get(key);
if (item) {
// 触发实际的UI更新
this.onFlush && this.onFlush(key, item.data);
this.cache.delete(key);
}
}
}
`

3. 连接池复用

多页面场景下,用SharedWorker或者ServiceWorker共享一个WebSocket连接,避免每个tab都开一条连接。

核心:一张性能对比图,展示优化前后消息延迟和CPU占用率的差异。

生产环境必须做的监控

没有监控的WebSocket就是裸奔。我见过最离谱的事故:某个用户的WebSocket连接因为网络问题变成了半开状态(服务端以为还连着,客户端已经断了),结果服务端一直给这个连接推消息,内存直接爆了。

为什么要这样写:监控就是给系统装个心电图,一眼就能看出哪里有问题。

`javascript
class WebSocketMonitor {
constructor() {
this.metrics = {
connectCount: 0,
disconnectCount: 0,
reconnectCount: 0,
totalMessages: 0,
errorCount: 0,
latency: []
};

// 定时上报
setInterval(() => {
this.report();
}, 60000); // 每分钟上报一次
}

recordLatency() {
const start = performance.now();
return () => {
const duration = performance.now() - start;
this.metrics.latency.push(duration);

// 只保留最近100条
if (this.metrics.latency.length > 100) {
this.metrics.latency.shift();
}
};
}

report() {
const avgLatency = this.metrics.latency.length > 0
? this.metrics.latency.reduce((a, b) => a + b, 0) / this.metrics.latency.length
: 0;

console.log('[WebSocket Monitor]', {
...this.metrics,
avgLatency: avgLatency.toFixed(2) + 'ms',
memoryUsage: performance.memory?.usedJSHeapSize
? (performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2) + 'MB'
: 'N/A'
});

// 告警阈值
if (this.metrics.errorCount > 10) {
alertDevops('WebSocket错误率过高');
}

if (avgLatency > 500) {
alertDevops('WebSocket延迟过高');
}

// 重置计数
this.metrics.errorCount = 0;
}
}
`

还有个技巧:优雅处理页面切换

用户切后台的时候,WebSocket还在疯狂推消息,这不仅是浪费带宽,还可能因为移动端网络限制导致连接被强杀。

`javascript
// 监听页面可见性变化
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
// 页面隐藏时,降低心跳频率
ws.setHeartbeatInterval(60000); // 从30秒变成60秒
} else {
// 页面恢复时,立即检查连接状态
ws.setHeartbeatInterval(30000);
ws.checkConnection();
}
});

官方文档这段文档不够清晰:关于WebSocket的状态码,文档说“1006是异常关闭,但具体原因不明”。我靠,那到底该怎么处理?后来在源码里才发现,1006其实就是客户端主动关闭触发的,但让开发者去挖源码,这不扯淡么?

总结前:一张架构图,展示完整的WebSocket通信链路:客户端 -> 负载均衡 -> WebSocket集群 -> 消息队列 -> 业务服务。

总结一下,你可以立刻用的三个点

  • 重连必须用指数退避:从3秒开始,每次翻倍,上限30秒。别直接setInterval无脑重连,那是自杀。
  • 消息用二进制传输:同样数据量,二进制比JSON体积小40%以上,1000人同时在线的场景,带宽直接省一半。
  • 一定要加监控:至少记录连接数、延迟、错误率。设定阈值告警,别等用户骂上门才发现问题。
  • 最后分享一个血泪经验:WebSocket不是银弹。如果只是简单的消息推送,Server-Sent Events可能更合适;如果是双向实时通信,那WebSocket绝对是王者。选对工具,比什么都重要。

    滚动至顶部