WebSocket 实时通信完全指南:从协议握手到生产部署

HTTP 的请求/响应模型在大部分场景够用,但碰到"服务端要主动推消息给客户端"这种需求就显得笨拙 —— 长轮询、SSE、轮询都是 workaround。WebSocket 是为这个场景设计的标准方案:一次握手,建立一条双向、长连接的全双工通道。这篇文章从协议握手讲到生产部署,所有代码都可以直接跑。

WebSocket 协议本身

WebSocket 用 HTTP 完成握手,握手成功后协议升级为 WebSocket,后续的数据帧不再走 HTTP。一次典型的握手请求:

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com

服务器响应:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Sec-WebSocket-AcceptSec-WebSocket-Key + 固定 GUID 的 SHA-1,再 base64 编码 —— 用于证明服务端确实理解 WebSocket 协议,不是傻乎乎照搬 HTTP 响应。握手后,TCP 连接保留,但数据格式变成 WebSocket 帧:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
+-+-+-+-+-------+-+-------------+-------------------------------+
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
|                     Payload Data                              |
+---------------------------------------------------------------+

关键字段:FIN(是否分片的最后一帧)、opcode(0x1 文本、0x2 二进制、0x8 关闭、0x9 ping、0xA pong)、MASK(客户端发出的帧必须 mask,服务端发出的不需要)、Payload Length(0-125 直接读,126 读后续 2 字节,127 读 8 字节)。理解这层有助于调试 —— 抓包看 wireshark 时不会迷茫。

浏览器侧:原生 WebSocket API

const ws = new WebSocket('wss://example.com/chat');

ws.addEventListener('open', () => {
    console.log('connected');
    ws.send(JSON.stringify({ type: 'hello', user: 'mores' }));
});

ws.addEventListener('message', (event) => {
    const data = JSON.parse(event.data);
    console.log('received', data);
});

ws.addEventListener('close', (event) => {
    console.log('closed', event.code, event.reason);
});

ws.addEventListener('error', (err) => {
    console.error('ws error', err);
});

// 主动关闭
// ws.close(1000, 'normal closure');

注意:生产环境永远用 wss://(WebSocket Secure,基于 TLS),理由和 HTTPS 一样 —— 防止中间人嗅探消息内容。很多公司内网代理只允许 443 端口的 TLS,普通 ws:// 会被拦截。

服务端:Node.js + ws 库

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws, req) => {
    const ip = req.socket.remoteAddress;
    console.log(`client connected from ${ip}`);

    ws.on('message', (data) => {
        const msg = JSON.parse(data);
        // 广播给所有客户端
        wss.clients.forEach((client) => {
            if (client.readyState === WebSocket.OPEN) {
                client.send(JSON.stringify(msg));
            }
        });
    });

    ws.on('close', () => console.log(`client ${ip} disconnected`));

    ws.send(JSON.stringify({ type: 'welcome', msg: '欢迎' }));
});

这是一个最简单的"广播聊天室"。要做"分房间",在 connection 上加 ws.room 属性,广播时只发给同房间的客户端:

const rooms = new Map();  // roomId -> Set<ws>

wss.on('connection', (ws, req) => {
    const url = new URL(req.url, 'http://x');
    const room = url.searchParams.get('room') || 'default';
    ws.room = room;

    if (!rooms.has(room)) rooms.set(room, new Set());
    rooms.get(room).add(ws);

    ws.on('message', (data) => {
        rooms.get(room).forEach((c) => {
            if (c !== ws && c.readyState === WebSocket.OPEN) c.send(data);
        });
    });

    ws.on('close', () => {
        rooms.get(room).delete(ws);
        if (rooms.get(room).size === 0) rooms.delete(room);
    });
});

关键问题:心跳与断线重连

TCP 连接看上去"长"在那里,但中间任何一个 NAT、防火墙、负载均衡都可能在空闲时把连接静默踢掉。你的代码需要主动维护连接活性。

服务端心跳:检测死连接

// ws 库内置 ping/pong 机制
function heartbeat() { this.isAlive = true; }

wss.on('connection', (ws) => {
    ws.isAlive = true;
    ws.on('pong', heartbeat);
});

const interval = setInterval(() => {
    wss.clients.forEach((ws) => {
        if (!ws.isAlive) return ws.terminate();  // 上一轮没回 pong,杀掉
        ws.isAlive = false;
        ws.ping();   // 发 ping,等 pong
    });
}, 30000);

wss.on('close', () => clearInterval(interval));

客户端断线重连(带指数退避)

class ReconnectingWS {
    constructor(url) {
        this.url = url;
        this.attempts = 0;
        this.handlers = {};
        this.connect();
    }

    connect() {
        this.ws = new WebSocket(this.url);
        this.ws.onopen = () => {
            this.attempts = 0;
            (this.handlers.open || []).forEach((fn) => fn());
        };
        this.ws.onmessage = (e) => {
            (this.handlers.message || []).forEach((fn) => fn(e));
        };
        this.ws.onclose = () => {
            const delay = Math.min(30000, 1000 * 2 ** this.attempts);
            this.attempts++;
            console.log(`reconnect in ${delay}ms (attempt ${this.attempts})`);
            setTimeout(() => this.connect(), delay);
        };
    }

    on(event, fn) { (this.handlers[event] ??= []).push(fn); }

    send(data) {
        if (this.ws.readyState === WebSocket.OPEN) this.ws.send(data);
        // 没连上的时候要不要 buffer,看业务
    }
}

