mirror of
https://github.com/infinite-iroha/touka.git
synced 2026-06-13 15:47:38 +08:00
BREAKING CHANGE: EventStreamChan signature changed from (chan<- Event, <-chan error) to (eventChan <-chan Event) The caller now creates and passes the channel instead of receiving it. The errChan return value is removed. The old non-blocking design allowed the handler to return before the SSE stream ended, causing ServeHTTP to return the Context to the pool while the internal goroutine was still writing to the pooled writer — a data race across requests. The new blocking design keeps the handler inside EventStreamChan until the event channel is closed or the client disconnects, ensuring the Context remains bound throughout the stream. - Caller creates channel, producer goroutine sends events - EventStreamChan blocks handler until stream ends - Internal goroutine captures stable references (Flusher, context.Context) instead of holding *Context pointer - Nil guard on Flusher type assertion - Add sse_test.go covering blocking, disconnect, and event format - Update docs/sse.md for new API
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)
// 在独立的 goroutine 中发送事件.
go func() {
defer close(eventChan) // 务必在结束时关闭以结束事件流.
for i := 0; i < 10; i++ {
select {
case <-c.Request.Context().Done():
return // 客户端已断开, 退出 goroutine.
default:
eventChan <- touka.Event{
Data: fmt.Sprintf("消息 #%d", i),
}
time.Sleep(1 * time.Second)
}
}
}()
// EventStreamChan 会阻塞直到流结束.
c.EventStreamChan(eventChan)
})
最佳实践
- 资源回收:
EventStreamChan是阻塞的,handler 在事件流结束前不会返回。请确保生产者 goroutine 在select中监听c.Request.Context().Done()以响应客户端断开。 - 关闭 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 来启动服务器,以便框架能自动管理这些信号。