第一个字节什么时候到
目标
亲手实现使用 SSE 时实际碰到的问题——代码是对的,界面上却一下子全部出来——并用时间来确认。
规则
- 文件全部创建在
/root/work/sse中。 - 应用实例的名称必须是
app。评分器会直接加载你的app.py来读取流。 - 请把事件间隔设为 0.3 秒,心跳设为 0.2 秒。评分器会计时。
启动服务器
cd /root/work/sse
uvicorn app:app --host 127.0.0.1 --port 8000 > /tmp/uv.log 2>&1 &
curl -N -s localhost:8000/stream
如果去掉 -N,curl 会在自己这一侧缓冲,所以即使服务器流式发送得很好,看起来也像是一下子全部出来的。诊断时最先该怀疑的是测量工具本身。
测量首字节
curl -N -s -o /dev/null -w '%{time_starttransfer}
' localhost:8000/stream
步骤
/stream——间隔 0.3 秒的 3 个事件- 线路格式 →
02-wire.txt id:、retry:Last-Event-ID续传 →04-resume.txt/idle——注释心跳- 复现
/buffered→06-timing.txt /long+finally→closed.log- 总结 →
08-notes.md
参考
如果不用空行(即连续两个换行符)结束事件,客户端就会认为事件还没有结束,并一直等待。这是“什么都收不到”的第 1 号原因。
创建永不结束的响应
在 /root/work/sse/app.py 中创建 FastAPI 应用,让 GET /stream 以 text/event-stream 发送 3 个事件,间隔为 0.3 秒。
mkdir -p /root/work/sse。应用名称必须是 app。StreamingResponse(gen(), media_type="text/event-stream") 就足够了(也安装了 sse-starlette)。请在生成器内部执行 await asyncio.sleep(0.3)。用 curl -N localhost:8000/stream 来确认——-N 会关闭 curl 的缓冲。
对齐线路格式
给每个事件加上 event: token 和 data:,并用空行结束事件。 把收到的原文原样保存为 02-wire.txt。
一个事件是 event: token\ndata: 안녕\n\n(韩文,意为“你好”)。漏掉最后的空行,是“什么都收不到”的第 1 号原因。保存方法:curl -N -s localhost:8000/stream > 02-wire.txt。
提供 id 和 retry
给每个事件从 1 开始加上 id:,并在流的最前面发送一次 retry:。
id 是浏览器记住之后,在重连时通过 Last-Event-ID 请求头还回来的值。retry: 3000 是重连等待时间(毫秒)。
从漏掉的地方接着发
如果请求中有 Last-Event-ID 请求头,就从下一个 id 开始发送。把总数增加到发送 5 个,并把以 Last-Event-ID: 3 收到的结果保存为 04-resume.txt。
request.headers.get("last-event-id")(请用小写查询)。没有就从 1 开始,有就从该值加 1 开始。确认方法:curl -N -s -H 'Last-Event-ID: 3' localhost:8000/stream > 04-resume.txt——必须只出现 id 4、5。这就是 SSE 的运维成本比 WebSocket 低的原因。
让空闲连接保持存活
创建 GET /idle,让它不发送任何事件,只以 0.2 秒的间隔发送 5 次注释心跳。
: ping\n\n——以冒号开头就是注释,所以客户端不会把它看作事件。只有字节在流动,这样就能越过负载均衡器的空闲超时(通常是 60 秒)。
复现缓冲
创建 GET /buffered。让它等待与 /stream 完全相同的量(0.3 秒 × 5 次),但要在全部生成完之后一次性返回。请在两条路径上测量首字节的到达时刻,并保存为 06-timing.txt。
不用生成器,而是创建一个列表,用 Response(...) 返回即可。测量:比较 curl -N -s -o /dev/null -w '%{time_starttransfer}\n' localhost:8000/stream 与 /buffered。流式传输在 0.3 秒以内,缓冲大约是 1.5 秒——总时间相同,首字节却不同。 这就是“代码是对的,界面上却一下子全部出来”这种症状的真面目。
察觉到连接断了
创建 GET /long,让它长时间流动,并在生成器中加入 finally,使其在断开时向 /root/work/sse/closed.log 写入一行。
即使客户端断开,生成器在察觉到之前也会一直运行。如果不用 try: ... finally: 放入清理代码,每关闭一次标签页,服务器上就会多积累一个僵尸任务。确认方法:curl -N -s --max-time 1 localhost:8000/long > /dev/null; cat closed.log。
什么时候用 SSE,什么时候用 WebSocket
在 08-notes.md 中写三行以上。第 6 步的两个数字意味着什么,Last-Event-ID 替你做了什么,以及必须选择 WebSocket 的一种情况。
正文中必须包含 버퍼링(韩文,意为“缓冲”)、Last-Event-ID 和 WebSocket。只需记住一条判断标准——连接断开时,什么会被自动恢复。