指数退避(1s, 2s, 4s, 8s, 16s, 30s 封顶)避免了"网络抖动时所有客户端同时疯狂重连"打垮服务的雷击效应。生产环境务必加这个。

消息可靠性:WebSocket 不保证

WebSocket 提供的可靠性等同于 TCP —— 字节流不丢、不乱序。但应用层不会自动重发:你 send 了一条消息,如果连接断开,这条消息就丢了。如果业务要求"必送达",需要自己实现:

  • 客户端给每条消息加 seq,服务端 ack 确认后客户端删除待发队列里的消息
  • 重连后用 last_seq 续传,服务端补发漏掉的消息
  • 关键消息走 HTTP API 做兜底,WebSocket 只负责实时通知。
// 极简版可靠传输框架
class ReliableWS {
    constructor() {
        this.pending = new Map();    // seq -> { data, timer }
        this.seq = 0;
    }
    send(data) {
        const s = ++this.seq;
        this.pending.set(s, { data });
        this.ws.send(JSON.stringify({ seq: s, data }));
        // 设置超时重发
        const timer = setTimeout(() => this.resend(s), 3000);
        this.pending.get(s).timer = timer;
    }
    onMessage(msg) {
        if (msg.type === 'ack') {
            const p = this.pending.get(msg.seq);
            if (p) { clearTimeout(p.timer); this.pending.delete(msg.seq); }
        }
    }
}

nginx 反向代理 WebSocket

# nginx.conf 片段
location /ws {
    proxy_pass http://backend:8080;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;       # 关键:转发 Upgrade
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_read_timeout 3600s;                      # 默认 60s 太短
    proxy_send_timeout 3600s;
}

三个最常踩的坑:没设 Upgrade 头 → 502;proxy_read_timeout 没改大 → 长连接 60s 后被切;用了 HTTP/1.0(默认是 1.0) → Upgrade 失效。

横向扩展:多实例间的消息同步

一台机器连不下所有用户,起多个 WebSocket 实例 —— 但用户 A 连在实例 1,用户 B 连在实例 2,A 给 B 发消息怎么办?需要一个消息总线(Redis Pub/Sub、NATS、Kafka 都行)在多实例之间转发。

// 用 Redis Pub/Sub 实现跨实例广播
const Redis = require('ioredis');
const sub = new Redis();
const pub = new Redis();

sub.subscribe('chat');
sub.on('message', (channel, msg) => {
    // 收到其他实例广播的消息,本机所有连接转发出去
    wss.clients.forEach((c) => {
        if (c.readyState === WebSocket.OPEN) c.send(msg);
    });
});

wss.on('connection', (ws) => {
    ws.on('message', (data) => {
        pub.publish('chat', data);   // 不直接广播,而是发到 Redis
    });
});

Socket.IO 内置 socket.io-redis 适配器做了这件事,推荐直接用。原生 WebSocket 需要自己实现。

WebSocket vs SSE vs 长轮询

  • WebSocket:全双工,服务端能主推,客户端也能频繁推。最灵活,但有协议复杂度。适合实时聊天、协同编辑、游戏。
  • SSE(Server-Sent Events):单向,服务端推、客户端只能用普通 HTTP 请求回应。基于 HTTP/1.1,自带断线重连,实现简单。适合"通知、行情、AI 流式输出"。
  • 长轮询:客户端发请求,服务端 hang 住直到有数据或超时。兼容性最好,但延迟和资源占用最差。现代场景几乎不用了,除非要绕开特殊网络限制。

Socket.IO 与原生 WebSocket 的取舍

Socket.IO 不只是封装,它在 WebSocket 不可用时自动回退到长轮询,还内置了"房间""自动重连""消息确认""二进制传输"等一堆生产级特性。新项目我通常推荐直接用它,自己造这套东西稳定到生产水平至少要 1-2 个月。

// 服务端
const { Server } = require('socket.io');
const io = new Server(httpServer, { cors: { origin: '*' } });

io.on('connection', (socket) => {
    socket.on('join', (room) => socket.join(room));
    socket.on('msg', ({ room, text }) => {
        io.to(room).emit('msg', { from: socket.id, text });
    });
});

// 客户端
import { io } from 'socket.io-client';
const socket = io('https://example.com');
socket.emit('join', 'lobby');
socket.on('msg', (m) => render(m));

// 带 ack 的可靠消息(server 端 callback 调用就表示收到)
socket.emit('save', payload, (response) => {
    if (response.ok) console.log('saved');
});

选型经验:有特殊协议需求或对协议层完全控制的场景用原生 WebSocket;一般业务直接 Socket.IO,把工程精力省下来做业务

写在最后

WebSocket 解决了一个具体问题:服务端要主动、低延迟、双向地和客户端通信。它不是 HTTP 的替代品,只是 HTTP 在长连接场景下的补充。用它的代价是一整套你以前不用操心的工程问题:心跳、重连、扩容、消息可靠性 —— 这些都得你自己处理或选 Socket.IO / Phoenix Channels / Centrifugo 这类成熟方案。

给你一个清单做技术选型:消息频率 < 1 次/分钟,选 SSE 或干脆轮询;仅服务端推送、客户端只 ack,选 SSE;双向频繁交互、需要房间/分组,选 WebSocket;对消息可靠性要求高、又有横向扩展,直接用 Socket.IO + Redis 适配器。把"用什么协议"想清楚再写代码,能避开 90% 的实时通信坑。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

观察者模式的四种现代演化:从 EventEmitter 到响应式与 RxJS

2026-5-15 11:12:40

技术教程

垃圾回收机制详解:从引用计数到三色标记的完全指南

2026-5-15 11:12:41

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索