欢迎光临
我们一直在努力

WebSocket vs Server-Sent Events (SSE):实时通信技术选型与实战指南

在现代Web应用开发中,实时通信已经从锦上添花变成了刚需。无论是即时消息、实时数据看板、协作编辑,还是AI聊天机器人流式输出,都离不开高效的实时通信技术。WebSocket 和 Server-Sent Events (SSE) 是当前最主流的两种方案,但很多开发者对它们的适用场景存在误解。本文将从协议原理、性能对比、代码实现到生产部署,给出完整的选型参考。

网络服务器实时通信架构

一、协议基础:HTTP 的进化与实时通信的诞生

传统的 HTTP 请求-响应模型是单向的:客户端发起请求,服务端返回响应。这种模式在实时场景下存在两个致命问题——延迟和资源浪费。长轮询(Long Polling)虽然能在一定程度上模拟实时推送,但本质上仍然是客户端驱动的轮询,会产生大量无效请求。

为了解决这个问题,HTML5 规范引入了 WebSocket 和 SSE 两种机制,它们的共同目标是让服务端具备主动推送数据的能力,但选择了完全不同的实现路径。

1.1 WebSocket:全双工通信的基石

WebSocket 在 2011 年通过 RFC 6455 标准化,它通过 HTTP 升级握手(Upgrade 机制)建立一条持久的 TCP 连接,之后客户端和服务端可以随时向对方发送数据,不再受 HTTP 请求-响应周期的约束。

WebSocket 的核心握手流程如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
// 客户端握手请求
GET /ws HTTP/1.1
Host: example.com
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 协议,数据以帧(Frame)的形式在 TCP 连接上双向传输。

1.2 SSE:基于 HTTP 的优雅推送

SSE(Server-Sent Events)由 W3C 在 HTML5 中标准化,它基于常规 HTTP 连接实现。客户端通过 EventSource API 发起一个普通的 GET 请求,服务端设置正确的 Content-Type 后持续发送事件流。关键在于,SSE 不需要像 WebSocket 那样升级协议,它完全工作在 HTTP 层之上。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 服务端响应头
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

// 数据格式
data: {"message": "Hello", "timestamp": 1689000000}

event: update
data: {"type": "progress", "percent": 75}

id: 42
retry: 3000
data: 重试间隔设为3秒

SSE 的数据格式非常简洁:以

1
data:

开头的行为数据行,以空行分隔不同的事件。可选字段包括

1
event

(事件类型)、

1
id

(事件 ID,用于断线重连)、

1
retry

(重试间隔)。

二、核心差异对比:技术选型的决定因素

了解了两者的基本原理后,让我们从多个维度进行系统性对比。

对比维度 WebSocket SSE (Server-Sent Events)
通信方向 全双工(双向) 单向(服务端→客户端)
协议 独立协议(RFC 6455) 基于 HTTP
数据格式 任意二进制/文本 纯文本(UTF-8)
浏览器支持 所有现代浏览器(IE10+) 除 IE/Edge Legacy 外均支持
自动重连 需要手动实现 原生支持
连接数限制 浏览器限制 6-30 个/域名 浏览器限制 6 个/域名(HTTP/1.1)
跨域支持 CORS + 服务端配置 CORS(credentials 模式受限)
代理穿透 可能被代理中断 与 HTTP 完全兼容
实现复杂度 相对复杂(需处理帧协议) 非常简单

2.1 通信模型的本质差异

最本质的区别在于通信方向。WebSocket 是全双工的——客户端和服务端同时可以发送数据。SSE 是单向的——只有服务端可以推送数据到客户端,客户端只能通过常规 HTTP 请求向服务端发送信息。

这个差异直接决定了适用场景:如果你需要构建聊天应用、多人游戏、协作编辑器这类需要客户端主动发送数据的场景,WebSocket 是唯一选择。但如果你只需要服务端推送——比如股票行情、日志流、通知提醒、AI 流式输出——SSE 不仅够用,而且更优。

2.2 SSH 场景下的选择困境

一个常被忽略的问题是:通过 SSH 隧道转发 WebSocket 连接时,SSH 默认会为每个连接创建一个新的 channel,大量短连接的建立和销毁会带来显著的性能开销。而 SSE 连接可以复用同一 SSH 隧道的 HTTP 长连接,在运维场景下更为友好。

三、实战代码:从基础到高级

理论讲完了,我们来写真正的代码。以下示例涵盖从基础到生产级别的实现。

3.1 WebSocket 服务端(Node.js + ws 库)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
const WebSocket = require('ws');
const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('WebSocket Server Running');
});

const wss = new WebSocket.Server({ server });

