perf: 大幅优化静态文件下载性能

- 使用 http.ServeFile 替代手动实现,利用 sendfile 零拷贝
- gzip 中间件跳过 /uploads/ 路径,避免额外开销
- 支持 Range 请求和断点续传(由标准库自动处理)
- 移除不必要的手动 MIME 类型映射

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-12 23:46:26 +08:00
parent 6c191d1b0d
commit 0d08bcc0f8
+14 -171
View File
@@ -4,14 +4,12 @@ import (
"compress/gzip"
"embed"
"fmt"
"io"
"io/fs"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"verification-platform-backend/internal/config"
"verification-platform-backend/internal/database"
@@ -388,6 +386,12 @@ func isCompressibleContentType(contentType string) bool {
func gzipMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 静态文件下载路径跳过 gzip 处理,避免影响下载性能
if strings.HasPrefix(c.Request.URL.Path, "/uploads/") {
c.Next()
return
}
if !strings.Contains(c.GetHeader("Accept-Encoding"), "gzip") {
c.Next()
return
@@ -443,11 +447,7 @@ func (w *gzipResponseWriter) WriteString(s string) (int, error) {
return w.ResponseWriter.WriteString(s)
}
// 32KB 缓冲区用于文件传输
// 1MB 缓冲区用于高速文件传输,适合 G 口带宽和大文件
const fileBufferSize = 1024 * 1024
// handleOptimizedStaticFile 优化的静态文件处理,支持 Range 请求和断点续传
// handleOptimizedStaticFile 优化的静态文件处理,使用 http.ServeFile 实现最高性能
func handleOptimizedStaticFile(c *gin.Context) {
// 获取请求的文件路径
filepathParam := c.Param("filepath")
@@ -467,170 +467,13 @@ func handleOptimizedStaticFile(c *gin.Context) {
return
}
// 打开文件
file, err := os.Open(cleanPath)
if err != nil {
if os.IsNotExist(err) {
c.Status(http.StatusNotFound)
} else {
c.Status(http.StatusInternalServerError)
}
return
}
defer file.Close()
// 获取文件信息
fileInfo, err := file.Stat()
if err != nil {
c.Status(http.StatusInternalServerError)
return
}
// 如果是目录,返回 404
if fileInfo.IsDir() {
c.Status(http.StatusNotFound)
return
}
// 获取文件大小
fileSize := fileInfo.Size()
// 设置响应头
c.Header("Accept-Ranges", "bytes")
// 设置缓存头
c.Header("Cache-Control", "public, max-age=31536000, immutable")
// 获取 MIME 类型
ext := strings.ToLower(filepath.Ext(cleanPath))
if mimeType, ok := mimeTypes[ext]; ok {
c.Header("Content-Type", mimeType)
} else {
c.Header("Content-Type", "application/octet-stream")
}
// 处理 Range 请求(断点续传)
rangeHeader := c.GetHeader("Range")
if rangeHeader != "" {
// 解析 Range 头
// 格式: bytes=start-end 或 bytes=start-
rangeParts := strings.TrimPrefix(rangeHeader, "bytes=")
if rangeParts != rangeHeader {
// 解析范围
var start, end int64
if strings.Contains(rangeParts, "-") {
parts := strings.Split(rangeParts, "-")
if len(parts) == 2 {
if parts[0] != "" {
start, _ = strconv.ParseInt(parts[0], 10, 64)
}
if parts[1] != "" {
end, _ = strconv.ParseInt(parts[1], 10, 64)
} else {
end = fileSize - 1
}
}
}
// 验证范围
if start >= fileSize || start < 0 || end < start || end >= fileSize {
c.Header("Content-Range", fmt.Sprintf("bytes */%d", fileSize))
c.Status(http.StatusRequestedRangeNotSatisfiable)
return
}
// 设置部分内容响应
c.Header("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fileSize))
c.Header("Content-Length", strconv.FormatInt(end-start+1, 10))
c.Status(http.StatusPartialContent)
// 定位到起始位置
file.Seek(start, io.SeekStart)
// 使用缓冲区传输
buf := make([]byte, fileBufferSize)
remaining := end - start + 1
for remaining > 0 {
toRead := int64(fileBufferSize)
if remaining < toRead {
toRead = remaining
}
n, err := file.Read(buf[:toRead])
if err != nil && err != io.EOF {
break
}
if n == 0 {
break
}
c.Writer.Write(buf[:n])
remaining -= int64(n)
}
return
}
}
// 普通请求,发送整个文件
c.Header("Content-Length", strconv.FormatInt(fileSize, 10))
c.Status(http.StatusOK)
// 使用缓冲区高效传输
buf := make([]byte, fileBufferSize)
_, err = io.CopyBuffer(c.Writer, file, buf)
if err != nil {
log.Printf("Error sending file %s: %v", cleanPath, err)
}
}
// mimeTypes 常见 MIME 类型映射
var mimeTypes = map[string]string{
".zip": "application/zip",
".exe": "application/vnd.microsoft.windows.executable",
".dll": "application/vnd.microsoft.windows.dll",
".apk": "application/vnd.android.package-archive",
".ipa": "application/vnd.apple.ipa",
".dmg": "application/vnd.apple.dmg",
".pdf": "application/pdf",
".doc": "application/msword",
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".xls": "application/vnd.ms-excel",
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".ppt": "application/vnd.ms-powerpoint",
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
".mp4": "video/mp4",
".mp3": "audio/mpeg",
".wav": "audio/wav",
".ogg": "audio/ogg",
".flac": "audio/flac",
".avi": "video/x-msvideo",
".mkv": "video/x-matroska",
".mov": "video/quicktime",
".wmv": "video/x-ms-wmv",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".bmp": "image/bmp",
".webp": "image/webp",
".svg": "image/svg+xml",
".ico": "image/x-icon",
".html": "text/html; charset=utf-8",
".htm": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "application/javascript; charset=utf-8",
".mjs": "application/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".xml": "application/xml; charset=utf-8",
".txt": "text/plain; charset=utf-8",
".md": "text/markdown; charset=utf-8",
".log": "text/plain; charset=utf-8",
".csv": "text/csv; charset=utf-8",
".tar": "application/x-tar",
".gz": "application/gzip",
".tgz": "application/gzip",
".rar": "application/vnd.rar",
".7z": "application/x-7z-compressed",
".iso": "application/x-iso9660-image",
".woff": "font/woff",
".woff2": "font/woff2",
".ttf": "font/ttf",
".eot": "application/vnd.ms-fontobject",
".otf": "font/otf",
// 使用 http.ServeFile,它已经:
// 1. 支持 Range 请求(断点续传)
// 2. 使用 sendfile 系统调用(零拷贝)
// 3. 自动处理 MIME 类型
// 4. 高效处理大文件
http.ServeFile(c.Writer, c.Request, cleanPath)
}