touka/serve.go
2025-06-04 20:44:23 +08:00

238 lines
7.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package touka
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/fenthope/reco"
)
const defaultShutdownTimeout = 5 * time.Second // 定义默认的优雅关闭超时时间
// resolveAddress 辅助函数,处理传入的地址参数。
func resolveAddress(addr []string) string {
switch len(addr) {
case 0:
return ":8080" // 默认端口
case 1:
return addr[0]
default:
panic("too many parameters for Run method") // 参数过多则报错
}
}
// Run 启动 HTTP 服务器。
// 接受一个可选的地址参数,如果未提供则默认为 ":8080"。
func (engine *Engine) Run(addr ...string) (err error) {
address := resolveAddress(addr) // 解析服务器地址
log.Printf("Touka server listening on %s\n", address)
err = http.ListenAndServe(address, engine) // 启动 HTTP 服务器
return
}
// getShutdownTimeout 解析可选的超时参数,如果未提供或无效,则返回默认超时。
func getShutdownTimeout(timeouts []time.Duration) time.Duration {
var timeout time.Duration
if len(timeouts) > 0 {
timeout = timeouts[0]
if timeout <= 0 {
log.Printf("Warning: Provided shutdown timeout (%v) is non-positive. Using default timeout %v.\n", timeout, defaultShutdownTimeout)
timeout = defaultShutdownTimeout
}
} else {
timeout = defaultShutdownTimeout
}
return timeout
}
// handleGracefulShutdown 处理一个或多个 http.Server 实例的优雅关闭。
// 它监听操作系统信号,并在指定超时时间内尝试关闭所有服务器。
func handleGracefulShutdown(servers []*http.Server, timeout time.Duration, logger *reco.Logger) error {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down Touka server(s)...")
go func() {
log.Println("Touka Logger Clossing...")
CloseLogger(logger)
}()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var wg sync.WaitGroup
var errs []error
var errsMutex sync.Mutex // 保护 errs 切片
for _, srv := range servers {
srv := srv // capture loop variable
wg.Add(1)
go func() {
defer wg.Done()
if err := srv.Shutdown(ctx); err != nil {
errsMutex.Lock()
if err == context.DeadlineExceeded {
log.Printf("Server %s shutdown timed out after %v.\n", srv.Addr, timeout)
errs = append(errs, fmt.Errorf("server %s shutdown timed out", srv.Addr))
} else {
log.Printf("Server %s forced to shutdown: %v\n", srv.Addr, err)
errs = append(errs, fmt.Errorf("server %s forced to shutdown: %w", srv.Addr, err))
}
errsMutex.Unlock()
}
}()
}
wg.Wait() // 等待所有服务器的关闭 Goroutine 完成
if len(errs) > 0 {
return errors.Join(errs...) // 返回所有收集到的错误
}
log.Println("Touka server(s) exited gracefully.")
return nil
}
// RunShutdown 启动 HTTP 服务器并支持优雅关闭。
// 它监听操作系统信号 (SIGINT, SIGTERM),并在指定超时时间内优雅地关闭服务器。
// addr: 服务器监听的地址,例如 ":8080"。
// timeouts: 可选的超时时间,如果未提供,则默认为 5 秒。
func (engine *Engine) RunShutdown(addr string, timeouts ...time.Duration) error {
timeout := getShutdownTimeout(timeouts)
srv := &http.Server{
Addr: addr,
Handler: engine, // Engine 实现了 http.Handler 接口
}
// 启动服务器在单独的 Goroutine 中运行,以便主 Goroutine 可以监听信号
go func() {
log.Printf("Touka HTTP server listening on %s\n", addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Touka HTTP server listen error: %s\n", err)
}
}()
return handleGracefulShutdown([]*http.Server{srv}, timeout, engine.LogReco)
}
// RunWithTLS 启动 HTTPS 服务器并支持优雅关闭。
// 用户需自行创建并传入 *tls.Config 实例,以提供完整的 TLS 配置自由度。
// addr: 服务器监听的地址,例如 ":8443"。
// tlsConfig: 包含 TLS 证书、密钥及其他配置的 tls.Config 实例。
// timeouts: 可选的超时时间,如果未提供,则默认为 5 秒。
func (engine *Engine) RunWithTLS(addr string, tlsConfig *tls.Config, timeouts ...time.Duration) error {
if tlsConfig == nil {
return errors.New("tls.Config must not be nil for RunWithTLS")
}
timeout := getShutdownTimeout(timeouts)
srv := &http.Server{
Addr: addr,
Handler: engine,
TLSConfig: tlsConfig, // 使用用户传入的 tls.Config
}
if engine.useDefaultProtocols {
//加入HTTP2支持
engine.SetProtocols(&ProtocolsConfig{
Http1: true,
Http2: true, // 默认启用 HTTP/2
Http2_Cleartext: false,
})
}
go func() {
log.Printf("Touka HTTPS server listening on %s\n", addr)
if err := srv.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
log.Fatalf("Touka HTTPS server listen error: %s\n", err)
}
}()
return handleGracefulShutdown([]*http.Server{srv}, timeout, engine.LogReco)
}
// RunWithTLSRedir 启动 HTTP 和 HTTPS 服务器,并将所有 HTTP 请求重定向到 HTTPS。
// httpAddr: HTTP 服务器监听的地址,例如 ":80"。
// httpsAddr: HTTPS 服务器监听的地址,例如 ":443"。
// tlsConfig: 包含 TLS 证书、密钥及其他配置的 tls.Config 实例,用于 HTTPS 服务器。
// timeouts: 可选的超时时间,如果未提供,则默认为 5 秒。
func (engine *Engine) RunWithTLSRedir(httpAddr, httpsAddr string, tlsConfig *tls.Config, timeouts ...time.Duration) error {
if tlsConfig == nil {
return errors.New("tls.Config must not be nil for RunWithTLSRedir")
}
timeout := getShutdownTimeout(timeouts)
// HTTPS Server
httpsSrv := &http.Server{
Addr: httpsAddr,
Handler: engine,
TLSConfig: tlsConfig, // 使用用户传入的 tls.Config
}
if engine.useDefaultProtocols {
//加入HTTP2支持
engine.SetProtocols(&ProtocolsConfig{
Http1: true,
Http2: true, // 默认启用 HTTP/2
Http2_Cleartext: false,
})
}
// HTTP Server for redirection
httpSrv := &http.Server{
Addr: httpAddr,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 从 r.Host 提取 hostname例如 "localhost:8080" -> "localhost"
hostOnly, _, err := net.SplitHostPort(r.Host)
if err != nil { // r.Host 可能没有端口,例如 "example.com"
hostOnly = r.Host
}
// 从 httpsAddr 提取目标 HTTPS 端口,例如 ":443" -> "443"
_, targetHttpsPort, err := net.SplitHostPort(httpsAddr)
if err != nil { // httpsAddr 必须包含一个有效的端口
log.Fatalf("Error: Invalid HTTPS address '%s' for redirection. Must specify a port (e.g., ':443').", httpsAddr)
}
var redirectHost string
if targetHttpsPort == "443" {
redirectHost = hostOnly // 如果是默认 HTTPS 端口,则无需在 URL 中显式指定端口
} else {
redirectHost = net.JoinHostPort(hostOnly, targetHttpsPort) // 否则,显式指定端口
}
// 构建目标 HTTPS URL
targetURL := "https://" + redirectHost + r.URL.RequestURI()
http.Redirect(w, r, targetURL, http.StatusMovedPermanently) // 301 Permanent Redirect
}),
}
// Start HTTPS server
go func() {
log.Printf("Touka HTTPS server listening on %s\n", httpsAddr)
if err := httpsSrv.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { // 同样,传入空字符串
log.Fatalf("Touka HTTPS server listen error: %s\n", err)
}
}()
// Start HTTP redirect server
go func() {
log.Printf("Touka HTTP redirect server listening on %s\n", httpAddr)
if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Touka HTTP redirect server listen error: %s\n", err)
}
}()
return handleGracefulShutdown([]*http.Server{httpsSrv, httpSrv}, timeout, engine.LogReco)
}