xfy ac9153f09d fix(proxy,stream,server): Phase 8 问题修复与功能完善
- WebSocket 代理集成:handleWebSocket 现调用 ProxyWebSocket 实现
- 删除 UDP Stream 冗余代码:移除 udpListener 类型及相关测试
- 热升级监听器继承:改用 net.Listen + Serve 模式支持监听器传递
- 代码格式修复:注释格式调整、字段对齐、文件末尾换行符

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-03 14:28:00 +08:00

247 lines
5.3 KiB
Go
Raw 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 compression 提供 HTTP 响应压缩中间件,支持 gzip 和 brotli 算法。
package compression
import (
"bytes"
"strings"
"sync"
"github.com/andybalholm/brotli"
"github.com/klauspost/compress/gzip"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/config"
)
// Algorithm 压缩算法类型。
type Algorithm int
const (
// AlgorithmGzip 使用 gzip 压缩。
AlgorithmGzip Algorithm = iota
// AlgorithmBrotli 使用 brotli 压缩。
AlgorithmBrotli
)
// CompressionMiddleware 响应压缩中间件。
type CompressionMiddleware struct {
types []string // 可压缩的 MIME 类型
level int // 压缩级别
minSize int // 最小压缩大小
algorithm Algorithm // 压缩算法
// 缓冲池
gzipPool sync.Pool
brotliPool sync.Pool
}
// New 创建压缩中间件。
func New(cfg *config.CompressionConfig) (*CompressionMiddleware, 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 "gzip":
algo = AlgorithmGzip
case "both":
// both 模式优先使用 brotli如果客户端支持
algo = AlgorithmBrotli
default:
algo = AlgorithmGzip
}
m := &CompressionMiddleware{
types: cfg.Types,
level: cfg.Level,
minSize: cfg.MinSize,
algorithm: algo,
}
// 初始化缓冲池
m.gzipPool = sync.Pool{
New: func() interface{} {
w, _ := gzip.NewWriterLevel(nil, cfg.Level)
return w
},
}
// 初始化 brotli 缓冲池
m.brotliPool = sync.Pool{
New: func() interface{} {
return brotli.NewWriterOptions(nil, brotli.WriterOptions{
Quality: 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 *CompressionMiddleware) Name() string {
return "compression"
}
// Process 应用压缩中间件。
func (m *CompressionMiddleware) Process(next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
// 检查客户端是否支持压缩
acceptEncoding := string(ctx.Request.Header.Peek("Accept-Encoding"))
// 根据算法和客户端支持选择压缩方式
var useGzip, useBrotli bool
if m.algorithm == AlgorithmGzip {
useGzip = strings.Contains(acceptEncoding, "gzip")
} else if m.algorithm == AlgorithmBrotli {
// brotli 或 both 模式
if strings.Contains(acceptEncoding, "br") {
useBrotli = true
} else if strings.Contains(acceptEncoding, "gzip") {
useGzip = true
}
}
// 如果不需要压缩,直接执行
if !useGzip && !useBrotli {
next(ctx)
return
}
// 执行处理器
next(ctx)
// 获取响应体
body := ctx.Response.Body()
bodyLen := len(body)
// 检查是否满足压缩条件
if bodyLen < m.minSize {
return // 不压缩
}
// 检查 MIME 类型
contentType := string(ctx.Response.Header.ContentType())
if !m.isCompressible(contentType) {
return // 不压缩此类型
}
// 执行压缩
var compressed []byte
var encoding string
if useBrotli {
compressed = m.compressBrotli(body)
encoding = "br"
} else if useGzip {
compressed = m.compressGzip(body)
encoding = "gzip"
}
if len(compressed) > 0 && len(compressed) < bodyLen {
ctx.Response.SetBody(compressed)
ctx.Response.Header.Set("Content-Encoding", encoding)
ctx.Response.Header.Del("Content-Length") // 让 fasthttp 自动计算
}
}
}
// isCompressible 检查 MIME 类型是否可压缩。
func (m *CompressionMiddleware) isCompressible(contentType string) bool {
// 移除 charset 等参数
ct := contentType
if idx := strings.Index(ct, ";"); idx >= 0 {
ct = ct[:idx]
}
ct = strings.TrimSpace(strings.ToLower(ct))
for _, t := range m.types {
if strings.ToLower(t) == ct {
return true
}
// 支持通配符匹配
if strings.HasSuffix(t, "/*") {
base := strings.TrimSuffix(t, "/*")
if strings.HasPrefix(ct, base) {
return true
}
}
}
return false
}
// compressGzip 使用 gzip 压缩数据。
func (m *CompressionMiddleware) compressGzip(data []byte) []byte {
w := m.gzipPool.Get().(*gzip.Writer)
defer m.gzipPool.Put(w)
var buf bytes.Buffer
w.Reset(&buf)
w.Write(data)
w.Close()
return buf.Bytes()
}
// compressBrotli 使用 brotli 压缩数据。
func (m *CompressionMiddleware) compressBrotli(data []byte) []byte {
w := m.brotliPool.Get().(*brotli.Writer)
defer m.brotliPool.Put(w)
var buf bytes.Buffer
w.Reset(&buf)
w.Write(data)
w.Close()
return buf.Bytes()
}
// Types 返回可压缩的 MIME 类型列表。
func (m *CompressionMiddleware) Types() []string {
return m.types
}
// Level 返回压缩级别。
func (m *CompressionMiddleware) Level() int {
return m.level
}
// MinSize 返回最小压缩大小。
func (m *CompressionMiddleware) MinSize() int {
return m.minSize
}