Files

164 lines
3.6 KiB
Go
Raw Permalink Normal View History

package middleware
import (
"compress/gzip"
"io"
"net/http"
"strings"
"sync"
"github.com/gin-gonic/gin"
)
// gzipMinLength 小于此字节数的响应不压缩(避免小响应压缩反而增大体积)
const gzipMinLength = 1024
// gzipPool 复用 gzip.Writer减少 GC 压力
var gzipPool = sync.Pool{
New: func() interface{} {
w, _ := gzip.NewWriterLevel(io.Discard, gzip.BestSpeed)
return w
},
}
// gzipResponseWriter 包装 gin.ResponseWriter按需启用 gzip 压缩。
// 所有写入先缓冲;第一次超过阈值时决定是否压缩。
type gzipResponseWriter struct {
gin.ResponseWriter
gz *gzip.Writer
buf []byte
threshold int
decided bool // 已决定是否压缩
}
func (g *gzipResponseWriter) Write(data []byte) (int, error) {
if g.decided {
if g.gz != nil {
return g.gz.Write(data)
}
return g.ResponseWriter.Write(data)
}
// 积累数据
g.buf = append(g.buf, data...)
if len(g.buf) >= g.threshold {
return len(data), g.decide()
}
return len(data), nil
}
func (g *gzipResponseWriter) WriteString(s string) (int, error) {
return g.Write([]byte(s))
}
// decide 根据已缓冲内容和 Content-Type 决定是否压缩,并写出缓冲数据
func (g *gzipResponseWriter) decide() error {
g.decided = true
ct := g.ResponseWriter.Header().Get("Content-Type")
if g.gz != nil && shouldCompress(ct) {
// 启用 gzip
g.ResponseWriter.Header().Set("Content-Encoding", "gzip")
g.ResponseWriter.Header().Set("Vary", "Accept-Encoding")
g.ResponseWriter.Header().Del("Content-Length")
g.gz.Reset(g.ResponseWriter)
if len(g.buf) > 0 {
_, err := g.gz.Write(g.buf)
g.buf = nil
return err
}
} else {
// 不压缩:回收 gzip.Writer
if g.gz != nil {
gzipPool.Put(g.gz)
g.gz = nil
}
if len(g.buf) > 0 {
_, err := g.ResponseWriter.Write(g.buf)
g.buf = nil
return err
}
}
g.buf = nil
return nil
}
// finalize 在请求处理完毕后刷出剩余缓冲数据并关闭 gzip.Writer
func (g *gzipResponseWriter) finalize() {
if !g.decided {
// 响应体小于阈值,直接透传(不压缩)
g.decided = true
if g.gz != nil {
gzipPool.Put(g.gz)
g.gz = nil
}
if len(g.buf) > 0 {
_, _ = g.ResponseWriter.Write(g.buf)
g.buf = nil
}
return
}
if g.gz != nil {
_ = g.gz.Flush()
_ = g.gz.Close()
gzipPool.Put(g.gz)
g.gz = nil
}
}
// shouldCompress 根据 Content-Type 判断是否值得压缩(二进制流不压缩)
func shouldCompress(contentType string) bool {
ct := strings.ToLower(strings.SplitN(contentType, ";", 2)[0])
switch ct {
case "application/json",
"application/javascript",
"text/html",
"text/plain",
"text/css",
"text/xml",
"application/xml",
"application/x-www-form-urlencoded":
return true
}
return false
}
// GzipMiddleware 对 JSON/文本类响应启用 GZIP 压缩。
//
// 仅在满足以下条件时压缩:
// - 客户端发送了 Accept-Encoding: gzip
// - 响应 Content-Type 为 JSON/文本类
// - 响应体超过 gzipMinLength默认 1 KiB
//
// 其余情况透传,不影响性能。
func GzipMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 客户端不接受 gzip 则跳过
if !strings.Contains(c.GetHeader("Accept-Encoding"), "gzip") {
c.Next()
return
}
gz := gzipPool.Get().(*gzip.Writer)
grw := &gzipResponseWriter{
ResponseWriter: c.Writer,
gz: gz,
threshold: gzipMinLength,
}
c.Writer = grw
defer func() {
grw.finalize()
c.Writer = grw.ResponseWriter
}()
c.Next()
}
}
// Ensure gzipResponseWriter implements http.Hijacker forwarding (needed by some WebSocket libs)
var _ http.ResponseWriter = (*gzipResponseWriter)(nil)