164 lines
3.6 KiB
Go
164 lines
3.6 KiB
Go
|
|
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)
|