touka/docs/advanced.md
wjqserver e2cf08d5dd feat: add redirect host selection options
Support explicit redirect host source selection for HTTP-to-HTTPS redirects with ordered header lookup, fixed host mode, and strict validation. Document the new redirect option relationships and add focused tests for 426 fallback, conflict checks, and non-graceful startup errors.
2026-04-07 19:49:13 +08:00

12 KiB
Raw Blame History

高级特性与优化

本章节涵盖了 Touka 的一些深层特性以及在生产环境中的最佳实践。

性能优化

1. Context 池化

Touka 使用 sync.Pool 来重用 touka.Context 对象。这极大减少了每个请求产生的内存分配和 GC 压力。

  • 代价: 您必须在处理器返回后立即停止对该 Context 指针的任何引用。
  • 解决方案: 如果需要在后台 Goroutine 中使用请求数据,请预先提取所需数据(如 c.Query 的值),或者深拷贝该对象(不推荐)。

2. 预分配参数切片

在路由匹配过程中Touka 会预分配路径参数切片,并根据路由深度进行缓存,从而在路由查找时实现几乎零分配。

服务器配置

服务器配置器 (ServerConfigurator)

Touka 允许您在服务器启动前对底层 *http.Server 进行自定义配置:

r := touka.New()

// 配置 HTTP 服务器
r.SetServerConfigurator(func(server *http.Server) {
    server.ReadTimeout = 30 * time.Second
    server.WriteTimeout = 30 * time.Second
    server.IdleTimeout = 120 * time.Second
    server.MaxHeaderBytes = 1 << 20 // 1MB
})

// 专门配置 HTTPS 服务器(优先级高于 ServerConfigurator
r.SetTLSServerConfigurator(func(server *http.Server) {
    server.ReadTimeout = 30 * time.Second
    server.WriteTimeout = 30 * time.Second
    // HTTPS 特定配置...
})

协议配置

Touka 支持配置 HTTP/1.1、HTTP/2 和 H2CHTTP/2 Cleartext

// 使用默认协议配置
// 普通 HTTP 启动时默认为 HTTP/1.1;若使用 WithTLS(...) 且未手动覆盖协议集,
// HTTPS 服务器会默认启用 HTTP/1.1 与 HTTP/2。
r.SetDefaultProtocols()

// 自定义协议配置
r.SetProtocols(&touka.ProtocolsConfig{
    Http1:           true,  // 启用 HTTP/1.1
    Http2:           true,  // 启用 HTTP/2需要 TLS
    Http2_Cleartext: true,  // 启用 H2C无需 TLS 的 HTTP/2
})

启动方式

Touka 统一通过 Run(opts...) 启动服务器:

// 1. 简单启动(无优雅停机)
r.Run(touka.WithAddr(":8080"))

// 2. 带优雅停机的启动
r.Run(touka.WithAddr(":8080"), touka.WithGracefulShutdown(10*time.Second))

// 3. 带上下文的优雅停机
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
r.Run(
    touka.WithAddr(":8080"),
    touka.WithGracefulShutdown(10*time.Second),
    touka.WithShutdownContext(ctx),
)

// 4. HTTPS 启动
tlsConfig := &tls.Config{
    MinVersion: tls.VersionTLS12,
    // 其他 TLS 配置...
}
// WithTLS(...) 与优雅关闭相互独立;这里演示 HTTPS + 默认优雅关闭超时。
r.Run(
    touka.WithAddr(":443"),
    touka.WithTLS(tlsConfig),
    touka.WithGracefulShutdownDefault(),
)

// 5. HTTPS + HTTP 重定向
// WithHTTPRedirect(...) 需要与 WithTLS(...) 配合使用。
r.Run(
    touka.WithAddr(":443"),
    touka.WithTLS(tlsConfig),
    touka.WithHTTPRedirect(":80"),
    touka.WithGracefulShutdown(10*time.Second),
)

// 6. HTTPS + HTTP 重定向(按 header 顺序决定跳转 host
r.Run(
    touka.WithAddr(":443"),
    touka.WithTLS(tlsConfig),
    touka.WithHTTPRedirect(
        ":80",
        touka.WithUseHeaderHost(true),
        touka.WithRedirectHostHeaders([]string{"X-Forwarded-Host", "X-Original-Host"}),
    ),
)

// 7. HTTPS + HTTP 重定向(固定跳转到配置的 host
r.Run(
    touka.WithAddr(":443"),
    touka.WithTLS(tlsConfig),
    touka.WithHTTPRedirect(
        ":80",
        touka.WithUseHeaderHost(false),
        touka.WithRedirectHost("example.com"),
    ),
)

