打破 Request-Response 的枷锁,开启实时通信新纪元
在 WebSocket 出现之前,Web 世界的通信主要依赖于 HTTP 协议。HTTP 是一种典型的“请求-响应”模型,就像“寄信”一样:你发出一封信(Request),对方回一封信(Response),然后连接就断开了。
如果我们想要实现"实时聊天"或"股票行情",传统的 HTTP 显得力不从心:
WebSocket 是一种在单个 TCP 连接上进行【全双工通信】的协议。它就像是一条“专用电话线”:
一旦拨通(握手成功),双方就可以随时说话(双向传输),直到其中一方挂断。它解决了 HTTP 协议在实时通信场景下的高延迟和高开销问题。
WebSocket 协议建立在 TCP 之上,但它借用了 HTTP 来完成初始的“握手”。
客户端发送一个标准的 HTTP GET 请求,但带上了特殊的 Header:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
如果服务器同意,会返回 101 Switching Protocols 状态码,表示“好的,我们切换到 WebSocket 协议吧”。
握手完成后,HTTP 协议的任务结束。后续的数据传输使用 WebSocket 独有的二进制帧格式。相比 HTTP 繁重的 Header,WebSocket 的帧非常轻量(最小仅 2 字节)。
我们将构建一个应用:多个用户进入网页,在一个画布上画画,所有人的笔迹都会实时同步给其他人。
我们需要安装 `ws` 库:npm install ws
// server.js
const WebSocket = require('ws');
// 创建 WebSocket 服务器,监听 8080 端口
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', function connection(ws) {
console.log('新用户连接!');
// 监听客户端发来的消息
ws.on('message', function incoming(data) {
// 广播:把收到的绘画数据转发给所有其他连接的客户端
wss.clients.forEach(function each(client) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
});
});
console.log('绘图板服务器运行在 ws://localhost:8080');
// client.js
const socket = new WebSocket('ws://localhost:8080');
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 1. 连接成功
socket.onopen = () => {
console.log('已连接到服务器');
};
// 2. 收到他人绘画数据
socket.onmessage = (event) => {
// data 可能是 Blob,需要根据实际情况处理,这里假设是 JSON 字符串
// 实际中可能需要 reader.readAsText(event.data)
event.data.text().then(text => {
const { x, y } = JSON.parse(text);
drawDot(x, y, '#bc13fe'); // 画别人的点(紫色)
});
};
// 3. 自己的绘画逻辑
canvas.addEventListener('mousemove', (e) => {
if (e.buttons !== 1) return; // 只在按住鼠标左键时绘制
const x = e.offsetX;
const y = e.offsetY;
drawDot(x, y, '#00f3ff'); // 画自己的点(青色)
// 发送坐标给服务器
socket.send(JSON.stringify({ x, y }));
});
function drawDot(x, y, color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, 5, 0, Math.PI * 2);
ctx.fill();
}
new WebSocket(url): 建立连接ws.send(data): 发送数据 (String/Blob/ArrayBuffer)ws.close(code, reason): 关闭连接ws.readyState: 连接状态(0-3)ws.bufferedAmount: 未发送的字节数onopen: 连接建立时触发onmessage: 收到消息时触发onerror: 发生错误时触发onclose: 连接关闭时触发1. 心跳保活 (Heartbeat)
网络设备可能会切断长时间静默的连接。定期发送 Ping/Pong 帧或空数据包以保持连接活跃。
2. 断线重连 (Reconnection)
网络波动是常态。在 onclose 中实现指数退避算法进行自动重连。
3. 安全性 (Security)
生产环境务必使用 WSS (WebSocket Secure, 类似 HTTPS)
加密传输。验证 Origin 头部防止跨站劫持。
4. 消息压缩 (Compression)
启用 permessage-deflate 扩展可大幅减少传输数据量,特别适合 JSON 等文本数据。
5. 连接池管理 (Connection Pooling)
对于服务端,合理管理连接池,避免单个服务器承载过多连接导致性能下降。
| 特性 | WebSocket | HTTP | Server-Sent Events |
|---|---|---|---|
| 通信方式 | 全双工(双向) | 半双工(请求-响应) | 单向(服务器→客户端) |
| 协议 | ws:// 或 wss:// | http:// 或 https:// | 基于 HTTP |
| 连接特性 | 持久连接 | 短连接(HTTP/1.1可keep-alive) | 持久连接 |
| 开销 | 低(握手后仅帧头) | 高(每次完整HTTP头) | 中等 |
| 实时性 | 极高(毫秒级) | 低(需轮询) | 高 |
| 浏览器支持 | 现代浏览器全支持 | 全部支持 | IE不支持 |
| 适用场景 | 聊天、游戏、协作工具 | 普通网页请求、RESTful API | 实时推送(股票、通知) |
🔹 选择 WebSocket: 需要高频双向实时通信(在线游戏、实时协作编辑器、视频会议)
🔹 选择 SSE: 仅需服务器单向推送(股票行情、系统通知、日志监控)
🔹 选择 HTTP: 传统请求-响应模式(RESTful API、表单提交、文件上传)
🔹 选择 WebRTC: 需要点对点通信(视频通话、文件传输、屏幕共享)
攻击者诱导用户访问恶意网站,该网站建立 WebSocket 连接到合法服务器,利用用户的已登录状态执行恶意操作。
防御措施:
// 服务端验证 Origin
wss.on('connection', (ws, req) => {
const origin = req.headers.origin;
const allowedOrigins = ['https://yoursite.com'];
if (!allowedOrigins.includes(origin)) {
ws.close();
return;
}
});
在生产环境中必须使用 WSS (WebSocket Secure),就像 HTTPS 一样,通过 TLS/SSL 加密所有传输数据。
配置示例:
const https = require('https');
const fs = require('fs');
const server = https.createServer({
cert: fs.readFileSync('cert.pem'),
key: fs.readFileSync('key.pem')
});
const wss = new WebSocket.Server({ server });
原因: 网络代理、负载均衡器或防火墙可能会切断长时间无数据传输的连接。
解决方案: 实现心跳机制
// 客户端心跳
let heartbeatInterval;
socket.onopen = () => {
heartbeatInterval = setInterval(() => {
socket.send(JSON.stringify({ type: 'ping' }));
}, 30000); // 每30秒发送一次
};
socket.onclose = () => {
clearInterval(heartbeatInterval);
};
问题: 网络波动导致连接意外断开,需要自动重连。
解决方案: 指数退避算法
class ReconnectingWebSocket {
constructor(url) {
this.url = url;
this.reconnectDelay = 1000; // 初始1秒
this.maxReconnectDelay = 30000; // 最大30秒
this.reconnectAttempts = 0;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('连接成功');
this.reconnectAttempts = 0;
this.reconnectDelay = 1000;
};
this.ws.onclose = () => {
console.log('连接断开,准备重连...');
this.reconnect();
};
}
reconnect() {
this.reconnectAttempts++;
const delay = Math.min(
this.reconnectDelay * Math.pow(2, this.reconnectAttempts),
this.maxReconnectDelay
);
console.log(`将在 ${delay}ms 后重连`);
setTimeout(() => this.connect(), delay);
}
}
问题: WebSocket 不保证消息送达(不像 TCP 的可靠性在应用层体现)。
解决方案: 实现消息确认机制(ACK)
// 消息 ID + 确认机制
let messageId = 0;
const pendingMessages = new Map();
function sendMessage(data) {
const id = ++messageId;
const message = { id, data };
socket.send(JSON.stringify(message));
// 设置超时重发
const timer = setTimeout(() => {
console.log(`消息 ${id} 未确认,重发`);
sendMessage(data);
}, 5000);
pendingMessages.set(id, timer);
}
socket.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'ack') {
clearTimeout(pendingMessages.get(msg.id));
pendingMessages.delete(msg.id);
}
};
WebSocket 是现代 Web 实时应用的基石。它并不取代 HTTP,而是互补。
Socket.IO:在原生 WebSocket 基础上提供了更强大的功能
其他相关技术
🎉 恭喜你完成学习!
现在你已经掌握了 WebSocket 的核心概念、工作原理、安全最佳实践和常见问题解决方案。接下来,试着在实际项目中应用这些知识,构建属于你自己的实时应用吧!