2026-06-03 10:30:42 +08:00
|
|
|
|
---
|
|
|
|
|
|
tags: [sse, event-stream, pub-sub, real-time, websockets-alternative, go]
|
|
|
|
|
|
create time: 2026-06-03 10:35
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# 08. SSE 实时推送
|
2026-06-03 10:12:49 +08:00
|
|
|
|
|
2026-06-03 10:30:42 +08:00
|
|
|
|
## 概述
|
|
|
|
|
|
|
|
|
|
|
|
内存 EventBus 发布/订阅,SSE 推送管线进度到浏览器,让用户实时看到生成过程。
|
2026-06-03 10:12:49 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-03 10:30:42 +08:00
|
|
|
|
## 正文
|
|
|
|
|
|
|
2026-06-03 10:12:49 +08:00
|
|
|
|
```mermaid
|
|
|
|
|
|
flowchart LR
|
2026-06-03 10:30:42 +08:00
|
|
|
|
P["Pipeline Callback"] -->|"Publish"| EB["EventBus Broker"]
|
|
|
|
|
|
EB -->|"Subscribe taskID"| H1["SSE Handler /tasks/:id/stream"]
|
|
|
|
|
|
EB -->|"SubscribeAll global"| H2["SSE Handler /projects/:id/stream"]
|
|
|
|
|
|
H1 -->|"text/event-stream"| B1["Browser EventSource"]
|
|
|
|
|
|
H2 -->|"text/event-stream"| B2["Browser EventSource"]
|
2026-06-03 10:12:49 +08:00
|
|
|
|
|
|
|
|
|
|
style P fill:#e8f5e9,stroke:#388e3c
|
|
|
|
|
|
style EB fill:#fff3e0,stroke:#f57c00
|
|
|
|
|
|
style H1 fill:#e3f2fd,stroke:#1976d2
|
|
|
|
|
|
style H2 fill:#e3f2fd,stroke:#1976d2
|
|
|
|
|
|
style B1 fill:#f3e5f5,stroke:#7b1fa2
|
|
|
|
|
|
style B2 fill:#f3e5f5,stroke:#7b1fa2
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-03 10:30:42 +08:00
|
|
|
|
## EventBus 架构
|
2026-06-03 10:12:49 +08:00
|
|
|
|
|
|
|
|
|
|
EventBus 是 Gen2D 的内存事件总线,负责在 Pipeline 执行过程中发布进度事件,并由 SSE Handler 订阅推送给客户端。
|
|
|
|
|
|
|
|
|
|
|
|
### 核心设计
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
type Broker struct {
|
|
|
|
|
|
mu sync.RWMutex
|
|
|
|
|
|
subs map[string][]chan TaskEvent // per-task 订阅
|
|
|
|
|
|
allSubs []chan TaskEvent // 全局订阅
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
| 组件 | 作用 |
|
|
|
|
|
|
|------|------|
|
|
|
|
|
|
| `subs` | 按 taskID 索引的订阅者列表 |
|
|
|
|
|
|
| `allSubs` | 全局订阅者(接收所有事件) |
|
|
|
|
|
|
| `sync.RWMutex` | 读写锁保护并发访问 |
|
|
|
|
|
|
|
|
|
|
|
|
### TaskEvent 数据结构
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
type TaskEvent struct {
|
|
|
|
|
|
TaskID string `json:"task_id"`
|
|
|
|
|
|
ProjectID string `json:"project_id,omitempty"`
|
|
|
|
|
|
Status string `json:"status"` // pending|running|saving|completed|failed
|
|
|
|
|
|
Stage string `json:"stage,omitempty"` // prompt_builder|asset_generator|...
|
|
|
|
|
|
Progress int `json:"progress"` // 0-100
|
|
|
|
|
|
Error string `json:"error,omitempty"`
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-03 10:30:42 +08:00
|
|
|
|
## 订阅模式
|
2026-06-03 10:12:49 +08:00
|
|
|
|
|
|
|
|
|
|
### Subscribe(taskID) — 任务级订阅
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
func (b *Broker) Subscribe(taskID string) <-chan TaskEvent {
|
|
|
|
|
|
ch := make(chan TaskEvent, 16) // 有界缓冲,容量 16
|
|
|
|
|
|
b.mu.Lock()
|
|
|
|
|
|
b.subs[taskID] = append(b.subs[taskID], ch)
|
|
|
|
|
|
b.mu.Unlock()
|
|
|
|
|
|
return ch
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- 用于 `GET /api/v1/tasks/:taskId/stream`
|
|
|
|
|
|
- 仅接收指定任务的状态变更
|
|
|
|
|
|
- Buffer 容量 **16**,足够应对正常进度更新频率
|
|
|
|
|
|
|
|
|
|
|
|
### SubscribeAll() — 全局订阅
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
func (b *Broker) SubscribeAll() <-chan TaskEvent {
|
|
|
|
|
|
ch := make(chan TaskEvent, 64) // 有界缓冲,容量 64
|
|
|
|
|
|
b.mu.Lock()
|
|
|
|
|
|
b.allSubs = append(b.allSubs, ch)
|
|
|
|
|
|
b.mu.Unlock()
|
|
|
|
|
|
return ch
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- 用于 `GET /api/v1/projects/:projectId/stream`
|
|
|
|
|
|
- 接收所有任务的事件,在 Handler 层按 projectID 过滤
|
|
|
|
|
|
- Buffer 容量 **64**,因为全局事件量更大
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-03 10:30:42 +08:00
|
|
|
|
## Publish — 扇出分发
|
2026-06-03 10:12:49 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
func (b *Broker) Publish(taskID string, event TaskEvent) {
|
|
|
|
|
|
// 1. 发送到任务级订阅者
|
|
|
|
|
|
for _, ch := range subs {
|
|
|
|
|
|
select {
|
|
|
|
|
|
case ch <- event:
|
|
|
|
|
|
default: // 满则丢弃,非阻塞
|
|
|
|
|
|
slog.Warn("subscriber buffer full, dropping event")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 2. 发送到全局订阅者
|
|
|
|
|
|
for _, ch := range allSubs {
|
|
|
|
|
|
select { ... }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**关键特性**:
|
|
|
|
|
|
- **Fan-out**:同时发送到 task 级和 global 级订阅者
|
|
|
|
|
|
- **Non-blocking send**:使用 `select default` 防止慢消费者阻塞发布方
|
|
|
|
|
|
- **慢消费者丢弃**:缓冲满时静默丢弃,保证 Pipeline 不被 SSE 拖慢
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-03 10:30:42 +08:00
|
|
|
|
## SSE Handler
|
2026-06-03 10:12:49 +08:00
|
|
|
|
|
|
|
|
|
|
### Stream — 任务级流
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
GET /api/v1/tasks/:taskId/stream
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
func (h *SSEHandler) Stream(c *gin.Context) {
|
|
|
|
|
|
// 1. 设置 SSE 响应头
|
|
|
|
|
|
c.Header("Content-Type", "text/event-stream")
|
|
|
|
|
|
c.Header("Cache-Control", "no-cache")
|
|
|
|
|
|
c.Header("X-Accel-Buffering", "no") // 禁用 nginx 缓冲
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 订阅事件
|
|
|
|
|
|
ch := h.broker.Subscribe(taskID)
|
|
|
|
|
|
defer h.broker.Unsubscribe(taskID, ch)
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 事件循环
|
|
|
|
|
|
for {
|
|
|
|
|
|
select {
|
|
|
|
|
|
case event := <-ch:
|
|
|
|
|
|
c.SSEvent("status", event)
|
|
|
|
|
|
c.Writer.Flush()
|
|
|
|
|
|
// 终态自动关闭
|
|
|
|
|
|
if event.Status == "completed" || event.Status == "failed" {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
case <-c.Request.Context().Done():
|
|
|
|
|
|
return // 客户端断开
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**SSE 响应头**:
|
|
|
|
|
|
|
|
|
|
|
|
| Header | 值 | 作用 |
|
|
|
|
|
|
|--------|---|------|
|
|
|
|
|
|
| `Content-Type` | `text/event-stream` | 标识 SSE 流 |
|
|
|
|
|
|
| `Cache-Control` | `no-cache` | 禁用缓存 |
|
|
|
|
|
|
| `X-Accel-Buffering` | `no` | 禁用 nginx 代理缓冲 |
|
|
|
|
|
|
|
|
|
|
|
|
### StreamProject — 工程级流
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
GET /api/v1/projects/:projectId/stream
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- 使用 `SubscribeAll()` 订阅全局事件
|
|
|
|
|
|
- 在 Handler 层按 `event.ProjectID != projectID` 过滤
|
|
|
|
|
|
- 工程级流不会因单个任务完成而关闭,持续监听新任务
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-03 10:30:42 +08:00
|
|
|
|
## 数据流全景
|
2026-06-03 10:12:49 +08:00
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
|
sequenceDiagram
|
|
|
|
|
|
participant P as Pipeline
|
|
|
|
|
|
participant DB as Database
|
|
|
|
|
|
participant EB as EventBus
|
|
|
|
|
|
participant SSE as SSE Handler
|
|
|
|
|
|
participant B as Browser
|
|
|
|
|
|
|
|
|
|
|
|
P->>DB: updateTaskInDB(status, progress)
|
|
|
|
|
|
P->>EB: Publish(taskID, event)
|
|
|
|
|
|
EB->>SSE: ch <- event
|
|
|
|
|
|
SSE->>B: data: {"status":"running","progress":45}
|
|
|
|
|
|
Note over B: EventSource.onmessage()
|
|
|
|
|
|
P->>DB: updateTaskInDB(completed)
|
|
|
|
|
|
P->>EB: Publish(taskID, terminal event)
|
|
|
|
|
|
EB->>SSE: ch <- event
|
|
|
|
|
|
SSE->>B: data: {"status":"completed","progress":100}
|
|
|
|
|
|
Note over SSE: 终态,关闭连接
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-03 10:30:42 +08:00
|
|
|
|
## 容错设计
|
2026-06-03 10:12:49 +08:00
|
|
|
|
|
|
|
|
|
|
| 场景 | 处理方式 |
|
|
|
|
|
|
|------|---------|
|
|
|
|
|
|
| 慢消费者 | Buffer 满时丢弃事件,Pipeline 不阻塞 |
|
|
|
|
|
|
| 客户端断开 | `c.Request.Context().Done()` 触发,自动 Unsubscribe |
|
|
|
|
|
|
| 终态到达 | completed/failed 后自动关闭 SSE 连接 |
|
|
|
|
|
|
| 无订阅者 | Publish 静默返回,不报错 |
|
|
|
|
|
|
| Broker 关闭 | Close() 关闭所有 channel,SSE 循环退出 |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-03 10:30:42 +08:00
|
|
|
|
## 关联文档
|
2026-06-03 10:12:49 +08:00
|
|
|
|
|
2026-06-03 10:30:42 +08:00
|
|
|
|
- [[05-生成管线]] — Pipeline 中的进度回调
|
|
|
|
|
|
- [[07-可观测性]] — SSE 连接的监控
|
|
|
|
|
|
- [[10-中间件链]] — SSE 端点的中间件配置
|