SSE 是什么?
SSE = Server-Sent Events(服务端推送事件)。
一种 HTTP 长连接 上的数据格式:服务器持续往客户端推一行一行数据,客户端边收边处理,不用等整份响应全部下完。
和 WebSocket 的区别(面试常问):
SSE
WebSocket
方向
主要是 服务器 → 客户端(单向)
双向
协议
普通 HTTP
独立协议
典型用途
日志流、大模型逐字输出
聊天室、游戏
OpenAI 的 stream: true 用的就是这类 SSE 流:响应体不是一个大 JSON,而是很多行以 data: 开头的文本。
为什么要用 SSE 做 LLM?
大模型生成慢,若等非流式:
- 用户盯着空白或 loading 很久才看到全文
- 体感延迟 = 整段生成时间
流式 SSE:
-
体感延迟 ≈ 首 token 时间(往往短很多)
-
总时间差不多,但 体验更好
流式读 SSE
拆成两部分:
-
流式(streaming)
模型每生成一小段字(一个 token 或几个字符),服务器就推一段;客户端 边收边显示,用户能更早看到「第一个字」,不用等整段回答生成完。 -
读 SSE
客户端用URLSession的 异步字节流(例如session.bytes(for:)),按行读响应,解析以data:开头的行,从里面的 JSON 取出delta.content,拼成完整译文。
逻辑(HTTPInterpretationClient.parseSSELineData)大致是:
服务器发来一行:
data: {"choices":[{"delta":{"content":"你"}}]}
再一行:
data: {"choices":[{"delta":{"content":"好"}}]}
最后一行:
data: [DONE]
客户端:把 "你" + "好" 拼起来 → 界面上先显示「你」再显示「你好」
这就是「流式读 SSE」:用流的方式读 HTTP 响应体里的 SSE 行,并增量更新 UI。
一句话记忆
SSE = 服务器用 HTTP 一行行推数据;流式读 SSE = App 边读这些行、边从每行 JSON 里抠出新增文字、边更新界面,而不是等一整块 JSON 下载完再解析。
async/await 和 SSE
概念
解决什么问题
SSE / 流式
数据怎么从服务器来——一块一块推,不是一次给全
async/await
代码怎么等 I/O——等的时候不堵死线程,用挂起/恢复
它们不是同一种东西,但在 App 里经常一起用:用 async/await 去等待并读取 SSE 流里的每一块数据。
关系可以概括成一句话
SSE 是传输形态;async/await 是 Swift 里读这种传输的写法。
没有 async/await,你仍可以用回调读 SSE;有了 async/await,可以用 for try await 按字节/行读,代码更直、也好和 Task 取消配合。
实际项目里是怎么合在一起的?
听译通用模式(我自己开发的一款翻译软件)走 HTTPInterpretationClient.streamPlainTranslation,典型顺序是:
1. async 函数里发起请求(await session.bytes(...))
2. 得到 AsyncSequence(字节流)
3. for try await byte in bytes { ... } 逐字节读
4. 拼出一行 → 解析 "data: {...}" → 取出 delta.content
5. onPartial(已拼接的全文) 更新 UI
对应含义:
步骤
async/await 在干什么
SSE 在干什么
await session.bytes(for: urlRequest)
挂起,等 HTTP 连接建立、开始收到 body
响应体是长流,不是一次性 Data
for try await byte in bytes
每来一个字节就恢复一次循环体
从 SSE 流里增量读
onPartialTranslation(assembled)
常在 Task { @MainActor in } 里改 UI
每解析出一段译文就推一次界面
所以:await 的是「下一批 I/O 就绪」;SSE 决定「就绪时来的是一小段 delta,不是整包 JSON」。
和非流式对比(同是 async/await)
同文件里两种路径都用 async,差别在读响应的方式:
// 非流式:一次 await,拿完整 Data
let (data, response) = try await session.data(for: urlRequest)
// 再 JSONDecoder 解析 choices[0].message.content
// 流式 SSE:await 打开流,再 for try await 读很多遍
let (bytes, response) = try await session.bytes(for: urlRequest)
for try await byte in bytes { ... parseSSELineData ... }
- 都是 async I/O
- 流式 = 一次
await开门 + 很多次await下一个字节/行(for try await) - 非流式 = 通常 一次
await等整包结束
面试可说:流式并没有取代 async/await,而是让 同一个 async 函数里出现更长的「多次挂起」循环。
和 UI、Task 的关系
- 按钮:
Task { await client.interpret(...) }—— 进入异步上下文。 interpret内部await读 SSE,partial 回调里可能再Task { @MainActor in 更新双栏 }。- 用户点停止:
Task.cancel()→for try await会抛取消错误,循环结束,不再读 SSE。
取消是 async/await + Task 的能力;边读边显示是 SSE 的能力;两者一起才能保证「停得掉、又不卡 UI」。
常见误解
误解 1:「用了 SSE 就不用 async/await了」
→ 错。读网络流仍是 I/O,本项目仍用 async 函数 + await / for try await。
误解 2:「async/await 会自动变成流式」
→ 错。必须请求里 "stream": true,且用 bytes + 解析 data: 行;data(for:) 是一次性读完。
误解 3:「流式 = 在后台线程」
→ 不准确。await 时让出执行权;是否在主线程由 @MainActor 决定。SSE 解析可在 async 上下文里跑,改 UI 再回 MainActor。
类比(帮助记忆)
- SSE:水龙头一直滴水(服务器一直推
data:行)。 - async/await:你站在旁边,不用傻站着干等(不阻塞 UI);每滴到了你就处理一下(
for try await),桶里水(assembled译文)慢慢变满。