自定义 HTTP 配置

Gin 的 Run() 方法虽然方便,但很多时候我们需要更精细地控制 HTTP 服务器的行为。超时时间、缓冲区大小、最大请求体这些参数,在处理大文件上传或慢客户端时特别重要。

为什么需要自定义配置

默认的 http.Server 配置比较宽松:

  • 没有读写超时,慢客户端可能耗尽连接资源
  • 请求头没有大小限制,可能被恶意请求攻击
  • 默认缓冲区可能不适合你的业务场景

生产环境需要根据实际情况调整这些参数。

基本配置方式

不使用 router.Run(),而是自己创建 http.Server

package main

import (
    "log"
    "net/http"
    "time"
    
    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()
    
    router.GET("/", func(c *gin.Context) {
        c.String(200, "Hello World")
    })

    server := &http.Server{
        Addr:         ":8080",
        Handler:      router,
        // 这里可以添加各种配置
    }

    log.Fatal(server.ListenAndServe())
}

关键配置参数

超时设置

超时是最重要的配置之一,能有效防止资源耗尽:

server := &http.Server{
    Addr:         ":8080",
    Handler:      router,
    ReadTimeout:  10 * time.Second,  // 读取请求超时
    WriteTimeout: 10 * time.Second,  // 写入响应超时
    IdleTimeout:  60 * time.Second,  // 空闲连接超时
}

各超时参数的含义:

参数说明
ReadTimeout从建立连接到读取完请求头的超时时间
ReadHeaderTimeout读取请求头的超时时间
WriteTimeout写入响应的超时时间
IdleTimeoutKeep-Alive 连接空闲超时时间

请求体大小限制

防止客户端发送超大请求体:

router := gin.Default()

// 限制请求体最大 10MB
router.Use(func(c *gin.Context) {
    c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 10*1024*1024)
    c.Next()
})

或者用 http.Server 的配置:

server := &http.Server{
    MaxHeaderBytes: 1 << 20, // 1MB,限制请求头大小
}

缓冲区大小

调整缓冲区大小可以优化性能:

server := &http.Server{
    ReadBufferSize:  4096, // 读缓冲区 4KB
    WriteBufferSize: 4096, // 写缓冲区 4KB
}

缓冲区越大,系统调用次数越少,但内存占用也越高。需要根据实际情况权衡。

完整配置示例

一个生产环境可用的配置:

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
    
    "github.com/gin-gonic/gin"
)

func main() {
    // 生产模式
    gin.SetMode(gin.ReleaseMode)
    
    router := gin.New()
    router.Use(gin.Recovery())
    
    // 请求体大小限制
    router.Use(func(c *gin.Context) {
        c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 10*1024*1024)
        c.Next()
    })

    router.GET("/", func(c *gin.Context) {
        c.String(200, "Hello World")
    })

    router.GET("/slow", func(c *gin.Context) {
        time.Sleep(5 * time.Second) // 模拟慢处理
        c.String(200, "Slow response")
    })

    server := &http.Server{
        Addr:              ":8080",
        Handler:           router,
        ReadTimeout:       10 * time.Second,
        ReadHeaderTimeout: 5 * time.Second,
        WriteTimeout:      10 * time.Second,
        IdleTimeout:       120 * time.Second,
        MaxHeaderBytes:    1 << 20, // 1MB
        ReadBufferSize:    4 * 1024,
        WriteBufferSize:   4 * 1024,
    }

    // 优雅关闭
    go func() {
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server error: %v", err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    log.Println("Shutting down server...")
    
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    
    if err := server.Shutdown(ctx); err != nil {
        log.Printf("Server shutdown error: %v", err)
    }
    
    log.Println("Server stopped")
}

Keep-Alive 配置

HTTP Keep-Alive 允许复用 TCP 连接,减少连接建立开销。Go 默认开启 Keep-Alive,通过 IdleTimeout 控制空闲连接的存活时间:

