前言
SSE(Server-Sent Events) 是一种基于 HTTP 的服务器单向推送技术。相比 WebSocket 的双向通信,SSE 更轻量、实现更简单,非常适合服务器向客户端持续推送数据的场景。ChatGPT、Claude 等 AI 产品都使用 SSE 来实现流式输出。
本文以 iOS 客户端实现为例,详细讲解 SSE 数据的接收与解析过程。
一、完整示例:一个 SSE 事件从发送到接收的全过程
服务器发送的数据
vbnet
event: chunk
id: 1
data: {"content":"Hello"}
⚠️ 注意:最后有一个空行,这是事件结束的标志!
第一步:服务器发送,网络传输
服务器发送的原始字节流:
less
e v e n t : c h u n k \n i d : 1 \n d a t a : { . . . } \n \n
问题:网络传输时,数据可能被分成多个块到达客户端。
假设网络把数据分成了 3 块:
| 数据块 | 内容 |
|---|---|
| 块 1 | "event: chu" |
| 块 2 | "nk\nid: 1\nda" |
| 块 3 | "ta: {\"content\":\"Hello\"}\n\n" |
第二步:行解析器处理(OKGrowthUTF8LineParser)
2.1 收到块 1:"event: chu"
arduino
┌────────────────────────────────────────────────────────────┐
│ 输入: "event: chu" │
│ │
│ 处理过程: │
│ 1. 缓冲区当前为空: "" │
│ 2. 合并: "" + "event: chu" = "event: chu" │
│ 3. 扫描换行符: 没找到 \n │
│ 4. 没有完整行,全部存入缓冲区 │
│ │
│ 缓冲区: "event: chu" │
│ 输出: [] ← 空数组,没有完整行 │
└────────────────────────────────────────────────────────────┘
2.2 收到块 2:"nk\nid: 1\nda"
swift
┌────────────────────────────────────────────────────────────┐
│ 输入: "nk\nid: 1\nda" │
│ │
│ 处理过程: │
│ 1. 缓冲区当前: "event: chu" │
│ 2. 合并: "event: chu" + "nk\nid: 1\nda" │
│ = "event: chunk\nid: 1\nda" │
│ 3. 扫描换行符: │
│ - 位置 12 找到 \n → 提取 "event: chunk" │
│ - 位置 18 找到 \n → 提取 "id: 1" │
│ - "da" 后面没有 \n,存入缓冲区 │
│ │
│ 缓冲区: "da" │
│ 输出: ["event: chunk", "id: 1"] │
└────────────────────────────────────────────────────────────┘
2.3 收到块 3:"ta: {\"content\":\"Hello\"}\n\n"
swift
┌────────────────────────────────────────────────────────────┐
│ 输入: "ta: {\"content\":\"Hello\"}\n\n" │
│ │
│ 处理过程: │
│ 1. 缓冲区当前: "da" │
│ 2. 合并: "da" + "ta: {...}\n\n" │
│ = "data: {\"content\":\"Hello\"}\n\n" │
│ 3. 扫描换行符: │
│ - 位置 27 找到 \n → 提取 "data: {...}" │
│ - 位置 28 找到 \n → 提取 "" ← 空行! │
│ │
│ 缓冲区: "" ← 清空 │
│ 输出: ["data: {\"content\":\"Hello\"}", ""] │
└────────────────────────────────────────────────────────────┘
2.4 行解析器总结
经过 3 次数据块处理,行解析器依次输出:
| 次序 | 输出的完整行 |
|---|---|
| 块 1 后 | [] (无) |
| 块 2 后 | ["event: chunk", "id: 1"] |
| 块 3 后 | ["data: {...}", ""] |
合计得到 4 行 : "event: chunk", "id: 1", "data: {...}", ""
第三步:事件解析器处理(OKGrowthEventParser)
SSEClient 将行解析器输出的每一行,依次传给事件解析器。
3.1 解析第 1 行:"event: chunk"
ini
┌────────────────────────────────────────────────────────────┐
│ 输入: "event: chunk" │
│ │
│ 处理过程: │
│ 1. 行长度 > 0,不是空行 │
│ 2. 查找冒号位置: 5 │
│ 3. 字段名 = "event" │
│ 4. 字段值 = "chunk" (冒号后面,跳过空格) │
│ 5. 字段名是 "event",存储 eventType │
│ │
│ 当前状态: │
│ eventType = "chunk" ✓ │
│ eventId = "" │
│ data = "" │
│ │
│ 动作: 继续等待下一行 │
└────────────────────────────────────────────────────────────┘
3.2 解析第 2 行:"id: 1"
ini
┌────────────────────────────────────────────────────────────┐
│ 输入: "id: 1" │
│ │
│ 处理过程: │
│ 1. 行长度 > 0,不是空行 │
│ 2. 查找冒号位置: 2 │
│ 3. 字段名 = "id" │
│ 4. 字段值 = "1" │
│ 5. 字段名是 "id",存储 eventId │
│ │
│ 当前状态: │
│ eventType = "chunk" ✓ │
│ eventId = "1" ✓ │
│ data = "" │
│ │
│ 动作: 继续等待下一行 │
└────────────────────────────────────────────────────────────┘
3.3 解析第 3 行:"data: {\"content\":\"Hello\"}"
swift
┌────────────────────────────────────────────────────────────┐
│ 输入: "data: {\"content\":\"Hello\"}" │
│ │
│ 处理过程: │
│ 1. 行长度 > 0,不是空行 │
│ 2. 查找冒号位置: 4 │
│ 3. 字段名 = "data" │
│ 4. 字段值 = "{\"content\":\"Hello\"}" │
│ 5. 字段名是 "data",追加到 data │
│ (当前 data 为空,直接赋值) │
│ │
│ 当前状态: │
│ eventType = "chunk" ✓ │
│ eventId = "1" ✓ │
│ data = "{\"content\":\"Hello\"}" ✓ │
│ │
│ 动作: 继续等待下一行 │
└────────────────────────────────────────────────────────────┘
3.4 解析第 4 行:"" (空行) ⚡
ini
┌────────────────────────────────────────────────────────────┐
│ 输入: "" (空行) │
│ │
│ 处理过程: │
│ 1. 行长度 == 0,是空行! │
│ 2. ⚡ 空行触发事件分发! │
│ │
│ 当前状态 (即将分发): │
│ eventType = "chunk" │
│ eventId = "1" │
│ data = "{\"content\":\"Hello\"}" │
│ │
│ 执行 dispatchEvent(): │
│ 1. 调用回调: onEvent("chunk", "1", "{...}") │
│ 2. 重置状态: │
│ eventType = "" │
│ eventId = "" │
│ data = "" │
│ │
│ 动作: 🎯 触发回调!准备解析下一个事件 │
└────────────────────────────────────────────────────────────┘
第四步:事件分发,回调业务层
scss
┌────────────────────────────────────────────────────────────┐
│ 回调链 │
├────────────────────────────────────────────────────────────┤
│ │
│ EventParser.dispatchEvent() │
│ │ │
│ │ onEvent("chunk", "1", "{\"content\":\"Hello\"}") │
│ ↓ │
│ SSEClient.handleEvent() │
│ │ │
│ │ 判断: eventType != "connected",不更新状态 │
│ │ │
│ │ onTextChunk("chunk", "1", "{...}") │
│ ↓ │
│ MLNSSETool │
│ │ │
│ │ [onTextChunk addStringArgument:@"chunk"]; │
│ │ [onTextChunk addStringArgument:@"1"]; │
│ │ [onTextChunk addStringArgument:@"{...}"]; │
│ │ [onTextChunk callIfCan]; │
│ ↓ │
│ Lua 业务层 │
│ │ │
│ │ onEvent("chunk", "1", '{"content":"Hello"}') │
│ ↓ │
│ 业务代码处理 │
│ │ │
│ │ local json = cjson.decode(data) │
│ │ print(json.content) -- 输出: Hello │
│ │ 更新 UI 显示 │
│ ↓ │
│ ✅ 完成! │
│ │
└────────────────────────────────────────────────────────────┘
完整流程图(从服务器到 Lua)
swift
服务器发送: "event: chunk\nid: 1\ndata: {...}\n\n"
│
↓ 网络分块传输
┌─────────────────────────────────────────────────────────────┐
│ 第一步:网络层 │
├─────────────────────────────────────────────────────────────┤
│ 块1: "event: chu" │
│ 块2: "nk\nid: 1\nda" │
│ 块3: "ta: {...}\n\n" │
└─────────────────────────────────────────────────────────────┘
│
↓ NSURLSession.didReceiveData
┌─────────────────────────────────────────────────────────────┐
│ 第二步:行解析器 │
│ (UTF8LineParser) │
├─────────────────────────────────────────────────────────────┤
│ 块1 → [] │
│ 块2 → ["event: chunk", "id: 1"] │
│ 块3 → ["data: {...}", ""] │
│ │
│ 合计得到4行: `"event: chunk"`, `"id: 1"`, `"data: {...}"`, `""` │
└─────────────────────────────────────────────────────────────┘
│
↓ 逐行传递
┌─────────────────────────────────────────────────────────────┐
│ 第三步:事件解析器 │
│ (EventParser) │
├─────────────────────────────────────────────────────────────┤
│ 第1行 "event: chunk" → eventType = "chunk" │
│ 第2行 "id: 1" → eventId = "1" │
│ 第3行 "data: {...}" → data = "{...}" │
│ 第4行 "" → ⚡ 触发 dispatchEvent() │
└─────────────────────────────────────────────────────────────┘
│
↓ onEvent 回调
┌─────────────────────────────────────────────────────────────┐
│ 第四步:事件分发 │
├─────────────────────────────────────────────────────────────┤
│ SSEClient.handleEvent("chunk", "1", "{...}") │
│ ↓ │
│ MLNSSETool.onTextChunk("chunk", "1", "{...}") │
│ ↓ │
│ Lua: onEvent("chunk", "1", '{"content":"Hello"}') │
│ ↓ │
│ 业务代码: 解析 JSON,更新 UI │
└─────────────────────────────────────────────────────────────┘
│
↓
✅ 处理完成!
二、关键点总结
2.1 为什么需要行解析器?
网络数据分块到达,一行可能被拆成多块。行解析器用缓冲区解决这个问题。
2.2 为什么空行这么重要?
makefile
event: chunk ← 存储 eventType,不触发
id: 1 ← 存储 eventId,不触发
data: {...} ← 存储 data,不触发
← ⚡ 只有空行才触发事件分发!
空行 = 事件结束的信号
2.3 一个事件的完整生命周期
| 阶段 | 输入 | 输出 |
|---|---|---|
| 网络传输 | 字节流 | 数据块 |
| 行解析器 | 数据块 | 文本行数组 |
| 事件解析器 | 文本行 | event/id/data |
| 空行触发 | "" | dispatchEvent() |
| 回调链 | event/id/data | Lua onEvent |