SSE:被低估的实时推送“轻骑兵”

在现代 Web 开发的江湖里,提到“实时通信”,大家往往脱口而出:“上 WebSocket!”仿佛它是解决一切延迟问题的银弹。

但你是否想过,如果你只是想让服务器给客户端发个通知、推个进度条,或者让 AI 像打字机一样逐字蹦出回答,真的需要动用 WebSocket 这种“重型武器”吗?

今天,我们要为一位被长期忽视的“轻骑兵”正名——SSE (Server-Sent Events)。它简单、优雅,且基于你最熟悉的 HTTP 协议。


🎭 场景模拟:餐厅里的故事

为了理解 SSE 和 WebSocket 的区别,我们走进一家餐厅。

1. 普通 HTTP:传统的“点餐模式”

你(客户端)走到柜台(服务器):“有新品吗?”
服务员:“没有。”(连接断开)
过了 5 分钟,你又跑过去:“有新品吗?”
服务员:“还是没有。”(连接断开)

痛点:你累得半死(带宽浪费),服务员烦得要命(服务器负载高),而且新品出炉了你还要等下一轮询问才能知道(延迟高)。这就是轮询 (Polling)

2. WebSocket:VIP 包厢的“对讲机”

你和服务员各拿一个对讲机。
你:“喂喂,听得到吗?”
服务员:“听得到。随时待命。”
一旦厨房有新菜,服务员立刻喊:“新菜出炉!”
你想加水,也立刻喊:“加杯水!”
特点:双向即时沟通,极其高效。
代价:你需要维护这套对讲机系统(复杂的握手、心跳、断线重连逻辑)。如果网络不好,对讲机坏了,你还得自己想办法重新配对。

3. SSE:餐厅的“广播喇叭” 📢

你坐在座位上,不用跑腿,也不用拿对讲机。
餐厅安装了一个广播喇叭(SSE 连接)。
服务员只需要在喇叭里喊:“今日特价:红烧肉!”、“3号桌的菜好了!”
你只管听着就行。
如果你想点菜(上行数据)?没关系,你举手示意一下(发送一个普通的 HTTP POST 请求)即可。
特点

  • 单向推送:服务员只管喊,你只管听。
  • 极简:不需要对讲机配对,只要有耳朵(浏览器原生支持)就能听。
  • 自动重连:如果喇叭信号中断了一下,恢复后它会自动接着播,不用你操心。

⚔️ SSE vs WebSocket:谁是你的菜?

特性SSE (广播喇叭)WebSocket (对讲机)
通信方向单向 (服务器 → 客户端)双向 (互相喊话)
底层协议HTTP/HTTPS (老熟人,防火墙不拦)ws:// (新协议,偶尔被拦截)
数据类型仅文本 (JSON, 字符串)文本 + 二进制 (图片, 音频流)
开发难度⭐ (极简)⭐⭐⭐ (需处理心跳、重连)
自动重连✅ 内置 (浏览器自带“复活甲”)❌ 无 (得自己写代码复活)
最佳场景AI 流式输出、股票行情、通知、日志在线游戏、即时聊天、协同编辑

一句话总结:

  • 如果主要是“服务器说,客户端听”,选 SSE
  • 如果双方要“频繁互喷”(如打游戏、协作画图),选 WebSocket

🛠️ 实战:用 Python (FastAPI) 搭建你的“广播站”

SSE 的魅力在于简单。在 FastAPI 中,实现一个 SSE 接口比写一个普通 Hello World 难不了多少。

1. 服务端:搭建广播喇叭

# sse_server.py
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio
import time
import json

app = FastAPI()

async def event_generator():
    """
    这是一个永动机生成器,只要客户端连着,它就不停说话。
    """
    count = 0
    try:
        while True:
            count += 1

            # 1. 准备数据
            data = {
                "time": time.strftime("%H:%M:%S"),
                "msg": f"第 {count} 次广播",
                "status": "online"
            }

            # 2. 按照 SSE 格式打包: "data: 内容\n\n"
            # 注意:两个换行符是必须的,代表一条消息结束
            yield f"data: {json.dumps(data)}\n\n"

            # 3. 心跳包:每 5 秒发个注释,防止中间商(Nginx)切断连接
            if count % 5 == 0:
                yield ": heartbeat\n\n"

            await asyncio.sleep(1) # 模拟每秒产生一条数据

    except asyncio.CancelledError:
        print("📻 客户端断开连接,广播停止。")
        raise

@app.get("/broadcast")
def start_broadcast():
    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream", # 告诉浏览器:这是流式数据
        headers={
            "Cache-Control": "no-cache", # 别缓存,我要实时的
            "Connection": "keep-alive"   # 保持连接不断
        }
    )

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

2. 客户端:竖起耳朵听

🌐 前端 (JavaScript) – 简单到哭

浏览器原生支持 EventSource,三行代码搞定:

const radio = new EventSource("http://localhost:8000/broadcast");

radio.onmessage = (event) => {
    const data = JSON.parse(event.data);
    console.log(`📢 收到广播: ${data.msg} at ${data.time}`);
    // 在这里更新你的 UI,比如把文字追加到页面上
};

radio.onerror = () => {
    console.log("📡 信号中断,浏览器正在尝试自动重连...");
};

🐍 后端 (Python) – 模拟监听者

如果你想在另一个 Python 服务里监听这个广播:

# sse_client.py
import requests
import json

def listen_to_radio(url):
    print("📻 正在调频...")
    with requests.get(url, stream=True) as r:
        for line in r.iter_lines():
            if line:
                decoded = line.decode('utf-8')
                if decoded.startswith("data:"):
                    data = json.loads(decoded[5:])
                    print(f"📩 收到: {data}")

listen_to_radio("http://localhost:8000/broadcast")

💡 避坑指南:老司机的经验之谈

虽然 SSE 很简单,但有几个坑容易踩:

1. Nginx 是个“急性子”

默认情况下,Nginx 会缓冲响应。这意味着服务器发了 10 条消息,Nginx 可能攒够了一包才发给客户端,导致“实时性”丧失。
解药:在 Nginx 配置里加上:

proxy_buffering off;
proxy_cache off;

2. 沉默是金?不,沉默是死

如果服务器半天不说话,中间的防火墙或负载均衡器会认为:“这连接是不是死了?”然后咔嚓切断。
解药:发送心跳包

yield ": heartbeat\n\n" 

以冒号开头的行是注释,客户端会忽略它,但连接会被激活,告诉中间设备:“我还活着!”

3. 只能传文本?

是的,SSE 只能传 UTF-8 文本。如果你想传图片或二进制视频流,请出门左转找 WebSocket。但对于 JSON 数据来说,这完全不是问题。


🏁 结语

SSE 就像是一位低调的实干家。它没有 WebSocket 那么炫技,也不像 HTTP 轮询那么笨重。它在“实时性”和“复杂性”之间找到了完美的平衡点。

下次当你需要实现:

  • AI 聊天的打字机效果
  • 📈 股票价格的实时跳动
  • 🔔 网页右上角的消息红点
  • 📊 后台任务的进度条

请记得,你不需要架设复杂的 WebSocket 服务器。打开你的 HTTP 接口,加上 text/event-stream,SSE 就会为你工作。

简单,才是最高级的复杂。