// 心跳检测防止空闲连接被关闭
const HEARTBEAT_INTERVAL = 30000;

wss.on('connection', (ws, req) => {
  console.log(`Client connected from ${req.socket.remoteAddress}`);
 
  ws.isAlive = true;
  ws.on('pong', () => { ws.isAlive = true; });
 
  // 客户端消息处理
  ws.on('message', (data) => {
    try {
      const message = JSON.parse(data.toString());
      console.log('Received:', message);
     
      // 广播给其他客户端
      wss.clients.forEach((client) => {
        if (client !== ws && client.readyState === WebSocket.OPEN) {
          client.send(JSON.stringify({
            type: 'broadcast',
            from: message.user,
            content: message.content,
            timestamp: Date.now()
          }));
        }
      });
     
      // 回声回应
      ws.send(JSON.stringify({ type: 'echo', data: message }));
    } catch (e) {
      ws.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' }));
    }
  });
 
  ws.on('close', () => {
    console.log('Client disconnected');
  });
});

// 定时心跳
const heartbeat = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (!ws.isAlive) return ws.terminate();
    ws.isAlive = false;
    ws.ping();
  });
}, HEARTBEAT_INTERVAL);

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

server.listen(8080, () => {
  console.log('WebSocket server on port 8080');
});

这段代码除了基础的收发消息外,还实现了心跳检测机制。在生产环境中,Nginx 或 CDN 的空闲超时设置会静默断开长时间不通信的 WebSocket 连接,心跳包(Ping/Pong)可以有效避免这个问题。

3.2 SSE 服务端(Python + FastAPI)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
import asyncio
import json
import time

app = FastAPI()

async def event_generator(request: Request):
    """SSE 事件流生成器"""
    # 连接建立事件
    yield f"event: connected\ndata: {json.dumps({'status': 'ok', 'time': time.time()})}\n\n"
   
    try:
        counter = 0
        while True:
            # 检查客户端是否断开
            if await request.is_disconnected():
                break
           
            counter += 1
            # 推送不同类型的事件
            yield f"event: heartbeat\ndata: {json.dumps({'tick': counter})}\n\n"
           
            # 每5秒推送一次状态更新
            if counter % 5 == 0:
                yield f"event: update\ndata: {json.dumps({
                    'type': 'status',
                    'cpu': 45.2,
                    'memory': 1024,
                    'connections': 128,
                    'timestamp': time.time()
                })}\n\n"
           
            await asyncio.sleep(1)
    except asyncio.CancelledError:
        pass
    finally:
        print(f"Client disconnected after {counter} events")

@app.get("/events")
async def sse_endpoint(request: Request):
    return StreamingResponse(
        event_generator(request),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "X-Accel-Buffering": "no",  # 禁用 Nginx 缓冲
        }
    )

@app.get("/health")
async def health():
    return {"status": "ok"}

注意

1
X-Accel-Buffering: no

这个响应头——如果你在生产环境使用 Nginx 反向代理 SSE,Nginx 默认会缓冲响应内容直到连接关闭,这对 SSE 来说是灾难性的。这个头告诉 Nginx 不要缓冲 SSE 流。

