SSE Connect 数据解析详解

前言

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
相关推荐
酒醉的胡铁1 小时前
uniapp解决video组件在ios上全屏页面旋转90度,组件旋转180度
ios·uni-app
张飞签名上架3 小时前
深度解析超级签:iOS 应用分发的便捷之选与风险权衡
ios·苹果签名·企业签名·苹果超级签名·tf签
2501_915918413 小时前
iOS App的tcp、udp数据包抓取在实际开发中的使用方式
android·tcp/ip·ios·小程序·udp·uni-app·iphone
2501_915909065 小时前
iOS 应用在混淆或修改后,如何完成签名、重签名与安装测试
android·ios·小程序·https·uni-app·iphone·webview
Digitally8 小时前
如何顺利地将手机号码转移到新iPhone
ios·iphone
茅根竹蔗水__1 天前
iOS应用(App)生命周期、视图控制器(UIViewController)生命周期和视图(UIView)生命周期
ios
JAVA+C语言1 天前
iOS App小组件(Widget)显示LottieFiles动画
ios
2501_915921431 天前
如何将 iOS 应用的 IPA 文件安装到手机进行测试
android·ios·智能手机·小程序·uni-app·iphone·webview