WebSocket 基础:什么时候该用它,怎么配

聊天、实时通知、协同编辑——这些场景用 HTTP 轮询会很浪费。WebSocket 提供真正的双向长连接,但也不是万能的。

一、先比较方案

方案特点适用
短轮询定时发请求简单但不实时,浪费请求
长轮询请求挂起直到有数据兼容性好,仍是一问一答
SSE服务端单向推送,基于 HTTP只需服务端→客户端时更简单
WebSocket全双工长连接需要双向实时通信

如果只是服务端推给客户端(如通知、进度),优先用 SSE。它基于普通 HTTP,不用改 Nginx 配置,还自带断线重连。

二、握手过程

WebSocket 先用 HTTP 握手,再升级协议:

bash
# 客户端请求
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://

js
const ws = new WebSocket("wss://www.bzii.cn/ws");

HTTPS 页面里必须wss://,浏览器会阻止 ws:// 的混合内容。

三、浏览器端用法

js
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 头,导致握手失败。必须显式传递:

nginx
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 动态处理:

nginx
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 超时而断开。必须自己做重连

js
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 示例)

js
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);

七、排错

现象原因
握手返回 400Nginx 没传 Upgrade 头
连接立刻断开proxy_read_timeout 太短
HTTPS 下报 mixed content用了 ws:// 而非 wss://
连接成功但收不到消息服务端广播逻辑或帧格式问题
Cloudflare 后连不上确认已开启 WebSocket 支持(免费版默认开)
bash
# 测试握手
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 就不用。