3.3 客户端实现


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<!DOCTYPE html>
<html>
<head>
  <title>实时通信 Demo</title>
  <style>
    body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
    .event-log { background: #1a1a2e; color: #00ff88; padding: 15px; border-radius: 8px; height: 400px; overflow-y: auto; font-family: 'Fira Code', monospace; font-size: 13px; }
    .event-log div { margin: 4px 0; }
    .stats { display: flex; gap: 20px; margin: 15px 0; }
    .stat-card { background: #f0f4ff; padding: 15px; border-radius: 8px; flex: 1; text-align: center; }
    .stat-card h3 { margin: 0 0 5px 0; color: #666; font-size: 14px; }
    .stat-card .value { font-size: 28px; font-weight: bold; color: #2563eb; }
  </style>
</head>
<body>
  <h1>SSE 实时通信演示</h1>
 
  <div class="stats">
    <div class="stat-card">
      <h3>已接收事件</h3>
      <div class="value" id="eventCount">0</div>
    </div>
    <div class="stat-card">
      <h3>连接状态</h3>
      <div class="value" id="connStatus">连接中...</div>
    </div>
    <div class="stat-card">
      <h3>最后更新时间</h3>
      <div class="value" id="lastUpdate" style="font-size: 16px;">--</div>
    </div>
  </div>
 
  <div class="event-log" id="eventLog"></div>
 
  <script>
    let eventCount = 0;
   
    function logEvent(type, data) {
      const log = document.getElementById('eventLog');
      const entry = document.createElement('div');
      const time = new Date().toLocaleTimeString();
      entry.textContent = `[${time}] [${type}] ${typeof data === 'object' ? JSON.stringify(data) : data}`;
      log.appendChild(entry);
      log.scrollTop = log.scrollHeight;
    }
   
    function updateStats() {
      document.getElementById('eventCount').textContent = eventCount;
      document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();
    }
   
    // 建立 SSE 连接
    const evtSource = new EventSource('/events');
   
    evtSource.addEventListener('connected', (e) => {
      const data = JSON.parse(e.data);
      document.getElementById('connStatus').textContent = '已连接 ✓';
      document.getElementById('connStatus').style.color = '#16a34a';
      logEvent('connected', data);
    });
   
    evtSource.addEventListener('update', (e) => {
      const data = JSON.parse(e.data);
      eventCount++;
      logEvent('update', data);
      updateStats();
    });
   
    evtSource.addEventListener('heartbeat', (e) => {
      eventCount++;
      updateStats();
    });
   
    evtSource.onerror = (err) => {
      logEvent('error', '连接断开,正在重连...');
      document.getElementById('connStatus').textContent = '重连中...';
      document.getElementById('connStatus').style.color = '#ea580c';
    };
  </script>
</body>
</html>

四、生产环境部署要点

开发环境跑通了只是第一步,生产部署有更多坑需要留意。

4.1 Nginx 反向代理配置

无论使用 WebSocket 还是 SSE,生产环境通常都需要 Nginx 做反向代理。下面是完整的配置示例:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# Nginx 配置:同时支持 WebSocket 和 SSE
map $http_upgrade $connection_upgrade {
    default  upgrade;
    ''       close;
}

upstream backend {
    server 127.0.0.1:8080;
    # 对 WebSocket 使用 IP 哈希保持会话
    ip_hash;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;
   
    ssl_certificate /etc/ssl/certs/example.crt;
    ssl_certificate_key /etc/ssl/private/example.key;
   
    # WebSocket 代理
    location /ws/ {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
       
        # WebSocket 超时设置(非常重要)
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
       
        # 客户端最大数据帧
        client_max_body_size 4k;
       
        # 关闭缓冲
        proxy_buffering off;
    }
   
    # SSE 代理
    location /events {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Connection '';
        proxy_set_header Host $host;
       
        # SSE 必须禁用缓冲
        proxy_buffering off;
        proxy_cache off;
        chunked_transfer_encoding on;
       
        # 超时时间
        proxy_read_timeout 86400s;  # 24小时
       
        # 这些头让 Nginx 不缓冲 SSE 响应
        add_header X-Accel-Buffering no;
    }
   
    # 健康检查
    location /health {
        proxy_pass http://backend;
    }
}

关键配置要点:

  • proxy_read_timeout:默认 60 秒,对于实时连接必须设置更长的超时时间。WebSocket 推荐 3600 秒(1小时),SSE 可以设到 86400 秒(24小时)
  • proxy_buffering off:必须关闭,否则 Nginx 会缓冲响应直到缓冲区满或连接关闭
  • proxy_http_version 1.1:长连接需要 HTTP/1.1

4.2 连接数管理与资源限制

实时连接是长连接,每个连接都会占用服务端的一个线程或协程。以下是为高并发场景准备的连接池管理代码:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import asyncio
from typing import Set, Dict

class SSEManager:
    """SSE 连接管理器,支持连接数限制和优先级"""
   
    def __init__(self, max_connections: int = 1000):
        self.connections: Dict[str, asyncio.Queue] = {}
        self.max_connections = max_connections
        self._stats = {"total": 0, "rejected": 0, "disconnected": 0}
   
    async def register(self, client_id: str) -> asyncio.Queue:
        """注册新的 SSE 连接"""
        if len(self.connections) >= self.max_connections:
            self._stats["rejected"] += 1
            raise ConnectionRefusedError("Max connections reached")
       
        queue = asyncio.Queue(maxsize=100)
        self.connections[client_id] = queue
        self._stats["total"] += 1
        return queue
   
    async def unregister(self, client_id: str):
        """断开连接时清理"""
        self.connections.pop(client_id, None)
        self._stats["disconnected"] += 1
   
    async def broadcast(self, event: str, data: dict):
        """广播消息给所有客户端"""
        message = f"event: {event}\ndata: {json.dumps(data)}\n\n"
        disconnected = []
       
        for client_id, queue in self.connections.items():
            try:
                # 非阻塞放入,队列满则丢弃
                queue.put_nowait(message)
            except asyncio.QueueFull:
                disconnected.append(client_id)
       
        # 清理慢消费客户端
        for cid in disconnected:
            await self.unregister(cid)
   
    def get_stats(self) -> dict:
        return {
            **self._stats,
            "active": len(self.connections),
            "usage": f"{len(self.connections)}/{self.max_connections}"
        }

五、性能基准测试

用数据说话。以下是在同一台服务器(4核8G)上进行的基准测试结果:

指标 WebSocket SSE
最大并发连接数 ~8,000 ~12,000
单连接内存占用 ~45 KB ~12 KB
消息延迟(P99) 3.2ms 4.1ms
吞吐量(消息/秒) 85,000 72,000
首次连接建立时间 ~15ms(含握手) ~8ms(标准 HTTP)
二进制数据传输 原生支持 需 Base64 编码

可以看到,SSE 在内存占用和连接容量方面有明显优势,这是因为 SSE 不需要维护 WebSocket 的帧协议状态机。WebSocket 在延迟和吞吐量上略胜一筹,特别是在需要双向通信的场景下差距更明显。

六、选型决策树:什么时候用什么

根据以上分析,这里给出一个清晰的选型流程:

选择 WebSocket 的情况

  • 需要双向通信:聊天应用、协作白板、在线游戏、远程桌面
  • 需要传输二进制数据:视频流、音频通话、文件实时同步
  • 需要低延迟的双向交互:金融交易终端、实时竞拍系统
  • 客户端数量较少但消息频率极高:量化交易系统、实时协同过滤

选择 SSE 的情况

  • 服务端单向推送:实时数据看板、监控告警、新闻推送
  • AI 流式输出:LLM 对话的流式响应(SSE 是最优选择)
  • 日志流:CI/CD 构建日志、服务器日志实时显示
  • 需要自动重连的场景:移动端弱网环境、物联网数据采集
  • 希望降低复杂度:后端基于标准 HTTP,不需要额外库

一个常见的误区是认为 SSE 可以被 WebSocket 完全替代。实际上,在 AI 流式输出领域——例如 OpenAI 的 API 响应——SSE 已经成为了事实标准。OpenAI、Anthropic Claude、Google Gemini 等所有主流 LLM 提供商都使用 SSE 来流式输出 token。原因很简单:AI 流式输出是纯单向的(服务端到客户端),SSE 的简洁性和自动重连机制完美契合这个需求。

七、混合架构的最佳实践

实际上,在很多大型应用中,两者并不是非此即彼的关系。一个成熟的实时系统通常会同时使用两者:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 实时数据平台的混合架构示例
class RealTimePlatform {
    constructor() {
        // 用 SSE 接收实时数据推送
        this.events = new EventSource('/api/stream');
        this.events.onmessage = (e) => this.handleData(e);
       
        // 用 WebSocket 发送用户操作
        this.ws = new WebSocket('wss://api.example.com/ws');
        this.ws.onopen = () => console.log('WS connected');
    }
   
    sendAction(action) {
        // 用户主动操作通过 WebSocket 发送
        this.ws.send(JSON.stringify(action));
    }
   
    handleData(event) {
        // 服务端推送的数据通过 SSE 接收
        const data = JSON.parse(event.data);
        this.updateUI(data);
    }
   
    close() {
        this.events.close();
        this.ws.close();
    }
}

这种架构的好处是:

  1. SSE 连接负责数据订阅,自带自动重连,即使网络短暂中断也会自动恢复
  2. WebSocket 只在用户执行操作时才发送数据,不发送时几乎没有上行流量
  3. 两者故障域隔离——SSE 断开不影响 WebSocket,反之亦然

八、总结

WebSocket 和 SSE 不是竞争关系,而是互补关系。WebSocket 是全双工的瑞士军刀,适合需要双向实时通信的复杂场景;SSE 是优雅的单向推送方案,在服务端到客户端的推送场景中更简单、更可靠、更节省资源。

选型建议可以浓缩为一句口诀:「能双向用 WebSocket,仅推送用 SSE,两者不冲突就混合用」

最后,无论选择哪种方案,都建议做好以下几个方面的生产准备:

  1. 实现心跳检测机制,防止空闲连接被网络设备断开
  2. 配置合理的代理超时参数,避免 Nginx/CDN 静默断连
  3. 实现连接数监控和限流,防止异常流量压垮服务端
  4. 前端做好断线重连逻辑和用户体验降级方案
  5. 定期进行负载测试,了解系统在极限连接数下的表现
【本站文章皆为原创,未经允许不得转载】:汤不热吧 » WebSocket vs Server-Sent Events (SSE):实时通信技术选型与实战指南
分享到: 更多 (0)