remove tgzip

This commit is contained in:
wjqserver 2025-06-10 00:04:15 +08:00
parent e891afe0b4
commit e6b54eedbf
3 changed files with 82 additions and 436 deletions

View file

@ -171,9 +171,7 @@ func min(a, b int) int {
### 内置
Recovery `r.Use(Recovery())`
Touka Gzip `r.Use(Gzip(-1))`
Recovery `r.Use(touka.Recovery())`
### fenthope

View file

@ -44,8 +44,8 @@ type Context struct {
// 携带ctx以实现关闭逻辑
ctx context.Context
// HTTPClient 用于在此上下文中执行出站 HTTP 请求
// 它由 Engine 提供
// HTTPClient 用于在此上下文中执行出站 HTTP 请求
// 它由 Engine 提供
HTTPClient *httpc.Client
// 引用所属的 Engine 实例,方便访问 Engine 的配置(如 HTMLRender
@ -56,8 +56,8 @@ type Context struct {
// --- Context 相关方法实现 ---
// reset 重置 Context 对象以供复用
// 每次从 sync.Pool 中获取 Context 后,都需要调用此方法进行初始化
// reset 重置 Context 对象以供复用
// 每次从 sync.Pool 中获取 Context 后,都需要调用此方法进行初始化
func (c *Context) reset(w http.ResponseWriter, req *http.Request) {
if rw, ok := c.Writer.(*responseWriterImpl); ok && !rw.IsHijacked() {
@ -80,8 +80,8 @@ func (c *Context) reset(w http.ResponseWriter, req *http.Request) {
// c.HTTPClient 和 c.engine 保持不变,它们引用 Engine 实例的成员
}
// Next 在处理链中执行下一个处理函数
// 这是中间件模式的核心,允许请求依次经过多个处理函数
// Next 在处理链中执行下一个处理函数
// 这是中间件模式的核心,允许请求依次经过多个处理函数
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
@ -90,25 +90,25 @@ func (c *Context) Next() {
}
}
// Abort 停止处理链的后续执行
// 通常在中间件中,当遇到错误或需要提前终止请求时调用
// Abort 停止处理链的后续执行
// 通常在中间件中,当遇到错误或需要提前终止请求时调用
func (c *Context) Abort() {
c.index = abortIndex // 将 index 设置为一个很大的值,使后续 Next() 调用跳过所有处理函数
}
// IsAborted 返回处理链是否已被中止
// IsAborted 返回处理链是否已被中止
func (c *Context) IsAborted() bool {
return c.index >= abortIndex
}
// AbortWithStatus 中止处理链并设置 HTTP 状态码
// AbortWithStatus 中止处理链并设置 HTTP 状态码
func (c *Context) AbortWithStatus(code int) {
c.Writer.WriteHeader(code) // 设置响应状态码
c.Abort() // 中止处理链
}
// Set 将一个键值对存储到 Context 中
// 这是一个线程安全的操作,用于在中间件之间传递数据
// Set 将一个键值对存储到 Context 中
// 这是一个线程安全的操作,用于在中间件之间传递数据
func (c *Context) Set(key string, value interface{}) {
c.mu.Lock() // 加写锁
if c.Keys == nil {
@ -118,8 +118,8 @@ func (c *Context) Set(key string, value interface{}) {
c.mu.Unlock() // 解写锁
}
// Get 从 Context 中获取一个值
// 这是一个线程安全的操作
// Get 从 Context 中获取一个值
// 这是一个线程安全的操作
func (c *Context) Get(key string) (value interface{}, exists bool) {
c.mu.RLock() // 加读锁
value, exists = c.Keys[key]
@ -127,8 +127,8 @@ func (c *Context) Get(key string) (value interface{}, exists bool) {
return
}
// MustGet 从 Context 中获取一个值,如果不存在则 panic
// 适用于确定值一定存在的场景
// MustGet 从 Context 中获取一个值,如果不存在则 panic
// 适用于确定值一定存在的场景
func (c *Context) MustGet(key string) interface{} {
if value, exists := c.Get(key); exists {
return value
@ -136,8 +136,8 @@ func (c *Context) MustGet(key string) interface{} {
panic("Key \"" + key + "\" does not exist in context.")
}
// Query 从 URL 查询参数中获取值
// 懒加载解析查询参数,并进行缓存
// Query 从 URL 查询参数中获取值
// 懒加载解析查询参数,并进行缓存
func (c *Context) Query(key string) string {
if c.queryCache == nil {
c.queryCache = c.Request.URL.Query() // 首次访问时解析并缓存
@ -145,7 +145,7 @@ func (c *Context) Query(key string) string {
return c.queryCache.Get(key)
}
// DefaultQuery 从 URL 查询参数中获取值,如果不存在则返回默认值
// DefaultQuery 从 URL 查询参数中获取值,如果不存在则返回默认值
func (c *Context) DefaultQuery(key, defaultValue string) string {
if value := c.Query(key); value != "" {
return value
@ -153,8 +153,8 @@ func (c *Context) DefaultQuery(key, defaultValue string) string {
return defaultValue
}
// PostForm 从 POST 请求体中获取表单值
// 懒加载解析表单数据,并进行缓存
// PostForm 从 POST 请求体中获取表单值
// 懒加载解析表单数据,并进行缓存
func (c *Context) PostForm(key string) string {
if c.formCache == nil {
c.Request.ParseMultipartForm(defaultMemory) // 解析 multipart/form-data 或 application/x-www-form-urlencoded
@ -163,7 +163,7 @@ func (c *Context) PostForm(key string) string {
return c.formCache.Get(key)
}
// DefaultPostForm 从 POST 请求体中获取表单值,如果不存在则返回默认值
// DefaultPostForm 从 POST 请求体中获取表单值,如果不存在则返回默认值
func (c *Context) DefaultPostForm(key, defaultValue string) string {
if value := c.PostForm(key); value != "" {
return value
@ -171,8 +171,8 @@ func (c *Context) DefaultPostForm(key, defaultValue string) string {
return defaultValue
}
// Param 从 URL 路径参数中获取值
// 例如,对于路由 /users/:idc.Param("id") 可以获取 id 的值
// Param 从 URL 路径参数中获取值
// 例如,对于路由 /users/:idc.Param("id") 可以获取 id 的值
func (c *Context) Param(key string) string {
return c.Params.ByName(key)
}
@ -184,14 +184,14 @@ func (c *Context) Raw(code int, contentType string, data []byte) {
c.Writer.Write(data)
}
// String 向响应写入格式化的字符串
// String 向响应写入格式化的字符串
func (c *Context) String(code int, format string, values ...interface{}) {
c.Writer.WriteHeader(code)
c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
}
// JSON 向响应写入 JSON 数据
// 设置 Content-Type 为 application/json
// JSON 向响应写入 JSON 数据
// 设置 Content-Type 为 application/json
func (c *Context) JSON(code int, obj interface{}) {
c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
c.Writer.WriteHeader(code)
@ -205,10 +205,10 @@ func (c *Context) JSON(code int, obj interface{}) {
c.Writer.Write(jsonBytes)
}
// HTML 渲染 HTML 模板
// 如果 Engine 配置了 HTMLRender则使用它进行渲染
// 否则,会进行简单的字符串输出
// 预留接口,可以扩展为支持多种模板引擎
// HTML 渲染 HTML 模板
// 如果 Engine 配置了 HTMLRender则使用它进行渲染
// 否则,会进行简单的字符串输出
// 预留接口,可以扩展为支持多种模板引擎
func (c *Context) HTML(code int, name string, obj interface{}) {
c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
c.Writer.WriteHeader(code)
@ -229,8 +229,8 @@ func (c *Context) HTML(code int, name string, obj interface{}) {
c.Writer.Write([]byte(fmt.Sprintf("<!-- HTML rendered for %s -->\n<pre>%v</pre>", name, obj)))
}
// Redirect 执行 HTTP 重定向
// code 应为 3xx 状态码 (如 http.StatusMovedPermanently, http.StatusFound)
// Redirect 执行 HTTP 重定向
// code 应为 3xx 状态码 (如 http.StatusMovedPermanently, http.StatusFound)
func (c *Context) Redirect(code int, location string) {
http.Redirect(c.Writer, c.Request, location, code)
c.Abort()
@ -239,7 +239,7 @@ func (c *Context) Redirect(code int, location string) {
}
}
// ShouldBindJSON 尝试将请求体绑定到 JSON 对象
// ShouldBindJSON 尝试将请求体绑定到 JSON 对象
func (c *Context) ShouldBindJSON(obj interface{}) error {
if c.Request.Body == nil {
return errors.New("request body is empty")
@ -257,9 +257,9 @@ func (c *Context) ShouldBindJSON(obj interface{}) error {
return nil
}
// ShouldBind 尝试将请求体绑定到各种类型JSON, Form, XML 等)
// 这是一个复杂的通用绑定接口,通常根据 Content-Type 或其他头部来判断绑定方式
// 预留接口,可根据项目需求进行扩展
// ShouldBind 尝试将请求体绑定到各种类型JSON, Form, XML 等)
// 这是一个复杂的通用绑定接口,通常根据 Content-Type 或其他头部来判断绑定方式
// 预留接口,可根据项目需求进行扩展
func (c *Context) ShouldBind(obj interface{}) error {
// TODO: 完整的通用绑定逻辑
// 可以根据 c.Request.Header.Get("Content-Type") 来判断是 JSON, Form, XML 等
@ -274,45 +274,45 @@ func (c *Context) ShouldBind(obj interface{}) error {
return errors.New("generic binding not fully implemented yet, implement based on Content-Type")
}
// AddError 添加一个错误到 Context
// 允许在处理请求过程中收集多个错误
// AddError 添加一个错误到 Context
// 允许在处理请求过程中收集多个错误
func (c *Context) AddError(err error) {
c.Errors = append(c.Errors, err)
}
// Errors 返回 Context 中收集的所有错误
// Errors 返回 Context 中收集的所有错误
func (c *Context) GetErrors() []error {
return c.Errors
}
// Client 返回 Engine 提供的 HTTPClient
// 方便在请求处理函数中进行出站 HTTP 请求
// Client 返回 Engine 提供的 HTTPClient
// 方便在请求处理函数中进行出站 HTTP 请求
func (c *Context) Client() *httpc.Client {
return c.HTTPClient
}
// Context() 返回请求的上下文,用于取消操作
// 这是 Go 标准库的 `context.Context`,用于请求的取消和超时管理
// Context() 返回请求的上下文,用于取消操作
// 这是 Go 标准库的 `context.Context`,用于请求的取消和超时管理
func (c *Context) Context() context.Context {
return c.ctx
}
// Done returns a channel that is closed when the request context is cancelled or times out.
// 继承自 `context.Context`
// 继承自 `context.Context`
func (c *Context) Done() <-chan struct{} {
return c.ctx.Done()
}
// Err returns the error, if any, that caused the context to be canceled or to
// time out.
// 继承自 `context.Context`
// 继承自 `context.Context`
func (c *Context) Err() error {
return c.ctx.Err()
}
// Value returns the value associated with this context for key, or nil if no
// value is associated with key.
// 可以用于从 Context 中获取与特定键关联的值,包括 Go 原生 Context 的值和 Touka Context 的 Keys
// 可以用于从 Context 中获取与特定键关联的值,包括 Go 原生 Context 的值和 Touka Context 的 Keys
func (c *Context) Value(key interface{}) interface{} {
if keyAsString, ok := key.(string); ok {
if val, exists := c.Get(keyAsString); exists {
@ -322,18 +322,18 @@ func (c *Context) Value(key interface{}) interface{} {
return c.ctx.Value(key) // 尝试从 Go 原生 Context 中获取值
}
// GetWriter 获得一个 io.Writer 接口,可以直接向响应体写入数据
// 这对于需要自定义流式写入或与其他需要 io.Writer 的库集成非常有用
// GetWriter 获得一个 io.Writer 接口,可以直接向响应体写入数据
// 这对于需要自定义流式写入或与其他需要 io.Writer 的库集成非常有用
func (c *Context) GetWriter() io.Writer {
return c.Writer // ResponseWriter 接口嵌入了 http.ResponseWriter而 http.ResponseWriter 实现了 io.Writer
}
// WriteStream 接受一个 io.Reader 并将其内容流式传输到响应体
// 返回写入的字节数和可能遇到的错误
// 该方法在开始写入之前,会确保设置 HTTP 状态码为 200 OK
// WriteStream 接受一个 io.Reader 并将其内容流式传输到响应体
// 返回写入的字节数和可能遇到的错误
// 该方法在开始写入之前,会确保设置 HTTP 状态码为 200 OK
func (c *Context) WriteStream(reader io.Reader) (written int64, err error) {
// 确保在写入数据前设置状态码
// WriteHeader 会在第一次写入时被 Write 方法隐式调用,但显式调用可以确保状态码的预期
// 确保在写入数据前设置状态码
// WriteHeader 会在第一次写入时被 Write 方法隐式调用,但显式调用可以确保状态码的预期
if !c.Writer.Written() {
c.Writer.WriteHeader(http.StatusOK) // 默认 200 OK
}
@ -346,14 +346,14 @@ func (c *Context) WriteStream(reader io.Reader) (written int64, err error) {
}
// GetReqBody 以获取一个 io.ReadCloser 接口,用于读取请求体
// 注意:请求体只能读取一次
// 注意:请求体只能读取一次
func (c *Context) GetReqBody() io.ReadCloser {
return c.Request.Body
}
// GetReqBodyFull
// GetReqBodyFull 读取并返回请求体的所有内容
// 注意:请求体只能读取一次
// GetReqBodyFull 读取并返回请求体的所有内容
// 注意:请求体只能读取一次
func (c *Context) GetReqBodyFull() ([]byte, error) {
if c.Request.Body == nil {
return nil, nil
@ -367,9 +367,9 @@ func (c *Context) GetReqBodyFull() ([]byte, error) {
return data, nil
}
// RequestIP 返回客户端的 IP 地址
// RequestIP 返回客户端的 IP 地址
// 它会根据 Engine 的配置 (ForwardByClientIP) 尝试从 X-Forwarded-For 或 X-Real-IP 等头部获取,
// 否则回退到 Request.RemoteAddr
// 否则回退到 Request.RemoteAddr
func (c *Context) RequestIP() string {
if c.engine.ForwardByClientIP {
for _, headerName := range c.engine.RemoteIPHeaders {
@ -409,55 +409,60 @@ func (c *Context) RequestIP() string {
return ""
}
// ClientIP 返回客户端的 IP 地址
// 这是一个别名,与 RequestIP 功能相同
// ClientIP 返回客户端的 IP 地址
// 这是一个别名,与 RequestIP 功能相同
func (c *Context) ClientIP() string {
return c.RequestIP()
}
// ContentType 返回请求的 Content-Type 头部
// ContentType 返回请求的 Content-Type 头部
func (c *Context) ContentType() string {
return c.GetReqHeader("Content-Type")
}
// UserAgent 返回请求的 User-Agent 头部
// UserAgent 返回请求的 User-Agent 头部
func (c *Context) UserAgent() string {
return c.GetReqHeader("User-Agent")
}
// Status 设置响应状态码
// Status 设置响应状态码
func (c *Context) Status(code int) {
c.Writer.WriteHeader(code)
}
// File 将指定路径的文件作为响应发送
// 它会设置 Content-Type 和 Content-Disposition 头部
// File 将指定路径的文件作为响应发送
// 它会设置 Content-Type 和 Content-Disposition 头部
func (c *Context) File(filepath string) {
http.ServeFile(c.Writer, c.Request, filepath)
c.Abort() // 发送文件后中止后续处理
}
// SetHeader 设置响应头部
// SetHeader 设置响应头部
func (c *Context) SetHeader(key, value string) {
c.Writer.Header().Set(key, value)
}
// AddHeader 添加响应头部
// AddHeader 添加响应头部
func (c *Context) AddHeader(key, value string) {
c.Writer.Header().Add(key, value)
}
// DelHeader 删除响应头部。
// Header 作为SetHeader的别名
func (c *Context) Header(key, value string) {
c.SetHeader(key, value)
}
// DelHeader 删除响应头部
func (c *Context) DelHeader(key string) {
c.Writer.Header().Del(key)
}
// GetReqHeader 获取请求头部的值。
// GetReqHeader 获取请求头部的值
func (c *Context) GetReqHeader(key string) string {
return c.Request.Header.Get(key)
}
// GetAllReqHeader 获取所有请求头部
// GetAllReqHeader 获取所有请求头部
func (c *Context) GetAllReqHeader() http.Header {
return c.Request.Header
}
@ -489,12 +494,12 @@ func (c *Context) GetLogger() *reco.Logger {
return c.engine.LogReco
}
// SetSameSite 设置响应的 SameSite cookie 属性
// SetSameSite 设置响应的 SameSite cookie 属性
func (c *Context) SetSameSite(samesite http.SameSite) {
c.sameSite = samesite
}
// SetCookie 设置一个 HTTP cookie
// SetCookie 设置一个 HTTP cookie
func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) {
if path == "" {
path = "/"
@ -521,7 +526,7 @@ func (c *Context) SetCookieData(cookie *http.Cookie) {
http.SetCookie(c.Writer, cookie)
}
// GetCookie 获取指定名称的 cookie 值
// GetCookie 获取指定名称的 cookie 值
func (c *Context) GetCookie(name string) (string, error) {
cookie, err := c.Request.Cookie(name)
if err != nil {
@ -535,8 +540,8 @@ func (c *Context) GetCookie(name string) (string, error) {
return value, nil
}
// DeleteCookie 删除指定名称的 cookie
// 通过设置 MaxAge 为 -1 来删除 cookie
// DeleteCookie 删除指定名称的 cookie
// 通过设置 MaxAge 为 -1 来删除 cookie
func (c *Context) DeleteCookie(name string) {
c.SetCookie(name, "", -1, "/", "", false, false) // 设置 MaxAge 为 -1 删除 cookie
}

357
tgzip.go
View file

@ -1,357 +0,0 @@
package touka
import (
"bufio"
"compress/gzip"
"errors"
"io"
"net"
"net/http"
"strconv"
"strings"
"sync"
)
const (
headerAcceptEncoding = "Accept-Encoding" // 请求头部,客户端声明接受的编码
headerContentEncoding = "Content-Encoding" // 响应头部,服务器声明使用的编码
headerContentLength = "Content-Length" // 响应头部,内容长度
headerContentType = "Content-Type" // 响应头部,内容类型
headerVary = "Vary" // 响应头部,指示缓存行为
encodingGzip = "gzip" // Gzip 编码名称
)
var (
// 默认可压缩的 MIME 类型
defaultCompressibleTypes = []string{
"text/html", "text/css", "text/plain", "text/javascript",
"application/javascript", "application/x-javascript", "application/json",
"application/xml", "image/svg+xml",
}
)
// GzipOptions 用于配置 Gzip 中间件。
type GzipOptions struct {
// Level 设置 Gzip 压缩级别。
// 例如: gzip.DefaultCompression, gzip.BestSpeed, gzip.BestCompression。
Level int
// MinContentLength 是应用 Gzip 的最小内容长度。
// 如果响应的 Content-Length 小于此值,则不应用 Gzip。
// 默认为 0 (无最小长度限制)。
MinContentLength int64
// CompressibleTypes 是要压缩的 MIME 类型列表。
// 如果为空,将使用 defaultCompressibleTypes。
CompressibleTypes []string
// DecompressFn 是一个可选函数,用于解压缩请求体 (如果请求体是 gzipped)。
// 如果为 nil则禁用请求体解压缩。
// 注意: 本次实现主要关注响应压缩,请求解压可以作为扩展。
// DecompressFn func(c *Context)
}
// gzipResponseWriter 包装了 touka.ResponseWriter 以提供 Gzip 压缩功能。
type gzipResponseWriter struct {
ResponseWriter // 底层的 ResponseWriter (可能是 ecw 或 responseWriterImpl)
gzWriter *gzip.Writer // compress/gzip 的 writer
options *GzipOptions // Gzip 配置
wroteHeader bool // 标记 Header 是否已写入
doCompression bool // 标记是否执行压缩
statusCode int // 存储状态码,在实际写入底层 Writer 前使用
}
// --- 对象池 ---
var gzipResponseWriterPool = sync.Pool{
New: func() interface{} {
return &gzipResponseWriter{}
},
}
// gzip.Writer 实例的对象池。
// 注意: gzip.Writer.Reset() 不会改变压缩级别,所以对象池需要提供已正确初始化级别的 writer。
// 我们为每个可能的级别创建一个池。
var gzipWriterPools [gzip.BestCompression - gzip.BestSpeed + 2]*sync.Pool // 覆盖 -1 (Default) 到 9 (BestCompression)
func init() {
for i := gzip.BestSpeed; i <= gzip.BestCompression; i++ {
level := i // 捕获循环变量用于闭包
gzipWriterPools[level-gzip.BestSpeed] = &sync.Pool{
New: func() interface{} {
// 初始化时 writer 为 nil在 Reset 时设置
w, _ := gzip.NewWriterLevel(nil, level)
return w
},
}
}
// 为 gzip.DefaultCompression (-1) 映射一个索引
defaultLevelIndex := gzip.BestCompression - gzip.BestSpeed + 1
gzipWriterPools[defaultLevelIndex] = &sync.Pool{
New: func() interface{} {
w, _ := gzip.NewWriterLevel(nil, gzip.DefaultCompression)
return w
},
}
}
// 从对象池获取一个 gzip.Writer
func getGzipWriterFromPool(level int, underlyingWriter io.Writer) *gzip.Writer {
var poolIndex int
if level == gzip.DefaultCompression {
poolIndex = gzip.BestCompression - gzip.BestSpeed + 1
} else if level >= gzip.BestSpeed && level <= gzip.BestCompression {
poolIndex = level - gzip.BestSpeed
} else { // 无效级别,使用默认级别
poolIndex = gzip.BestCompression - gzip.BestSpeed + 1
level = gzip.DefaultCompression // 保证一致性
}
gz := gzipWriterPools[poolIndex].Get().(*gzip.Writer)
gz.Reset(underlyingWriter) // 重置并关联到底层的 io.Writer
return gz
}
// 将 gzip.Writer 返还给对象池
func putGzipWriterToPool(gz *gzip.Writer, level int) {
var poolIndex int
if level == gzip.DefaultCompression {
poolIndex = gzip.BestCompression - gzip.BestSpeed + 1
} else if level >= gzip.BestSpeed && level <= gzip.BestCompression {
poolIndex = level - gzip.BestSpeed
} else { // 不应该发生,如果 getGzipWriterFromPool 进行了标准化
poolIndex = gzip.BestCompression - gzip.BestSpeed + 1
}
gzipWriterPools[poolIndex].Put(gz)
}
// 从对象池获取一个 gzipResponseWriter
func acquireGzipResponseWriter(underlying ResponseWriter, opts *GzipOptions) *gzipResponseWriter {
gzw := gzipResponseWriterPool.Get().(*gzipResponseWriter)
gzw.ResponseWriter = underlying
gzw.options = opts
gzw.wroteHeader = false
gzw.doCompression = false
gzw.statusCode = 0 // 重置状态码
// gzWriter 将在 WriteHeader 中如果需要时获取
return gzw
}
// 将 gzipResponseWriter 返还给对象池
func releaseGzipResponseWriter(gzw *gzipResponseWriter) {
if gzw.gzWriter != nil {
// 确保它被关闭并返回到池中
_ = gzw.gzWriter.Close() // 关闭会 flush
putGzipWriterToPool(gzw.gzWriter, gzw.options.Level)
gzw.gzWriter = nil
}
gzw.ResponseWriter = nil // 断开引用
gzw.options = nil
gzipResponseWriterPool.Put(gzw)
}
// --- gzipResponseWriter 方法实现 ---
// Header 返回底层 ResponseWriter 的头部 map。
func (gzw *gzipResponseWriter) Header() http.Header {
return gzw.ResponseWriter.Header()
}
// WriteHeader 发送 HTTP 响应头部和指定的状态码。
// 在这里决定是否进行压缩。
func (gzw *gzipResponseWriter) WriteHeader(statusCode int) {
if gzw.wroteHeader {
return
}
gzw.wroteHeader = true
gzw.statusCode = statusCode // 存储状态码
// 在修改头部以进行压缩之前进行条件检查
// 1. 如果状态码是信息性(1xx)、重定向(3xx)、无内容(204)、重置内容(205)或未修改(304),则不压缩
if statusCode < http.StatusOK || statusCode == http.StatusNoContent || statusCode == http.StatusResetContent || statusCode == http.StatusNotModified {
gzw.ResponseWriter.WriteHeader(statusCode)
return
}
// 2. 如果响应已经被编码,则不压缩
if gzw.Header().Get(headerContentEncoding) != "" {
gzw.ResponseWriter.WriteHeader(statusCode)
return
}
// 3. 检查 Content-Type
contentType := strings.ToLower(strings.TrimSpace(strings.Split(gzw.Header().Get(headerContentType), ";")[0]))
compressibleTypes := gzw.options.CompressibleTypes
if len(compressibleTypes) == 0 {
compressibleTypes = defaultCompressibleTypes
}
isCompressible := false
for _, t := range compressibleTypes {
if strings.HasPrefix(contentType, t) { // 使用 HasPrefix 以匹配如 "text/html; charset=utf-8"
isCompressible = true
break
}
}
if !isCompressible {
gzw.ResponseWriter.WriteHeader(statusCode)
return
}
// 4. 检查 MinContentLength
if gzw.options.MinContentLength > 0 {
if clStr := gzw.Header().Get(headerContentLength); clStr != "" {
if cl, err := strconv.ParseInt(clStr, 10, 64); err == nil && cl < gzw.options.MinContentLength {
gzw.ResponseWriter.WriteHeader(statusCode)
return
}
}
// 如果未设置 Content-Length但设置了 MinContentLength我们可能仍会压缩。
// 这是一个权衡:可能会压缩小的动态内容。
}
// 所有检查通过,进行压缩
gzw.doCompression = true
gzw.Header().Set(headerContentEncoding, encodingGzip)
gzw.Header().Add(headerVary, headerAcceptEncoding) // 使用 Add 以避免覆盖其他 Vary 值
gzw.Header().Del(headerContentLength) // Gzip 会改变内容长度,所以删除它
// 从池中获取 gzWriter并将其 Reset 指向实际的底层 ResponseWriter
// 注意gzw.ResponseWriter 是被 Gzip 包装的 writer (例如,原始的 responseWriterImpl 或 ecw)
gzw.gzWriter = getGzipWriterFromPool(gzw.options.Level, gzw.ResponseWriter)
gzw.ResponseWriter.WriteHeader(statusCode) // 调用原始的 WriteHeader
}
// Write 将数据写入连接作为 HTTP 回复的一部分。
func (gzw *gzipResponseWriter) Write(data []byte) (int, error) {
if !gzw.wroteHeader {
// 如果在 WriteHeader 之前调用 Write根据 http.ResponseWriter 规范,
// 应写入 200 OK 头部。
gzw.WriteHeader(http.StatusOK)
}
if gzw.doCompression {
return gzw.gzWriter.Write(data)
}
return gzw.ResponseWriter.Write(data)
}
// Close 确保 gzip writer 被关闭并释放资源。
// 中间件应该在 c.Next() 之后调用它(通常在 defer 中)。
func (gzw *gzipResponseWriter) Close() error {
if gzw.gzWriter != nil {
err := gzw.gzWriter.Close() // Close 会 Flush
putGzipWriterToPool(gzw.gzWriter, gzw.options.Level)
gzw.gzWriter = nil // 标记为已返回
return err
}
return nil
}
// Flush 将所有缓冲数据发送到客户端。
// 实现 http.Flusher。
func (gzw *gzipResponseWriter) Flush() {
if gzw.doCompression && gzw.gzWriter != nil {
_ = gzw.gzWriter.Flush() // 确保 gzip writer 的缓冲被刷新
}
// 然后刷新底层的 writer (如果它支持)
if fl, ok := gzw.ResponseWriter.(http.Flusher); ok {
fl.Flush()
}
}
// Hijack 允许调用者接管连接。
// 实现 http.Hijacker。
func (gzw *gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
// 如果正在压缩hijack 的意义不大或不安全。
// 然而WriteHeader 应该会阻止对 101 状态码的压缩。
// 此调用必须转到实际的底层 ResponseWriter。
if hj, ok := gzw.ResponseWriter.(http.Hijacker); ok {
return hj.Hijack()
}
// 返回英文错误
return nil, nil, errors.New("touka.gzipResponseWriter: underlying ResponseWriter does not implement http.Hijacker")
}
// Status 返回已写入的 HTTP 状态码。委托给底层的 ResponseWriter。
// 这确保了与 ecw 或其他可能跟踪状态的包装器的兼容性。
func (gzw *gzipResponseWriter) Status() int {
if gzw.statusCode != 0 { // 如果我们在 WriteHeader 期间存储了它
return gzw.statusCode
}
return gzw.ResponseWriter.Status() // 委托
}
// Size 返回已写入的字节数。委托给底层的 ResponseWriter。
// 如果已压缩,这将是压缩后的大小 (由底层 writer 记录)。
func (gzw *gzipResponseWriter) Size() int {
return gzw.ResponseWriter.Size() // GzipResponseWriter 本身不直接跟踪大小,依赖底层
}
// Written 返回 WriteHeader 是否已被调用。委托给底层的 ResponseWriter。
func (gzw *gzipResponseWriter) Written() bool {
// 如果 gzw.wroteHeader 为 true说明 WriteHeader 至少被 gzw 处理过。
// 但最终是否写入底层取决于 gzw 的逻辑。
// 更可靠的是询问底层 writer。
return gzw.ResponseWriter.Written() // 委托
}
// --- Gzip 中间件 ---
// Gzip 返回一个使用 Gzip 压缩 HTTP 响应的中间件。
// 它会检查客户端的 "Accept-Encoding" 头部和响应的 "Content-Type"
// 来决定是否应用压缩。
// level 参数指定压缩级别 (例如 gzip.DefaultCompression)。
// opts 参数是可选的 GzipOptions。
func Gzip(level int, opts ...GzipOptions) HandlerFunc {
config := GzipOptions{ // 初始化默认配置
Level: level,
MinContentLength: 0, // 默认:无最小长度
CompressibleTypes: defaultCompressibleTypes,
}
if len(opts) > 0 { // 如果传入了 GzipOptions则覆盖默认值
opt := opts[0]
config.Level = opt.Level // 允许通过结构体覆盖级别
if opt.MinContentLength > 0 {
config.MinContentLength = opt.MinContentLength
}
if len(opt.CompressibleTypes) > 0 {
config.CompressibleTypes = opt.CompressibleTypes
}
}
// 验证级别
if config.Level < gzip.DefaultCompression || config.Level > gzip.BestCompression {
config.Level = gzip.DefaultCompression
}
return func(c *Context) {
// 1. 检查客户端是否接受 gzip
if !strings.Contains(c.Request.Header.Get(headerAcceptEncoding), encodingGzip) {
c.Next()
return
}
// 2. 包装 ResponseWriter
originalWriter := c.Writer
gzw := acquireGzipResponseWriter(originalWriter, &config)
c.Writer = gzw // 替换上下文的 writer
// defer 确保即使后续处理函数发生 panic也能进行清理
// 尽管恢复中间件应该自己处理 panic 响应。
defer func() {
// 必须关闭 gzip writer 以刷新其缓冲区。
// 这也会将 gzip.Writer 返回到其对象池。
if err := gzw.Close(); err != nil {
// 记录关闭 gzip writer 时的错误,但不应覆盖已发送的响应
// 通常这个错误不严重,因为数据可能已经大部分发送
// 使用英文记录日志
// log.Printf("Error closing gzip writer: %v", err)
c.AddError(err) // 可以选择将错误添加到 Context 中
}
// 恢复原始 writer 并将 gzipResponseWriter 返回到其对象池
c.Writer = originalWriter
releaseGzipResponseWriter(gzw)
}()
// 3. 调用链中的下一个处理函数
c.Next()
// c.Next() 执行完毕后,响应头部应该已经设置。
// gzw.WriteHeader 会被显式调用或通过第一次 Write 隐式调用。
// 如果 gzw.doCompression 为 true响应体已写入 gzw.gzWriter。
// defer 中的 gzw.Close() 会刷新最终的压缩字节。
}
}