HTTPS Redirect Host 策略

WithHTTPRedirect(addr, opts...) 除了开启 HTTP -> HTTPS 重定向外,还支持通过 redirect 子选项控制最终跳转目标的 host。

可用的 redirect 子选项:

  • touka.WithUseHeaderHost(true|false)
  • touka.WithRedirectHostHeaders([]string{...})
  • touka.WithRedirectHost("example.com")

模式一:使用请求输入侧的 host

WithUseHeaderHost(true) 时:

  • 如果没有配置 WithRedirectHostHeaders(...),使用 Request.Host
  • 如果配置了 WithRedirectHostHeaders(...),按给定顺序读取这些 header并使用第一个非空值
  • 如果配置了 WithRedirectHostHeaders(...) 但所有 header 都为空,返回 426 Upgrade Required

示例:

r.Run(
    touka.WithAddr(":443"),
    touka.WithTLS(tlsConfig),
    touka.WithHTTPRedirect(
        ":80",
        touka.WithUseHeaderHost(true),
        touka.WithRedirectHostHeaders([]string{"X-Forwarded-Host", "X-Original-Host"}),
    ),
)

模式二:使用配置的固定 host

WithUseHeaderHost(false) 时:

  • 不读取 Request.Host
  • 不读取 WithRedirectHostHeaders(...)
  • 必须配置 WithRedirectHost("example.com")

示例:

r.Run(
    touka.WithAddr(":443"),
    touka.WithTLS(tlsConfig),
    touka.WithHTTPRedirect(
        ":80",
        touka.WithUseHeaderHost(false),
        touka.WithRedirectHost("example.com"),
    ),
)

严格校验规则

以下组合会直接返回配置错误:

  • WithHTTPRedirect(...) 但没有 WithTLS(...)
  • 配置了 WithRedirectHostHeaders(...),但没有显式传入 WithUseHeaderHost(true)
  • WithUseHeaderHost(false) 但没有配置 WithRedirectHost(...)
  • WithUseHeaderHost(false) 同时配置了 WithRedirectHostHeaders(...)
  • WithUseHeaderHost(true) 同时配置了 WithRedirectHost(...)

优先级关系

  1. 是否启用 WithHTTPRedirect(...) 决定是否进入 HTTPS + redirect 模式
  2. WithUseHeaderHost(...) 决定 host 来源模式
  3. WithUseHeaderHost(true) 时:
    • 配置了 WithRedirectHostHeaders(...) 就按 header 顺序查询
    • 未配置时使用 Request.Host
  4. WithUseHeaderHost(false) 时:
    • 只使用 WithRedirectHost(...)

注意: WithRedirectHostHeaders(...) 读取的是普通请求头值。只有在您明确知道请求经过受信任代理并会正确填充这些 header 时,才建议启用它。

优雅停机 (Graceful Shutdown)

在部署新版本时我们希望服务器停止接收新请求但能处理完当前正在进行的请求。启用优雅关闭后Touka 会监听 SIGINT/SIGTERM,并在关闭时取消活动请求的上下文。

r := touka.Default()
// ... 注册路由 ...

// 监听 SIGINT 和 SIGTERM 信号
// 如果在 10 秒内未处理完,则强制关闭
if err := r.Run(touka.WithAddr(":8080"), touka.WithGracefulShutdown(10*time.Second)); err != nil {
    log.Fatal("服务器退出异常:", err)
}

SSE 长连接的优雅关闭

对于 SSE 等长连接场景Touka 会自动将引擎的关闭信号注入到请求的 Context 中:

r.GET("/events", func(c *touka.Context) {
    c.EventStream(func(w io.Writer) bool {
        select {
        case <-c.Request.Context().Done():
            // 收到关闭信号,优雅退出
            return false
        case <-time.After(1 * time.Second):
            // 发送数据
            event := touka.Event{Data: "tick"}
            event.Render(w)
            return true
        }
    })
})

路由行为配置

r := touka.New()

// 是否自动重定向尾部斜杠(默认 true
// /foo/ -> /foo 或 /foo -> /foo/
r.SetRedirectTrailingSlash(true)

// 是否自动修复路径大小写(默认 true
// /FOO -> /foo
r.SetRedirectFixedPath(true)

// 是否处理 405 Method Not Allowed默认 true
// 当路径匹配但方法不匹配时返回 405 而非 404
r.SetHandleMethodNotAllowed(true)

自定义 404 处理

// 单个处理器
r.NoRoute(func(c *touka.Context) {
    c.JSON(http.StatusNotFound, touka.H{
        "error": "Page not found",
        "path":  c.Request.URL.Path,
    })
})

