perf: 大幅优化静态文件下载性能
- 使用 http.ServeFile 替代手动实现,利用 sendfile 零拷贝 - gzip 中间件跳过 /uploads/ 路径,避免额外开销 - 支持 Range 请求和断点续传(由标准库自动处理) - 移除不必要的手动 MIME 类型映射 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+14
-171
@@ -4,14 +4,12 @@ import (
|
|||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"verification-platform-backend/internal/config"
|
"verification-platform-backend/internal/config"
|
||||||
"verification-platform-backend/internal/database"
|
"verification-platform-backend/internal/database"
|
||||||
@@ -388,6 +386,12 @@ func isCompressibleContentType(contentType string) bool {
|
|||||||
|
|
||||||
func gzipMiddleware() gin.HandlerFunc {
|
func gzipMiddleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
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") {
|
if !strings.Contains(c.GetHeader("Accept-Encoding"), "gzip") {
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
@@ -443,11 +447,7 @@ func (w *gzipResponseWriter) WriteString(s string) (int, error) {
|
|||||||
return w.ResponseWriter.WriteString(s)
|
return w.ResponseWriter.WriteString(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 32KB 缓冲区用于文件传输
|
// handleOptimizedStaticFile 优化的静态文件处理,使用 http.ServeFile 实现最高性能
|
||||||
// 1MB 缓冲区用于高速文件传输,适合 G 口带宽和大文件
|
|
||||||
const fileBufferSize = 1024 * 1024
|
|
||||||
|
|
||||||
// handleOptimizedStaticFile 优化的静态文件处理,支持 Range 请求和断点续传
|
|
||||||
func handleOptimizedStaticFile(c *gin.Context) {
|
func handleOptimizedStaticFile(c *gin.Context) {
|
||||||
// 获取请求的文件路径
|
// 获取请求的文件路径
|
||||||
filepathParam := c.Param("filepath")
|
filepathParam := c.Param("filepath")
|
||||||
@@ -467,170 +467,13 @@ func handleOptimizedStaticFile(c *gin.Context) {
|
|||||||
return
|
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")
|
c.Header("Cache-Control", "public, max-age=31536000, immutable")
|
||||||
|
|
||||||
// 获取 MIME 类型
|
// 使用 http.ServeFile,它已经:
|
||||||
ext := strings.ToLower(filepath.Ext(cleanPath))
|
// 1. 支持 Range 请求(断点续传)
|
||||||
if mimeType, ok := mimeTypes[ext]; ok {
|
// 2. 使用 sendfile 系统调用(零拷贝)
|
||||||
c.Header("Content-Type", mimeType)
|
// 3. 自动处理 MIME 类型
|
||||||
} else {
|
// 4. 高效处理大文件
|
||||||
c.Header("Content-Type", "application/octet-stream")
|
http.ServeFile(c.Writer, c.Request, cleanPath)
|
||||||
}
|
|
||||||
|
|
||||||
// 处理 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",
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user