WebSocket 基础:什么时候该用它,怎么配
聊天、实时通知、协同编辑——这些场景用 HTTP 轮询会很浪费。WebSocket 提供真正的双向长连接,但也不是万能的。
一、先比较方案
| 方案 | 特点 | 适用 |
|---|---|---|
| 短轮询 | 定时发请求 | 简单但不实时,浪费请求 |
| 长轮询 | 请求挂起直到有数据 | 兼容性好,仍是一问一答 |
| SSE | 服务端单向推送,基于 HTTP | 只需服务端→客户端时更简单 |
| WebSocket | 全双工长连接 | 需要双向实时通信 |
如果只是服务端推给客户端(如通知、进度),优先用 SSE。它基于普通 HTTP,不用改 Nginx 配置,还自带断线重连。
二、握手过程
WebSocket 先用 HTTP 握手,再升级协议:
# 客户端请求
GET /ws HTTP/1.1
Host: www.bzii.cn
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
# 服务端响应
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=之后就不再是 HTTP 了,变成 WebSocket 帧协议。
URL 用 ws:// 或加密的 wss://:
const ws = new WebSocket("wss://www.bzii.cn/ws");HTTPS 页面里必须用
wss://,浏览器会阻止ws://的混合内容。
三、浏览器端用法
const ws = new WebSocket("wss://www.bzii.cn/ws");
ws.onopen = () => {
console.log("已连接");
ws.send(JSON.stringify({ type: "hello" }));
};
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
console.log("收到", data);
};
ws.onerror = (err) => console.error("出错", err);
ws.onclose = (e) => {
console.log("断开", e.code, e.reason);
// 这里要做重连
};四、Nginx 反代的关键配置
默认 Nginx 会「吃掉」Upgrade 头,导致握手失败。必须显式传递:
location /ws {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
# 这两行是关键
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# WebSocket 是长连接,读超时要设长一些
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}更稳妥的写法
Connection "upgrade" 是硬编码的,某些情况下会有问题。用 map 动态处理:
map $http_upgrade $connection_upgrade {
default upgrade;
"" close;
}
server {
location /ws {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
}
}
map必须写在http块里(不能放 server 块)。这段配置几乎是所有 WebSocket 反代的标准答案。
五、心跳与重连
连接会因为网络波动、服务端重启、Nginx 超时而断开。必须自己做重连:
let ws = null;
let retry = 0;
const MAX_RETRY = 10;
function connect() {
ws = new WebSocket("wss://www.bzii.cn/ws");
ws.onopen = () => { retry = 0; startHeartbeat(); };
ws.onclose = () => {
if (retry >= MAX_RETRY) return;
const delay = Math.min(1000 * Math.pow(2, retry), 30000); // 指数退避
retry++;
setTimeout(connect, delay);
};
}
// 心跳:定时发 ping,检测连接是否还活着
let heartbeatTimer = null;
function startHeartbeat() {
clearInterval(heartbeatTimer);
heartbeatTimer = setInterval(() => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "ping" }));
}
}, 30000);
}
connect();要点:
- 指数退避:别一秒重连十次,会把服务端打垮;
- 心跳:双向的,服务端也要发,否则 NAT/代理会静默断开空闲连接;
- 页面从后台切回时也该检查连接状态。
六、服务端(Node 示例)
const WebSocket = require("ws");
const wss = new WebSocket.Server({ port: 3000 });
wss.on("connection", (ws) => {
console.log("新连接");
ws.on("message", (msg) => {
const data = JSON.parse(msg);
if (data.type === "ping") return ws.send(JSON.stringify({ type: "pong" }));
// 广播给所有人
wss.clients.forEach(c => {
if (c.readyState === WebSocket.OPEN) c.send(msg.toString());
});
});
ws.on("close", () => console.log("断开"));
ws.isAlive = true;
ws.on("pong", () => { ws.isAlive = true; });
});
// 服务端也要做心跳检测
setInterval(() => {
wss.clients.forEach(ws => {
if (!ws.isAlive) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);七、排错
| 现象 | 原因 |
|---|---|
| 握手返回 400 | Nginx 没传 Upgrade 头 |
| 连接立刻断开 | proxy_read_timeout 太短 |
| HTTPS 下报 mixed content | 用了 ws:// 而非 wss:// |
| 连接成功但收不到消息 | 服务端广播逻辑或帧格式问题 |
| Cloudflare 后连不上 | 确认已开启 WebSocket 支持(免费版默认开) |
# 测试握手
curl -i -N \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
-H "Sec-WebSocket-Version: 13" \
https://www.bzii.cn/ws
# 期望返回 101 Switching Protocols八、什么时候不该用
我的判断:
| 场景 | 建议 |
|---|---|
| 几分钟更新一次的数据 | 轮询就够了 |
| 只需服务端推送 | SSE 更简单 |
| 需要穿越严格的企业代理 | WebSocket 可能被拦,用长轮询 |
| 服务端无状态、要水平扩容 | 需要额外的消息广播层(Redis Pub/Sub) |
WebSocket 的隐藏成本:
- 有状态连接,扩容时要考虑会话亲和性或用 Redis 做消息广播;
- 长时间连接占用服务器资源;
- 监控和排查比 HTTP 麻烦(看不到请求日志)。
个人小项目想加实时功能,先试试 SSE 或 2 秒一次轮询。能不用 WebSocket 就不用。
