mirror of
https://github.com/infinite-iroha/touka.git
synced 2026-06-13 23:57:38 +08:00
- Created a detailed \`docs\` directory with Chinese documentation for all major features. - Implemented \`BaseContext\` propagation in \`Engine\` to allow long-lived connections (like SSE) to receive shutdown signals. - Added \`Engine.Context()\` to provide access to the shutdown context. - Updated \`docs/sse.md\` with a guide on handling graceful shutdowns. - Cleaned up temporary build/update scripts. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
131 lines
4.5 KiB
Markdown
131 lines
4.5 KiB
Markdown
# Server-Sent Events (SSE)
|
||
|
||
Server-Sent Events 允许服务器向客户端实时推送数据。Touka 对此提供了原生且易用的支持。
|
||
|
||
## 核心结构:`Event`
|
||
|
||
`Event` 结构体代表一个 SSE 消息:
|
||
|
||
```go
|
||
type Event struct {
|
||
Event string // 事件名称
|
||
Data string // 数据内容 (支持多行)
|
||
Id string // 事件 ID
|
||
Retry string // 重连时间 (毫秒)
|
||
}
|
||
```
|
||
|
||
## 模式一:回调模式 (EventStream)
|
||
|
||
这是最推荐的使用方式,它更简单且能自动管理连接生命周期。
|
||
|
||
```go
|
||
r.GET("/events", func(c *touka.Context) {
|
||
c.EventStream(func(w io.Writer) bool {
|
||
// 构建事件
|
||
event := touka.Event{
|
||
Data: "现在的时间是: " + time.Now().Format(time.RFC3339),
|
||
}
|
||
|
||
// 渲染并写入
|
||
if err := event.Render(w); err != nil {
|
||
return false // 发生写入错误(如客户端断开),返回 false 停止流
|
||
}
|
||
|
||
time.Sleep(2 * time.Second)
|
||
return true // 返回 true 继续下一次循环
|
||
})
|
||
})
|
||
```
|
||
|
||
## 模式二:通道模式 (EventStreamChan)
|
||
|
||
如果您需要更高级的并发控制(例如从多个异步源接收数据),可以使用通道模式。
|
||
|
||
```go
|
||
r.GET("/events-chan", func(c *touka.Context) {
|
||
eventChan, errChan := c.EventStreamChan()
|
||
|
||
// 监听错误/断开连接
|
||
go func() {
|
||
if err := <-errChan; err != nil {
|
||
log.Printf("SSE 错误: %v", err)
|
||
}
|
||
}()
|
||
|
||
// 发送数据
|
||
go func() {
|
||
defer close(eventChan) // 务必在结束时关闭
|
||
|
||
for i := 0; i < 10; i++ {
|
||
select {
|
||
case <-c.Request.Context().Done():
|
||
return
|
||
default:
|
||
eventChan <- touka.Event{
|
||
Data: fmt.Sprintf("消息 #%d", i),
|
||
}
|
||
time.Sleep(1 * time.Second)
|
||
}
|
||
}
|
||
}()
|
||
})
|
||
```
|
||
|
||
## 最佳实践
|
||
|
||
1. **资源回收**: 确保在 `EventStreamChan` 模式下正确监听 `c.Request.Context().Done()` 以避免 Goroutine 泄漏。
|
||
2. **数据格式**: SSE 协议要求数据为 UTF-8。Touka 的 `Render` 方法会自动处理多行数据并加上必要的 `data:` 前缀。
|
||
3. **超时管理**: SSE 连接通常是长连接,请确保您的反向代理(如 Nginx)配置了足够大的写超时时间。
|
||
|
||
## 优雅关闭与资源清理
|
||
|
||
在长连接场景下,正确处理客户端断开或服务器关闭信号至关重要,以防止资源泄漏。
|
||
|
||
### 示例:监听 Context 取消信号
|
||
|
||
```go
|
||
r.GET("/events-graceful", func(c *touka.Context) {
|
||
// 设置响应头(如果手动处理,EventStream 会自动设置)
|
||
|
||
// 使用 Context 的 Done 通道来感知连接关闭
|
||
ctx := c.Request.Context()
|
||
|
||
// 启动一个用于模拟数据生成的循环
|
||
ticker := time.NewTicker(2 * time.Second)
|
||
defer ticker.Stop()
|
||
|
||
c.EventStream(func(w io.Writer) bool {
|
||
select {
|
||
case <-ctx.Done():
|
||
// 收到优雅关闭信号(客户端离开或服务器正在关闭)
|
||
fmt.Println("SSE 连接正在关闭,开始清理资源...")
|
||
return false // 返回 false 告知框架停止流
|
||
|
||
case t := <-ticker.C:
|
||
event := touka.Event{
|
||
Data: "Tick at " + t.Format(time.RFC3339),
|
||
}
|
||
if err := event.Render(w); err != nil {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
})
|
||
|
||
fmt.Println("SSE 连接已彻底释放")
|
||
})
|
||
```
|
||
|
||
在该示例中,我们显式地在回调函数中使用 `select` 监听 `ctx.Done()`。虽然 Touka 的 `EventStream` 内部也会检查此信号,但在回调内部自行处理可以执行更复杂的清理逻辑(如关闭数据库连接、停止特定的 Goroutine 等)。
|
||
|
||
### 为什么会出现 "context deadline exceeded"?
|
||
|
||
如果您在优雅停机时遇到 `context deadline exceeded` 错误,通常是因为 SSE 连接仍然活跃,而 `http.Server.Shutdown` 正在等待它们结束。
|
||
|
||
在 Touka 的新版本中,我们通过 `BaseContext` 将 `Engine` 的关闭信号注入到了每个请求的 `Context` 中。这意味着:
|
||
1. 当服务器收到关闭信号时,`engine.shutdownCtx` 会被取消。
|
||
2. 随后,所有活跃请求的 `c.Request.Context()` 也会收到取消信号。
|
||
3. 您的 SSE 处理器中的 `case <-c.Request.Context().Done():` 会立即触发,从而优雅地结束连接。
|
||
|
||
**注意:** 请务必使用 `RunShutdown`、`RunTLS` 或 `RunTLSRedir` 来启动服务器,以便框架能自动管理这些信号。
|