mirror of
https://github.com/infinite-iroha/touka.git
synced 2026-02-03 08:51:11 +08:00
update serve && add custom srv configure
This commit is contained in:
parent
896182417f
commit
803a6747f6
2 changed files with 329 additions and 280 deletions
343
serve.go
343
serve.go
|
|
@ -17,222 +17,247 @@ import (
|
|||
"github.com/fenthope/reco"
|
||||
)
|
||||
|
||||
const defaultShutdownTimeout = 5 * time.Second // 定义默认的优雅关闭超时时间
|
||||
// defaultShutdownTimeout 定义了在强制关闭前等待优雅关闭的最长时间
|
||||
const defaultShutdownTimeout = 5 * time.Second
|
||||
|
||||
// resolveAddress 辅助函数,处理传入的地址参数。
|
||||
// --- 内部辅助函数 ---
|
||||
|
||||
// resolveAddress 解析传入的地址参数,如果没有则返回默认的 ":8080"
|
||||
func resolveAddress(addr []string) string {
|
||||
switch len(addr) {
|
||||
case 0:
|
||||
return ":8080" // 默认端口
|
||||
return ":8080"
|
||||
case 1:
|
||||
return addr[0]
|
||||
default:
|
||||
panic("too many parameters for Run method") // 参数过多则报错
|
||||
panic("too many parameters provided for server address")
|
||||
}
|
||||
}
|
||||
|
||||
// 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 解析可选的超时参数,如果未提供或无效,则返回默认超时。
|
||||
// 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
|
||||
if len(timeouts) > 0 && timeouts[0] > 0 {
|
||||
return timeouts[0]
|
||||
}
|
||||
return timeout
|
||||
return defaultShutdownTimeout
|
||||
}
|
||||
|
||||
// handleGracefulShutdown 处理一个或多个 http.Server 实例的优雅关闭。
|
||||
// 它监听操作系统信号,并在指定超时时间内尝试关闭所有服务器。
|
||||
// runServer 是一个内部辅助函数,负责在一个新的 goroutine 中启动一个 http.Server,
|
||||
// 并处理其启动失败的致命错误
|
||||
// serverType 用于在日志中标识服务器类型 (例如 "HTTP", "HTTPS")
|
||||
func runServer(serverType string, srv *http.Server) {
|
||||
go func() {
|
||||
var err error
|
||||
protocol := "http"
|
||||
if srv.TLSConfig != nil {
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
log.Printf("Touka %s server listening on %s://%s", serverType, protocol, srv.Addr)
|
||||
|
||||
if srv.TLSConfig != nil {
|
||||
// 对于 HTTPS 服务器,如果 srv.TLSConfig.Certificates 已配置,
|
||||
// ListenAndServeTLS 的前两个参数可以为空字符串
|
||||
err = srv.ListenAndServeTLS("", "")
|
||||
} else {
|
||||
err = srv.ListenAndServe()
|
||||
}
|
||||
|
||||
// 如果服务器停止不是因为被优雅关闭 (http.ErrServerClosed),
|
||||
// 则认为是一个严重错误,并终止程序
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatalf("Touka %s server failed: %v", serverType, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// handleGracefulShutdown 监听系统信号 (SIGINT, SIGTERM) 并优雅地关闭所有提供的服务器
|
||||
// 这是所有支持优雅关闭的 RunXXX 方法的最终归宿
|
||||
func handleGracefulShutdown(servers []*http.Server, timeout time.Duration, logger *reco.Logger) error {
|
||||
// 创建一个 channel 来接收操作系统信号
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 监听中断和终止信号
|
||||
<-quit // 阻塞,直到接收到上述信号之一
|
||||
log.Println("Shutting down Touka server(s)...")
|
||||
|
||||
go func() {
|
||||
log.Println("Touka Logger Clossing...")
|
||||
CloseLogger(logger)
|
||||
}()
|
||||
// 关闭日志记录器
|
||||
if logger != nil {
|
||||
go func() {
|
||||
log.Println("Closing Touka logger...")
|
||||
CloseLogger(logger)
|
||||
}()
|
||||
}
|
||||
|
||||
// 创建一个带超时的上下文,用于 Shutdown
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var errs []error
|
||||
var errsMutex sync.Mutex // 保护 errs 切片
|
||||
errChan := make(chan error, len(servers)) // 用于收集关闭错误的 channel
|
||||
|
||||
// 并发地关闭所有服务器
|
||||
for _, srv := range servers {
|
||||
srv := srv // capture loop variable
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
go func(s *http.Server) {
|
||||
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()
|
||||
if err := s.Shutdown(ctx); err != nil {
|
||||
// 将错误发送到 channel
|
||||
errChan <- fmt.Errorf("server on %s shutdown failed: %w", s.Addr, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait() // 等待所有服务器的关闭 Goroutine 完成
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...) // 返回所有收集到的错误
|
||||
}(srv)
|
||||
}
|
||||
|
||||
wg.Wait() // 等待所有服务器的关闭 goroutine 完成
|
||||
close(errChan) // 关闭 channel,以便可以安全地遍历它
|
||||
|
||||
// 收集所有关闭过程中发生的错误
|
||||
var shutdownErrors []error
|
||||
for err := range errChan {
|
||||
shutdownErrors = append(shutdownErrors, err)
|
||||
log.Printf("Shutdown error: %v", err)
|
||||
}
|
||||
|
||||
if len(shutdownErrors) > 0 {
|
||||
return errors.Join(shutdownErrors...) // Go 1.20+ 的 errors.Join,用于合并多个错误
|
||||
}
|
||||
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)
|
||||
// --- 公共 Run 方法 ---
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: engine, // Engine 实现了 http.Handler 接口
|
||||
// Run 启动一个不支持优雅关闭的 HTTP 服务器
|
||||
// 这是一个阻塞调用,主要用于简单的场景或快速测试
|
||||
// 建议在生产环境中使用 RunShutdown 或其他支持优雅关闭的方法
|
||||
func (engine *Engine) Run(addr ...string) error {
|
||||
address := resolveAddress(addr)
|
||||
srv := &http.Server{Addr: address, Handler: engine}
|
||||
|
||||
// 即使是不支持优雅关闭的 Run,也应用默认和用户配置,以保持行为一致性
|
||||
//engine.applyDefaultServerConfig(srv)
|
||||
if engine.ServerConfigurator != nil {
|
||||
engine.ServerConfigurator(srv)
|
||||
}
|
||||
|
||||
// 启动服务器在单独的 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)
|
||||
log.Printf("Starting Touka HTTP server on %s (no graceful shutdown)", address)
|
||||
return srv.ListenAndServe()
|
||||
}
|
||||
|
||||
// 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")
|
||||
// RunShutdown 启动一个支持优雅关闭的 HTTP 服务器
|
||||
func (engine *Engine) RunShutdown(addr string, timeouts ...time.Duration) error {
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: engine,
|
||||
}
|
||||
|
||||
// 应用框架的默认配置和用户提供的自定义配置
|
||||
//engine.applyDefaultServerConfig(srv)
|
||||
if engine.ServerConfigurator != nil {
|
||||
engine.ServerConfigurator(srv)
|
||||
}
|
||||
|
||||
runServer("HTTP", srv)
|
||||
return handleGracefulShutdown([]*http.Server{srv}, getShutdownTimeout(timeouts), engine.LogReco)
|
||||
}
|
||||
|
||||
// RunTLS 启动一个支持优雅关闭的 HTTPS 服务器
|
||||
func (engine *Engine) RunTLS(addr string, tlsConfig *tls.Config, timeouts ...time.Duration) error {
|
||||
if tlsConfig == nil {
|
||||
return errors.New("tls.Config must not be nil for RunTLS")
|
||||
}
|
||||
|
||||
// 配置 HTTP/2 支持 (如果使用默认配置)
|
||||
if engine.useDefaultProtocols {
|
||||
engine.SetProtocols(&ProtocolsConfig{
|
||||
Http1: true,
|
||||
Http2: true, // 默认在 TLS 上启用 HTTP/2
|
||||
})
|
||||
}
|
||||
timeout := getShutdownTimeout(timeouts)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: engine,
|
||||
TLSConfig: tlsConfig, // 使用用户传入的 tls.Config
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
|
||||
if engine.useDefaultProtocols {
|
||||
//加入HTTP2支持
|
||||
engine.SetProtocols(&ProtocolsConfig{
|
||||
Http1: true,
|
||||
Http2: true, // 默认启用 HTTP/2
|
||||
Http2_Cleartext: false,
|
||||
})
|
||||
// 应用框架的默认配置和用户提供的自定义配置
|
||||
// 优先使用 TLSServerConfigurator,如果未设置,则回退到通用的 ServerConfigurator
|
||||
//engine.applyDefaultServerConfig(srv)
|
||||
if engine.TLSServerConfigurator != nil {
|
||||
engine.TLSServerConfigurator(srv)
|
||||
} else if engine.ServerConfigurator != nil {
|
||||
engine.ServerConfigurator(srv)
|
||||
}
|
||||
|
||||
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)
|
||||
runServer("HTTPS", srv)
|
||||
return handleGracefulShutdown([]*http.Server{srv}, getShutdownTimeout(timeouts), 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)
|
||||
// RunWithTLS 是 RunTLS 的别名,为了保持向后兼容性或更直观的命名
|
||||
func (engine *Engine) RunWithTLS(addr string, tlsConfig *tls.Config, timeouts ...time.Duration) error {
|
||||
return engine.RunTLS(addr, tlsConfig, timeouts...)
|
||||
}
|
||||
|
||||
// HTTPS Server
|
||||
// RunTLSRedir 启动 HTTP 重定向服务器和 HTTPS 应用服务器,两者都支持优雅关闭
|
||||
func (engine *Engine) RunTLSRedir(httpAddr, httpsAddr string, tlsConfig *tls.Config, timeouts ...time.Duration) error {
|
||||
if tlsConfig == nil {
|
||||
return errors.New("tls.Config must not be nil for RunTLSRedir")
|
||||
}
|
||||
|
||||
// --- HTTPS 服务器 ---
|
||||
if engine.useDefaultProtocols {
|
||||
engine.SetProtocols(&ProtocolsConfig{Http1: true, Http2: true})
|
||||
}
|
||||
httpsSrv := &http.Server{
|
||||
Addr: httpsAddr,
|
||||
Handler: engine,
|
||||
TLSConfig: tlsConfig, // 使用用户传入的 tls.Config
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
//engine.applyDefaultServerConfig(httpsSrv)
|
||||
if engine.TLSServerConfigurator != nil {
|
||||
engine.TLSServerConfigurator(httpsSrv)
|
||||
} else if engine.ServerConfigurator != nil {
|
||||
engine.ServerConfigurator(httpsSrv)
|
||||
}
|
||||
|
||||
if engine.useDefaultProtocols {
|
||||
//加入HTTP2支持
|
||||
engine.SetProtocols(&ProtocolsConfig{
|
||||
Http1: true,
|
||||
Http2: true, // 默认启用 HTTP/2
|
||||
Http2_Cleartext: false,
|
||||
})
|
||||
}
|
||||
// --- HTTP 重定向服务器 ---
|
||||
redirectHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
host, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
host = r.Host
|
||||
}
|
||||
|
||||
// HTTP Server for redirection
|
||||
_, httpsPort, err := net.SplitHostPort(httpsAddr)
|
||||
if err != nil {
|
||||
// 如果 httpsAddr 没有端口,这是一个配置错误
|
||||
|
||||
log.Fatalf("Invalid HTTPS address for redirection '%s': must include a port.", httpsAddr)
|
||||
}
|
||||
|
||||
targetURL := "https://" + host
|
||||
// 只有在非标准 HTTPS 端口 (443) 时才附加端口号
|
||||
if httpsPort != "443" {
|
||||
targetURL = "https://" + net.JoinHostPort(host, httpsPort)
|
||||
}
|
||||
targetURL += r.URL.RequestURI()
|
||||
|
||||
http.Redirect(w, r, targetURL, http.StatusMovedPermanently)
|
||||
})
|
||||
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
|
||||
}),
|
||||
Addr: httpAddr,
|
||||
Handler: redirectHandler,
|
||||
}
|
||||
//engine.applyDefaultServerConfig(httpSrv)
|
||||
if engine.ServerConfigurator != nil {
|
||||
engine.ServerConfigurator(httpSrv)
|
||||
}
|
||||
|
||||
// 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)
|
||||
// --- 启动服务器和优雅关闭 ---
|
||||
runServer("HTTPS", httpsSrv)
|
||||
runServer("HTTP Redirect", httpSrv)
|
||||
return handleGracefulShutdown([]*http.Server{httpsSrv, httpSrv}, getShutdownTimeout(timeouts), engine.LogReco)
|
||||
}
|
||||
|
||||
// RunWithTLSRedir 是 RunTLSRedir 的别名,为了保持向后兼容性
|
||||
func (engine *Engine) RunWithTLSRedir(httpAddr, httpsAddr string, tlsConfig *tls.Config, timeouts ...time.Duration) error {
|
||||
return engine.RunTLSRedir(httpAddr, httpsAddr, tlsConfig, timeouts...)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue