// Package compression 提供 HTTP 响应压缩中间件,支持 gzip 和 brotli 算法。 // // 该文件包含压缩相关的核心逻辑,包括: // - gzip 压缩(兼容性好,所有浏览器支持) // - brotli 压缩(压缩率更高,适合现代浏览器) // - MIME 类型过滤 // - 最小压缩大小控制 // // 主要用途: // // 用于压缩 HTTP 响应内容,减少传输数据量,提升页面加载速度。 // // 注意事项: // - 使用缓冲池复用压缩对象,减少内存分配 // - 小于 MinSize 的响应不压缩 // // 作者:xfy package compression import ( "bufio" "bytes" "io" "strings" "sync" "github.com/andybalholm/brotli" "github.com/klauspost/compress/gzip" "github.com/valyala/fasthttp" "rua.plus/lolly/internal/config" ) // resettableWriteCloser 接口用于统一 gzip.Writer 和 brotli.Writer 的操作。 // 这两个 writer 都实现了 Reset(io.Writer), Write([]byte) (int, error), Close() error type resettableWriteCloser interface { Reset(w io.Writer) Write(data []byte) (int, error) Close() error } // compressorPool 是一个通用的压缩 writer 池。 type compressorPool struct { pool sync.Pool level int factory func(level int) resettableWriteCloser } // newGzipPool 创建 gzip writer 池。 func newGzipPool(level int) *compressorPool { return &compressorPool{ level: level, factory: func(level int) resettableWriteCloser { w, err := gzip.NewWriterLevel(nil, level) if err != nil { w, _ = gzip.NewWriterLevel(nil, gzip.DefaultCompression) } return w }, } } // newBrotliPool 创建 brotli writer 池。 func newBrotliPool(level int) *compressorPool { return &compressorPool{ level: level, factory: func(level int) resettableWriteCloser { return brotli.NewWriterOptions(nil, brotli.WriterOptions{ Quality: level, }) }, } } // Get 从池中获取 writer。 func (p *compressorPool) Get() (resettableWriteCloser, bool) { if p.pool.New == nil { p.pool.New = func() any { return p.factory(p.level) } } v, ok := p.pool.Get().(resettableWriteCloser) return v, ok } // Put 将 writer 放回池中。 func (p *compressorPool) Put(w resettableWriteCloser) { p.pool.Put(w) } // streamingThreshold 流式压缩阈值。 // 响应体超过此大小时使用 SetBodyStreamWriter 流式压缩, // 消除 compressed buffer 分配,降低内存峰值。 const streamingThreshold = 64 * 1024 // 64KB // Algorithm 压缩算法类型。 type Algorithm int const ( // AlgorithmGzip 使用 gzip 压缩算法。 AlgorithmGzip Algorithm = iota // AlgorithmBrotli 使用 brotli 压缩算法。 AlgorithmBrotli compressionGZIP = "gzip" ) // Middleware 响应压缩中间件。 type Middleware struct { // gzipPool gzip.Writer 缓冲池 gzipPool *compressorPool // brotliPool brotli.Writer 缓冲池 brotliPool *compressorPool // types 可压缩的 MIME 类型列表 types []string // level 压缩级别(1-9) level int // minSize 最小压缩大小(字节) minSize int // algorithm 压缩算法 algorithm Algorithm } // New 创建压缩中间件。 // // 参数: // - cfg: 压缩配置,包含算法类型、压缩级别、最小压缩大小等 // // 返回值: // - *Middleware: 压缩中间件实例 // - error: 配置无效时返回错误 func New(cfg *config.CompressionConfig) (*Middleware, error) { if cfg == nil { cfg = &config.CompressionConfig{ Type: "gzip", Level: 6, MinSize: 1024, Types: defaultCompressibleTypes(), } } // 设置默认值 if cfg.Level == 0 { cfg.Level = 6 } if cfg.MinSize == 0 { cfg.MinSize = 1024 } if len(cfg.Types) == 0 { cfg.Types = defaultCompressibleTypes() } // 解析算法类型 var algo Algorithm switch strings.ToLower(cfg.Type) { case "brotli": algo = AlgorithmBrotli case compressionGZIP: algo = AlgorithmGzip case "both": // both 模式优先使用 brotli(如果客户端支持) algo = AlgorithmBrotli default: algo = AlgorithmGzip } m := &Middleware{ types: cfg.Types, level: cfg.Level, minSize: cfg.MinSize, algorithm: algo, } // 初始化缓冲池 m.gzipPool = newGzipPool(cfg.Level) m.brotliPool = newBrotliPool(cfg.Level) return m, nil } // defaultCompressibleTypes 返回默认可压缩的 MIME 类型。 func defaultCompressibleTypes() []string { return []string{ "text/html", "text/css", "text/javascript", "text/plain", "text/xml", "application/json", "application/javascript", "application/xml", "application/xhtml+xml", } } // Name 返回中间件名称。 func (m *Middleware) Name() string { return "compression" } // Process 应用压缩中间件。 // // 参数: // - next: 下一个请求处理器 // // 返回值: // - fasthttp.RequestHandler: 包装后的请求处理器 func (m *Middleware) Process(next fasthttp.RequestHandler) fasthttp.RequestHandler { return func(ctx *fasthttp.RequestCtx) { // 检查客户端是否支持压缩(零拷贝使用 []byte) acceptEncoding := ctx.Request.Header.Peek("Accept-Encoding") // 根据算法和客户端支持选择压缩方式 var useGzip, useBrotli bool switch m.algorithm { case AlgorithmGzip: useGzip = bytes.Contains(acceptEncoding, []byte("gzip")) case AlgorithmBrotli: // brotli 或 both 模式 if bytes.Contains(acceptEncoding, []byte("br")) { useBrotli = true } else if bytes.Contains(acceptEncoding, []byte("gzip")) { useGzip = true } } // 如果不需要压缩,直接执行 if !useGzip && !useBrotli { next(ctx) return } // 执行处理器 next(ctx) // 检查是否已有 Content-Encoding(由 gzip_static 或上游处理器设置) // 已编码的响应不应再次压缩,避免双重编码导致数据损坏 if len(ctx.Response.Header.Peek("Content-Encoding")) > 0 { return } // 获取响应体 body := ctx.Response.Body() bodyLen := len(body) // 检查是否满足压缩条件 if bodyLen < m.minSize { return // 不压缩 } // 检查 MIME 类型(零拷贝使用 []byte) contentType := ctx.Response.Header.ContentType() if !m.isCompressible(contentType) { return // 不压缩此类型 } // 执行压缩 var encoding string if useBrotli { encoding = "br" } else if useGzip { encoding = compressionGZIP } if bodyLen > streamingThreshold { // 大响应:流式压缩,消除 compressed buffer 分配 if useBrotli { m.streamWithPool(ctx, encoding, m.brotliPool) } else if useGzip { m.streamWithPool(ctx, encoding, m.gzipPool) } } else { // 小响应:缓冲压缩 var compressed []byte if useBrotli { compressed = m.compressWithPool(body, m.brotliPool) } else if useGzip { compressed = m.compressWithPool(body, m.gzipPool) } if len(compressed) > 0 && len(compressed) < bodyLen { ctx.Response.SetBody(compressed) ctx.Response.Header.Set("Content-Encoding", encoding) ctx.Response.Header.Del("Content-Length") } } } } // isCompressible 检查 MIME 类型是否可压缩。 // // 参数: // - contentType: 内容类型(MIME 类型)[] // // 返回值: // - bool: 是否可压缩 func (m *Middleware) isCompressible(contentType []byte) bool { // 移除 charset 等参数 ct := contentType if idx := bytes.IndexByte(ct, ';'); idx >= 0 { ct = ct[:idx] } ct = bytes.TrimSpace(ct) for _, t := range m.types { if bytes.Equal(bytes.ToLower([]byte(t)), ct) { return true } // 支持通配符匹配 if base, found := strings.CutSuffix(t, "/*"); found { if bytes.HasPrefix(ct, []byte(base)) { return true } } } return false } // compressWithPool 使用缓冲池压缩数据。 // // 参数: // - data: 待压缩的原始数据 // - pool: 压缩 writer 缓冲池 // // 返回值: // - []byte: 压缩后的数据 func (m *Middleware) compressWithPool(data []byte, pool *compressorPool) []byte { w, ok := pool.Get() if !ok { return data // fallback to uncompressed } defer pool.Put(w) var buf bytes.Buffer w.Reset(&buf) if _, err := w.Write(data); err != nil { //nolint:staticcheck // intentionally empty branch // 忽略写入错误,缓冲到 bytes.Buffer 时不太可能失败 } _ = w.Close() return buf.Bytes() } // streamWithPool 使用流式压缩。 // // 通过 SetBodyStreamWriter 将压缩数据直接写入响应流, // 消除 compressed buffer 分配,降低内存峰值。 // // 参数: // - ctx: fasthttp 请求上下文 // - encoding: Content-Encoding 值 // - pool: 压缩 writer 缓冲池 func (m *Middleware) streamWithPool(ctx *fasthttp.RequestCtx, encoding string, pool *compressorPool) { ctx.Response.Header.Set("Content-Encoding", encoding) ctx.Response.Header.Del("Content-Length") // 使用 chunked encoding body := ctx.Response.Body() ctx.SetBodyStreamWriter(func(w *bufio.Writer) { writer, ok := pool.Get() if !ok { // pool 获取失败,直接写原始 body _, _ = w.Write(body) _ = w.Flush() return } defer pool.Put(writer) writer.Reset(w) _, _ = writer.Write(body) _ = writer.Close() _ = w.Flush() }) } // Types 返回可压缩的 MIME 类型列表。 // // 返回值: // - []string: 可压缩的 MIME 类型列表 func (m *Middleware) Types() []string { return m.types } // Level 返回压缩级别。 // // 返回值: // - int: 压缩级别(1-9) func (m *Middleware) Level() int { return m.level } // MinSize 返回最小压缩大小。 // // 返回值: // - int: 最小压缩大小(字节) func (m *Middleware) MinSize() int { return m.minSize }