本页目录

WebSocket 与 SSE

HTTP 的请求-响应模型不适合实时场景——WebSocket 升级为全双工通道,SSE 用单向流保持简洁。选哪个取决于"服务器是否需要主动推送"和"是否需要二进制帧"。

概述

WebSocket(RFC 6455, 2011)在 HTTP 握手基础上创建持久的双向 TCP 通道——浏览器和服务器可以随时互发消息。这解决了 HTTP 的"请求-响应"单向限制,使实时应用(聊天、游戏、直播弹幕、Grafana Live)成为可能。SSE(Server-Sent Events)是更简单的单向替代:服务器向客户端推送事件流,基于纯 HTTP,防火墙兼容性更好。两者都有心跳机制(ping/pong vs SSE 的自动重连),保持长连接在 NAT/proxy 环境中不掉线。

WebSocket Upgrade

Client → Server: GET /ws HTTP/1.1
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==   ← random, base64
  Sec-WebSocket-Version: 13
  Sec-WebSocket-Protocol: chat                    ← optional subprotocol

Server → Client: HTTP/1.1 101 Switching Protocols
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=  ← SHA1(Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
  Sec-WebSocket-Protocol: chat

  → 握手完成 → TCP 连接升级为双向 WebSocket

WebSocket Frame

 0                   1                   2                   3
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |        (16/64)              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                   Masking-key (if MASK set, 4 bytes)          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Payload Data (可变, masked if from client)   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

FIN(1): 1 = 这是消息的最后一帧
opcode(4): 0=continuation, 1=text(UTF-8), 2=binary, 8=close, 9=ping, 10=pong
MASK(1): client→server 必须 mask, server→client 必须不 mask

Masking (client → server):
  1. Client: 随机生成 4-byte masking key
  2. 对 payload 逐 byte: payload[i] = original[i] XOR key[i % 4]
  3. 目的: 防止中间代理缓存投毒 (攻击者让 browser 发 crafting WS payload
     → proxy 误解析为 HTTP response → corrupt cache)

Close frame (opcode=8):
  [status code (2B, big-endian)][reason (UTF-8, optional)]
  1000: normal closure
  1001: going away (server shutdown, page navigated away)

Ping/Pong (Heartbeat)

Ping frame (opcode=9): 可选 payload (application-specific)
Pong frame (opcode=10): 必须复制 ping 的 payload

→ 应用层心跳 (不同于 TCP keepalive)
→ 检测: dead connection (即使 TCP 还 alive 但 remote app hung)

Proxy/server timeout: nginx proxy_read_timeout 适用于 WebSocket →
  如果没有 ping/pong, 连接可能被 proxy 超时断开

SSE (Server-Sent Events)

单向 (server → client) 流, 基于 HTTP chunked encoding:

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache

id: 42                         ← last event ID (auto-reconnect since this ID)
event: alert                   ← optional event type (otherwise "message")
data: {"status":"firing"}      ← event data

data: multi-line data
data: second line
                               ← 空行 → dispatch event

Client API: EventSource (built-in HTML5):
  const es = new EventSource("/events");
  es.onmessage = (e) => { console.log(e.data); };
  es.addEventListener("alert", (e) => { ... });

WebSocket vs SSE

WebSocketSSE
方向双向Server → Client
协议WebSocket (upgrade)HTTP chunked
Binary data天然 (opcode=2)需 base64 encode
防火墙兼容差 (某些 proxy 不支持 upgrade)好 (纯 HTTP)
自动重连手动EventSource 自动 (since last-event-id)
适合聊天, 游戏, 实时协作通知, feed, 实时数据推送

参考

  • RFC: 6455
  • SSE: html.spec.whatwg.org/multipage/server-sent-events.html
  • WS 测试⁠: websocat (CLI WebSocket client)

Keywords: WebSocket, upgrade, frame, masking, ping/pong, SSE, EventSource, heartbeat