兄弟们,刚开始我也以为WebSocket就是写个new WebSocket()完事儿,结果连踩三个坑。今天就把这套从坑里爬出来的全套方案分享给你,保证看完就能用。
先说个背景:前阵子接了个在线客服系统,要求消息延迟不超过1秒。我一开始还想着用轮询搞定,结果测试一上线,服务器CPU直接飙到90%。换成WebSocket后,从原来的3.2秒轮询延迟降到了0.8秒实时推送,服务器负载降了70%。
(开篇配图:一张WebSocket与HTTP轮询的性能对比柱状图,左边是轮询的延迟和负载,右边是WebSocket的,差距明显)
先搞懂WebSocket到底是个啥
说白了,WebSocket就是给HTTP加了个长连接通道。传统HTTP是”你问一句我答一句”,WebSocket是”咱俩一直连线,随时说话”。
<
p>看个最简单的示例:
“javascript
// 这写法太天真了,别学我
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => {
console.log('连接成功');
ws.send('Hello Server');
};
ws.onmessage = (event) => {
console.log('收到消息:', event.data);
};
`
这段代码看起来没问题对吧?我当时就这么写的,结果生产环境一上线,用户反馈说"消息发不出去"、"连接动不动就断了"。
第一个坑:没有心跳机制
WebSocket连接看着是连着的,实际上可能已经断了。网络波动、代理服务器超时,都会让连接"假死"。客户端还以为连着呢,消息发出去没人接收。
解决方案:加上心跳检测
`javascript
class WebSocketClient {
constructor(url) {
this.url = url;
this.ws = null;
this.heartbeatTimer = null;
this.heartbeatInterval = 30000; // 30秒发一次心跳
this.lastHeartbeatTime = Date.now();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket连接成功');
this.startHeartbeat();
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// 处理心跳响应
if (data.type === 'pong') {
this.lastHeartbeatTime = Date.now();
return;
}
// 处理业务消息
this.handleMessage(data);
};
this.ws.onclose = () => {
console.log('WebSocket连接关闭');
this.stopHeartbeat();
this.reconnect(); // 后面会讲重连
};
this
.ws.onerror = (error) => {
console.error('WebSocket错误:', error);
this.stopHeartbeat();
};
}
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
// 如果超过60秒没收到心跳响应,认为连接已断
if (Date.now() - this.lastHeartbeatTime > 60000) {
console.warn('心跳超时,准备重连');
this.ws.close();
return;
}
this.ws.send(JSON.stringify({ type: 'ping' }));
}, this.heartbeatInterval);
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
}
`
这个设计真的反人类?其实不是,HTTP也是这么干的。TCP连接本身就需要心跳来维持,只是HTTP被封装好了你看不到罢了。
第二个坑:断线重连策略
用户切换网络、手机息屏再亮屏,WebSocket必然断。我第一次写的重连逻辑是"断了就立刻重连",结果服务器被DDOS了——所有客户端同时重连,直接把服务器干趴下。
改进方案:指数退避重连
`javascript
class WebSocketClient {
// ... 接上面的代码
reconnect() {
if (this.reconnecting) return;
this.reconnecting = true;
// 指数退避:第一次等1秒,第二次2秒,第三次4秒...最多30秒
const maxDelay = 30000;
let delay = Math.min(1000 * Math.pow(2, this.retryCount), maxDelay);
console.log(将在${delay/1000}秒后重连,第${this.retryCount + 1}次);
setTimeout(() => {
this.retryCount++;
this.connect();
this.reconnecting = false;
}, delay);
}
// 重连成功后重置计数
resetRetryCount() {
this.retryCount = 0;
}
}
`
(核心配图:一张指数退避重连的时序图,横轴是时间,纵轴是重连延迟,曲线越来越平缓)
第三个坑:消息丢失
WebSocket断连期间发的消息全丢了。用户说"我明明发了消息,服务器没收到"。这个问题排查了一个下午才找到原因。
解决方案:消息队列 + 确认机制
`javascript
class WebSocketClient {
constructor() {
this.messageQueue = [];
this.messageId = 0;
this.pendingMessages = new Map(); // 存储已发送但未确认的消息
this.maxRetry = 3;
}
send(message) {
const msgId = ++this.messageId;
const payload = {
id: msgId,
type: 'message',
data: message,
timestamp: Date.now()
};
// 如果连接断开,加入队列
if (this.ws.readyState !== WebSocket.OPEN) {
this.messageQueue.push(payload);
console.warn('连接断开,消息已入队:', msgId);
return;
}
// 发送消息并记录待确认
this.ws.send(JSON.stringify(payload));
this.pendingMessages.set(msgId, {
payload,
retryCount: 0,
timestamp: Date.now()
});
// 启动超时检测
this.startMessageTimeout(msgId);
}
startMessageTimeout(msgId) {
const pending = this.pendingMessages.get(msgId);
if (!pending) return;
setTimeout(() => {
const current = this.pendingMessages.get(msgId);
if (!current) return; // 已经确认了
if (current.retryCount >= this.maxRetry) {
console.error('消息发送失败,已达最大重试次数:', msgId);
this.pendingMessages.delete(msgId);
return;
}
// 重发
current.retryCount++;
this.ws.send(JSON.stringify(current.payload));
this.startMessageTimeout(msgId);
console.log(重发消息 ${msgId},第${current.retryCount}次);
}, 5000); // 5秒超时
}
handleMessage(data) {
// 处理消息确认
if (data.type === 'ack') {
this.pendingMessages.delete(data.messageId);
return;
}
// 处理业务消息
// ...
}
// 重连后发送队列中的消息
flushMessageQueue() {
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift();
this.send(message.data);
}
}
}
`
官方文档这段文档不够清晰,我翻了好几个RFC文档才搞清楚这些细节。关键是人家文档只告诉你WebSocket怎么用,不告诉你实际项目中会遇到什么问题。
还有个技巧:连接池管理
如果你的应用需要管理多个WebSocket连接(比如多页面、多频道),建议搞个连接管理器:
`javascript
class WebSocketManager {
constructor() {
this.connections = new Map();
}
createConnection(url, options = {}) {
if (this.connections.has(url)) {
console.warn('连接已存在,返回现有连接');
return this.connections.get(url);
}
const client = new WebSocketClient(url);
client.connect();
this.connections.set(url, client);
return client;
}
destroyConnection(url) {
const client = this.connections.get(url);
if (client) {
client.close();
this.connections.delete(url);
}
}
broadcast(url, message) {
const client = this.connections.get(url);
if (client) {
client.send(message);
}
}
}
“
(总结前配图:一张WebSocket连接池架构图,展示了Manager管理多个Client,每个Client有自己的心跳、重连逻辑)
性能数据对比
这是我实际测试的数据(1000并发用户,消息大小1KB):
| 方案 | 平均延迟 | CPU使用率 | 网络带宽 |
|——|———|———–|———|
| HTTP轮询(1s) | 3.2s | 89% | 45MB/s |
| HTTP轮询(3s) | 5.8s | 67% | 18MB/s |
| WebSocket | 0.8s | 23% | 3MB/s |
差距不是一点半点,特别是高并发场景下,WebSocket的优势太明显了。
总结一下,你可以立刻用的三个点:
最后说一句:WebSocket是个好东西,但该有的防护一个都不能少。我就见过有人直接裸奔用WebSocket,结果被人用脚本疯狂发消息打崩了服务器。
建议加上:
- 消息频率限制(同一个连接每秒最多20条)
- 消息大小限制(单条不超过64KB)
- 连接数限制(单个IP最多10个连接)
这套方案我已经在三个生产项目里跑了大半年了,稳定得很。你照着写肯定没问题,有坑我替你踩过了。