server := &http.Server{
    Addr:         ":8080",
    Handler:      router,
    IdleTimeout:  120 * time.Second, // 空闲连接保持 2 分钟
}

如果不需要 Keep-Alive,可以禁用:

server := &http.Server{
    Addr:         ":8080",
    Handler:      router,
    IdleTimeout:  -1, // 禁用 Keep-Alive
}

但通常不建议禁用,Keep-Alive 能显著提升性能。

连接状态回调

可以监控连接状态变化:

server := &http.Server{
    Addr:    ":8080",
    Handler: router,
    ConnState: func(conn net.Conn, state http.ConnState) {
        log.Printf("Connection %s -> %s", conn.RemoteAddr(), state)
    },
}

连接状态包括:

  • http.StateNew:新连接
  • http.StateActive:正在处理请求
  • http.StateIdle:空闲,等待新请求
  • http.StateHijacked:被劫持(如 WebSocket)
  • http.StateClosed:已关闭

这个功能可以用来监控连接数、发现异常连接等。

错误日志处理

自定义错误日志输出:

type customLogger struct {
    *log.Logger
}

func (l *customLogger) Write(p []byte) (n int, err error) {
    // 可以在这里过滤或格式化日志
    return l.Logger.Write(p)
}

func main() {
    router := gin.Default()
    
    logger := &customLogger{log.New(os.Stderr, "[HTTP] ", log.LstdFlags)}
    
    server := &http.Server{
        Addr:     ":8080",
        Handler:  router,
        ErrorLog: logger,
    }
    
    log.Fatal(server.ListenAndServe())
}

多端口监听

有时候需要监听多个端口:

func main() {
    router := gin.Default()
    
    router.GET("/", func(c *gin.Context) {
        c.String(200, "Main server")
    })

    // 主服务
    mainServer := &http.Server{
        Addr:    ":8080",
        Handler: router,
    }

    // 管理接口
    adminRouter := gin.New()
    adminRouter.GET("/health", func(c *gin.Context) {
        c.String(200, "OK")
    })
    adminRouter.GET("/metrics", func(c *gin.Context) {
        c.String(200, "Metrics endpoint")
    })
    
    adminServer := &http.Server{
        Addr:    ":8081",
        Handler: adminRouter,
    }

    // 并发启动
    go func() {
        log.Println("Main server on :8080")
        if err := mainServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()

    go func() {
        log.Println("Admin server on :8081")
        if err := adminServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()

    // 等待退出信号
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    
    log.Println("Shutting down...")
}

BaseContext 传递

可以在创建连接时注入自定义 context:

type contextKey string

const requestIDKey contextKey = "requestID"

func main() {
    router := gin.Default()
    
    router.GET("/", func(c *gin.Context) {
        requestID := c.Request.Context().Value(requestIDKey)
        c.String(200, "Request ID: %v", requestID)
    })

    server := &http.Server{
        Addr:    ":8080",
        Handler: router,
        BaseContext: func(ln net.Listener) context.Context {
            // 可以在这里添加全局 context 值
            return context.Background()
        },
        ConnContext: func(ctx context.Context, c net.Conn) context.Context {
            // 每个连接的 context
            requestID := uuid.New().String()
            return context.WithValue(ctx, requestIDKey, requestID)
        },
    }
    
    log.Fatal(server.ListenAndServe())
}

小结

自定义 HTTP 配置是生产环境必备技能:

  • 超时设置:ReadTimeout、WriteTimeout、IdleTimeout 防止资源耗尽
  • 大小限制:MaxHeaderBytes、请求体限制防止恶意请求
  • 缓冲区:根据业务调整读写缓冲区大小
  • Keep-Alive:合理设置空闲超时,平衡性能和资源
  • 监控:用 ConnState 监控连接状态

这些配置没有放之四海皆准的值,需要根据实际业务场景测试调整。建议在压力测试中观察服务器表现,找到最适合的配置组合。