// 处理器链(可以在 404 前执行额外中间件)
r.NoRoutes(
    LogNotFoundMiddleware(),
    func(c *touka.Context) {
        c.JSON(http.StatusNotFound, touka.H{"error": "Not found"})
    },
)

未匹配路径作为静态文件服务

// 当没有路由匹配时,尝试从文件系统中查找文件
// 非常适合单页应用SPA部署
r.SetUnMatchFS(http.Dir("./frontend/dist"))

// 也可以添加额外的中间件
r.SetUnMatchFS(http.Dir("./frontend/dist"), AuthMiddleware())

IP 地址解析配置

在反向代理环境中,正确配置 IP 解析非常重要:

r := touka.New()

// 是否信任代理头部获取客户端 IP默认 true
r.SetForwardByClientIP(true)

// 设置用于获取客户端 IP 的头部列表(按优先级排序)
r.SetRemoteIPHeaders([]string{
    "X-Forwarded-For",
    "X-Real-IP",
    "CF-Connecting-IP", // Cloudflare
})

如果您同时使用 Touka 的 ReverseProxy 把请求继续转发给其他后端,请再参考 docs/reverse-proxy.md 中关于 ForwardedX-Forwarded-*Via 的说明。前者解决“当前请求的客户端 IP 如何被 Touka 正确解析”,后者解决“代理后的请求如何把链路信息继续传给下一跳”。

请求体大小限制

为了防止恶意的大数据包攻击(如慢速 HTTP 攻击或内存溢出Touka 内置了请求体大小限制机制。

全局限制

// 设置全局最大请求体大小(例如 10MB
r.SetGlobalMaxRequestBodySize(10 << 20)

单个请求限制

r.POST("/upload", func(c *touka.Context) {
    // 为特定请求设置限制(覆盖全局设置)
    c.SetMaxRequestBodySize(100 << 20) // 100MB

    body, err := c.GetReqBodyFull()
    if err != nil {
        // 如果超过限制,会返回 ErrBodyTooLarge
        if errors.Is(err, touka.ErrBodyTooLarge) {
            c.ErrorUseHandle(http.StatusRequestEntityTooLarge, err)
            return
        }
        c.ErrorUseHandle(http.StatusBadRequest, err)
        return
    }
    // 处理 body...
})

与标准库集成

Touka 遵循 net/http 哲学。您可以方便地使用现有的标准库组件。

适配 http.HandlerFunc

r.GET("/pprof/*any", touka.AdapterStdFunc(pprof.Index))

适配 http.Handler

// 适配 http.FileServer
fileServer := http.FileServer(http.Dir("./static"))
r.GET("/static/*filepath", touka.AdapterStdHandle(http.StripPrefix("/static", fileServer)))

手动注入

由于 Engine 实现了 http.Handler 接口,您可以将其挂载到任何地方。

s := &http.Server{
    Addr:           ":8080",
    Handler:        r, // Engine 实例
    ReadTimeout:    10 * time.Second,
    WriteTimeout:   10 * time.Second,
    MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()

自定义日志集成

Touka 默认集成了 reco 日志库。您可以自定义其输出行为。

logConfig := reco.Config{
    Level:      reco.LevelInfo,
    Mode:       reco.ModeText, // 或 reco.ModeJSON
    Output:     os.Stdout,
    Async:      true, // 异步写入提高性能
    TimeFormat: time.RFC3339,
}
r.SetLoggerCfg(logConfig)

// 或直接传入日志实例
logger, _ := reco.New(logConfig)
r.SetLogger(logger)

// 关闭日志(在服务器关闭时)
defer r.CloseLogger()

HTTP 客户端配置

Touka 内置了 httpc HTTP 客户端,可以在请求处理中方便地发起出站请求:

// 创建自定义 HTTP 客户端
customClient := httpc.New()
r.SetHTTPClient(customClient)

// 在处理器中使用
r.GET("/proxy", func(c *touka.Context) {
    resp, err := c.GetHTTPC().Get("https://api.example.com/data")
    // ...
})

条件中间件

Touka 支持根据条件动态启用或禁用中间件:

// 单个条件中间件
r.Use(r.UseIf(config.EnableLogging, AccessLoggerMiddleware()))

// 条件中间件链
r.Use(r.UseChainIf(config.EnableMetrics,
    MetricsMiddleware,
    PrometheusMiddleware,
    MonitoringMiddleware,
))

获取路由信息

// 获取所有已注册的路由信息
routes := r.GetRouterInfo()
for _, route := range routes {
    fmt.Printf("Method: %s, Path: %s, Handler: %s, Group: %s\n",
        route.Method, route.Path, route.Handler, route.Group)
}