touka/docs/sse.md
WJQSERVER 2e55db4e2d docs: add comprehensive documentation and fix SSE graceful shutdown
- 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>
2026-02-18 15:52:42 +00:00

4.5 KiB
Raw Permalink Blame History

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)

如果您需要更高级的并发控制(例如从多个异步源接收数据),可以使用通道模式。

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 取消信号

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 的新版本中,我们通过 BaseContextEngine 的关闭信号注入到了每个请求的 Context 中。这意味着:

  1. 当服务器收到关闭信号时,engine.shutdownCtx 会被取消。
  2. 随后,所有活跃请求的 c.Request.Context() 也会收到取消信号。
  3. 您的 SSE 处理器中的 case <-c.Request.Context().Done(): 会立即触发,从而优雅地结束连接。

注意: 请务必使用 RunShutdownRunTLSRunTLSRedir 来启动服务器,以便框架能自动管理这些信号。