
Server-Sent Events (SSE) :ChatGPT打印机式输出
Server-Sent Events 提供了一种健壮、简洁且高效的解决方案,用于实现从服务器到客户端的实时、单向数据传输。其基于 HTTP 的基础、内置的重连机制以及易于实现的特点,使其非常适合用于广播更新、通知以及流式传输大型语言模型 (LLM) 响应等增量内容。
1.实时Web通信简介
2. 理解Server-Sent Events (SSE)
2.1. 关键特性
2.2. 常见场景
3. SSE协议的消息结构
3.1 完整HTTP响应示例
4. 使用SSE实现类似ChatGPT的打字机效果
4.2. 前端实现(HTML+JS)
4.1. 后端实现(FastAPI)
4.3. 运行方法
4.4. 效果图
4.5. 抓包来往HTTP消息
4.6 SSE流式通信时序图
5. 浏览器兼容性
6. SSE 与 WebSockets
在大语言模型(LLM)蓬勃发展的今天,我们经常看到聊天界面中LLM能够像打字机一样逐字输出回复。这种流畅的交互体验背后,离不开实时Web通信技术的支持。
1.实时Web通信简介
历史上,为了获取新数据,客户端通常需要反复向服务器发送请求,这种方式被称为轮询。这种传统方法效率低下,会产生大量的网络流量,并引入不可避免的延迟。
为了解决这些问题,Server-Sent Events (SSE) 和 WebSocket 等推送技术应运而生,它们允许服务器主动向客户端发起数据传输,从而实现即时更新,无需客户端持续请求。
2. 理解Server-Sent Events (SSE)
Server-Sent Events (SSE) 是一种服务器推送技术,它允许服务器通过单个 HTTP 连接向客户端发送数据更新。
SSE 定义了服务器如何在建立初始客户端连接后,主动向客户端发起数据传输。它通常用于向浏览器客户端发送消息更新或连续的数据流。
2.1. 关键特性
- 单向性
SSE 的一个显著特点是其单向通信模式:数据消息仅从服务器流向客户端(例如,用户的网络浏览器)。
SSE 的单向性并非局限,而是一种设计选择。通过限制通信方向,协议本身的复杂性得以降低,从而优化了其在广播场景中的性能。这意味着 SSE 完美地适用于客户端主要接收更新的场景,避免了管理双向协议中固有的开销和复杂性。 - HTTP 协议基础
SSE 完全基于标准 HTTP 协议运行。它使用标准的端口(80/443),并且通常无需特殊配置即可穿透代理和防火墙。SSE 依赖于长连接和分块传输编码(transfer-encoding: chunked),因此至少需要 HTTP/1.1 版本。 - 自动重连
SSE 的一个重要优势是其内置的自动重连机制。如果连接意外中断,客户端会自动尝试重新连接,确保数据流不中断。在重新连接时,客户端可以发送一个Last-Event-ID
头,允许服务器从上次已知的位置恢复数据流。 - 文本数据
SSE 只能发送基于文本的事件数据,通常以 UTF-8 编码的字符串形式。它不支持发送二进制数据。
2.2. 常见场景
- 实时更新: 新闻推送、社交媒体状态更新、实时体育赛事比分、股票价格波动 。
- 监控: 服务器或应用程序监控仪表板、物联网设备数据 。
- 通知: Web 应用程序的实时通知、电子邮件通知、系统警报 。
- AI 生成内容流式传输: SSE 非常适合流式传输大型语言模型 (LLM) 的增量响应,以创建类似“打字机”的效果 。
3. SSE协议的消息结构
SSE(Server-Sent Events)协议基于HTTP协议,服务端通过标准的HTTP响应持续推送事件。
3.1 完整HTTP响应示例
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
data: 你好,世界
data: 这是第二条消息
event: custom
data: 自定义事件内容
id: 42
响应头:
-
Content-Type: text/event-stream
必须,表明是SSE流。 -
Cache-Control: no-cache
建议,防止中间缓存。 -
Connection: keep-alive
建议,保持连接不断开。
消息体:
- 每条消息以
\n\n
结尾。 -
data:
字段为主要内容,可多行。 - 可选
event:
、id:
、retry:
等字段。
字段名称 | 描述 |
data | 事件的主要内容。 |
id | 可选的事件标识符,用于客户端重连时恢复流。 |
event | 可选的事件类型字符串。如果指定,客户端将分派命名事件。 |
retry | 可选的毫秒数,指定连接丢失后的重连等待时间。 |
4. 使用SSE实现类似ChatGPT的打字机效果
AI 并不是等待一段时间再一次性返回完整的答案,而是 边生成边显示,逐步展示 AI 的思考过程。这种方式不仅减少了等待时间,而且让 AI 的回答看起来更自然,仿佛它在思考时逐渐给出答案。这种交互体验显得更加生动、真实,用户可以感觉到 AI “在实时作答”。
如果不使用流式输出,模型需要等到所有文本生成完毕后才返回结果,用户可能会感到延迟很长。而流式调用能够逐步生成输出,减少等待时间,提升交互体验,类似于 ChatGPT 的打字效果。
普通 vs. 流式输出
模式 | 特点 |
普通输出(非流式) | 用户提交问题 → 等待几秒 → 一次性返回完整答案 |
流式输出(像 ChatGPT) | 用户提交问题 → AI 边生成边显示 → 逐步输出答案 |
相关阅读:从0开始:用 Streamlit + LangChain 搭建个简易ChatGPT
SSE 非常适合这种增量输出生成,因为它专为从服务器到客户端的连续、单向数据流而设计。LLM 后端生成的每个“令牌”或小块文本都可以作为独立的 SSE data
字段发送。
服务器只需将新生成的内容逐块写入 text/event-stream
连接,并在每个块后加上 \n\n
分隔符 。这种“原生令牌流式传输”使用户能够自然地体验 AI 交互,仿佛正在观察 AI 思考的过程。
4.1. 前端实现(HTML+JS)
建立连接
要打开连接并开始从服务器接收事件,需要创建一个新的 EventSource 对象,并提供生成事件的服务器端的 URL 。
例如:
const evtSource = new EventSource("/sse");
监听 message
或自定义 event
类型
-
onmessage
处理程序:
服务器如果没有发送 event
字段,那么默认将作为 message
事件接收,并由 EventSource
对象的 onmessage
属性处理 。数据可以通过 event.data
访问 。
例如:
evtSource.onmessage = (e) => { console.log(e.data); };
-
addEventListener()
用于命名事件:
服务器可以发送带有 event
字段的消息,指定自定义事件类型。客户端可以使用 EventSource.addEventListener()
监听这些命名事件。
例如:
sse.addEventListener("notice", (e) => { console.log(e.data); });
实际代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSE 打字机 Demo</title>
<style>
body { font-family: Consolas, monospace; background: #f9f9f9; padding: 2em; }
#output { font-size: 1.5em; background: #fff; padding: 1em; border-radius: 8px; min-height: 2em; }
</style>
</head>
<body>
<h2>SSE 打字机效果演示</h2>
<div id="output"></div>
<script>
// 连接SSE接口
const output = document.getElementById('output');
const evtSource = new EventSource('/sse');
evtSource.onmessage = function(event) {
if (event.data === '[END]') {
evtSource.close();
return;
}
output.textContent += event.data;
};
evtSource.onerror = function() {
output.textContent += '\n[连接已断开]';
};
</script>
</body>
</html>
-
EventSource('/sse')
连接后端SSE接口,自动接收流式数据。 - 每收到一个字符就追加到页面,实现打字机效果。
- 收到
[END]
时关闭连接。
4.2. 后端实现(FastAPI)
设置 Content-Type: text/event-stream 头
负责发送事件的服务器端脚本必须以 MIME 类型 Content-Type: text/event-stream 响应 。这会告知浏览器将传入数据解释为事件流。
实际代码示例
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
from fastapi.staticfiles import StaticFiles
import asyncio
app = FastAPI()
# 挂载静态文件目录
app.mount('/static', StaticFiles(directory='static'), name='static')
asyncdef gpt_stream(text: str, delay: float = 0.05):
"""
模拟GPT打字机效果,逐字发送文本。
:param text: 要发送的完整文本
:param delay: 每个字符之间的延迟(秒)
"""
for char in text:
yieldf"data: {char}\n\n"
await asyncio.sleep(delay)
yield"data: [END]\n\n"
@app.get('/sse')
asyncdef sse():
"""
SSE接口,流式返回文本内容。
"""
with open('novel.txt', 'r', encoding='utf-8') as f:
text = f.read()
return StreamingResponse(gpt_stream(text), media_type='text/event-stream')
4.3. 运行方法
1.启动服务:
uvicorn main:app --reload
2.在浏览器访问:
http://127.0.0.1:8000/static/index.html
即可看到SSE打字机效果。
4.4. 效果图
4.5. 抓包来往HTTP消息
- 每个
data:
字段后跟一个字符或一段文本,前端逐条接收。 - 结尾用
[END]
标记流式输出结束。
4.6 SSE流式通信时序图
5. 浏览器兼容性
6. SSE 与 WebSockets
特性 | Server-Sent Events (SSE) | WebSockets |
通信方向 | 单向(服务器到客户端) | 双向(客户端与服务器) |
底层协议 | HTTP/HTTPS | 自定义 WebSocket 协议 (ws://, wss://) |
支持数据格式 | 仅文本(UTF-8) | 文本(UTF-8)和二进制 |
实现复杂性 | 简单 | 相对复杂 |
防火墙/代理兼容性 | 良好(基于标准 HTTP) | 可能需要特定配置 |
理想用例 | 实时新闻、股票报价、通知、仪表板、LLM流式响应 | 聊天应用、在线游戏、协作工具、实时交互式仪表板 |
Server-Sent Events 提供了一种健壮、简洁且高效的解决方案,用于实现从服务器到客户端的实时、单向数据传输。其基于 HTTP 的基础、内置的重连机制以及易于实现的特点,使其非常适合用于广播更新、通知以及流式传输大型语言模型 (LLM) 响应等增量内容。
本文转载自AI取经路,作者:AI取经路
