mirror of
https://github.com/infinite-iroha/touka.git
synced 2026-06-13 15:47:38 +08:00
Address PR review feedback: - Capture w := c.Writer before goroutine start, use w (not c.Writer) inside the goroutine to avoid holding *Context reference - Move channel send into select alongside context cancellation in all examples and tests, preventing goroutine leak when client disconnects while blocked on unbuffered send
4.8 KiB
4.8 KiB
Server-Sent Events (SSE)
Server-Sent Events 允许服务器向客户端实时推送数据。Touka 对此提供了原生且易用的支持。
核心结构:Event
Event 结构体代表一个 SSE 消息:
type Event struct {
Event string // 事件名称
Data string // 数据内容 (支持多行)
Id string // 事件 ID
Retry string // 重连时间 (毫秒)
}
模式一:回调模式 (EventStream)
这是最推荐的使用方式,它更简单且能自动管理连接生命周期。
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)
如果您需要更高级的并发控制(例如从多个异步源接收数据),可以使用通道模式。与回调模式类似,此方法是阻塞的:handler 会在此方法中停留,直到事件 channel 被关闭或客户端断开连接。
r.GET("/events-chan", func(c *touka.Context) {
eventChan := make(chan touka.Event)
ctx := c.Request.Context()
// 在独立的 goroutine 中发送事件.
go func() {
defer close(eventChan) // 务必在结束时关闭以结束事件流.
for i := 0; i < 10; i++ {
select {
case <-ctx.Done():
return // 客户端已断开, 退出 goroutine.
case eventChan <- touka.Event{
Data: fmt.Sprintf("消息 #%d", i),
}:
}
time.Sleep(1 * time.Second)
}
}()
// EventStreamChan 会阻塞直到流结束.
c.EventStreamChan(eventChan)
})
最佳实践
- 资源回收:
EventStreamChan是阻塞的,handler 在事件流结束前不会返回。将c.Request.Context().Done()和eventChan <- ...作为同一个select的两个分支,确保发送操作本身能够响应客户端断开。 - 关闭 Channel: 生产者完成发送后必须
close(eventChan),否则 handler 会永远阻塞。 - 数据格式: SSE 协议要求数据为 UTF-8。Touka 的
Render方法会自动处理多行数据并加上必要的data:前缀。 - 超时管理: SSE 连接通常是长连接,请确保您的反向代理(如 Nginx)配置了足够大的写超时时间。
优雅关闭与资源清理
在长连接场景下,正确处理客户端断开或服务器关闭信号至关重要,以防止资源泄漏。
示例:监听 Context 取消信号
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 中。这意味着:
- 当服务器收到关闭信号时,
engine.shutdownCtx会被取消。 - 随后,所有活跃请求的
c.Request.Context()也会收到取消信号。 - 您的 SSE 处理器中的
case <-c.Request.Context().Done():会立即触发,从而优雅地结束连接。
注意: 请务必使用 RunShutdown、RunTLS 或 RunTLSRedir 来启动服务器,以便框架能自动管理这些信号。