---
title: WebSocket 与 SSE
url: https://doc.liz6.com/networking/06-HTTP/06-websocket-and-sse
locale: zh
area: networking
tags:
- networking
- HTTP
date: 2026-06-30
modified: 2026-06-30
description: HTTP 的请求-响应模型不适合实时场景——WebSocket 升级为全双工通道,SSE 用单向流保持简洁。选哪个取决于"服务器是否需要主动推送"和"是否需要二进制帧"。
---

# 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

| | WebSocket | SSE |
|---|---|---|
| 方向 | 双向 | 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*
