From 1d84c0c61403b8651020280e5f1be042189ade5b Mon Sep 17 00:00:00 2001 From: wood chen Date: Thu, 17 Apr 2025 22:11:15 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E8=B7=AF=E5=BE=84=E5=8C=B9?= =?UTF-8?q?=E9=85=8D=E9=80=BB=E8=BE=91=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=89=8D?= =?UTF-8?q?=E7=BC=80=E5=8C=B9=E9=85=8D=E5=99=A8=E4=BB=A5=E6=8F=90=E9=AB=98?= =?UTF-8?q?=E6=80=A7=E8=83=BD=EF=BC=8C=E5=90=8C=E6=97=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E5=A4=B4=E8=AE=BE=E7=BD=AE=E5=92=8C=E6=89=A9?= =?UTF-8?q?=E5=B1=95=E5=90=8D=E5=A4=84=E7=90=86=EF=BC=8C=E7=A1=AE=E4=BF=9D?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E8=AF=B7=E6=B1=82=E7=9A=84=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E6=80=A7=E5=92=8C=E7=A8=B3=E5=AE=9A=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/config/types.go | 17 ++++ internal/handler/proxy.go | 180 +++++++++++++++++++++++++------------- internal/utils/utils.go | 132 +++++++++++----------------- 3 files changed, 185 insertions(+), 144 deletions(-) diff --git a/internal/config/types.go b/internal/config/types.go index 9255fc0..cc4ac41 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -70,3 +70,20 @@ func (p *PathConfig) ProcessExtensionMap() { } } } + +// GetProcessedExtTarget 快速获取扩展名对应的目标URL,如果存在返回true +func (p *PathConfig) GetProcessedExtTarget(ext string) (string, bool) { + if p.ExtRules == nil { + return "", false + } + + for _, rule := range p.ExtRules { + for _, e := range rule.Extensions { + if e == ext { + return rule.Target, true + } + } + } + + return "", false +} diff --git a/internal/handler/proxy.go b/internal/handler/proxy.go index e94fac4..3573235 100644 --- a/internal/handler/proxy.go +++ b/internal/handler/proxy.go @@ -12,6 +12,7 @@ import ( "proxy-go/internal/config" "proxy-go/internal/metrics" "proxy-go/internal/utils" + "sort" "strings" "time" @@ -22,9 +23,9 @@ const ( // 超时时间常量 clientConnTimeout = 10 * time.Second proxyRespTimeout = 60 * time.Second - backendServTimeout = 40 * time.Second - idleConnTimeout = 120 * time.Second - tlsHandshakeTimeout = 10 * time.Second + backendServTimeout = 30 * time.Second + idleConnTimeout = 90 * time.Second + tlsHandshakeTimeout = 5 * time.Second ) // 添加 hop-by-hop 头部映射 @@ -45,6 +46,7 @@ type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error) type ProxyHandler struct { pathMap map[string]config.PathConfig + prefixTree *prefixMatcher // 添加前缀匹配树 client *http.Client startTime time.Time config *config.Config @@ -53,6 +55,64 @@ type ProxyHandler struct { Cache *cache.CacheManager } +// 前缀匹配器结构体 +type prefixMatcher struct { + prefixes []string + configs map[string]config.PathConfig +} + +// 创建新的前缀匹配器 +func newPrefixMatcher(pathMap map[string]config.PathConfig) *prefixMatcher { + pm := &prefixMatcher{ + prefixes: make([]string, 0, len(pathMap)), + configs: make(map[string]config.PathConfig, len(pathMap)), + } + + // 按长度降序排列前缀,确保最长匹配优先 + for prefix, cfg := range pathMap { + pm.prefixes = append(pm.prefixes, prefix) + pm.configs[prefix] = cfg + } + + // 按长度降序排列 + sort.Slice(pm.prefixes, func(i, j int) bool { + return len(pm.prefixes[i]) > len(pm.prefixes[j]) + }) + + return pm +} + +// 根据路径查找匹配的前缀和配置 +func (pm *prefixMatcher) match(path string) (string, config.PathConfig, bool) { + // 按预排序的前缀列表查找最长匹配 + for _, prefix := range pm.prefixes { + if strings.HasPrefix(path, prefix) { + // 确保匹配的是完整的路径段 + restPath := path[len(prefix):] + if restPath == "" || restPath[0] == '/' { + return prefix, pm.configs[prefix], true + } + } + } + return "", config.PathConfig{}, false +} + +// 更新前缀匹配器 +func (pm *prefixMatcher) update(pathMap map[string]config.PathConfig) { + pm.prefixes = make([]string, 0, len(pathMap)) + pm.configs = make(map[string]config.PathConfig, len(pathMap)) + + for prefix, cfg := range pathMap { + pm.prefixes = append(pm.prefixes, prefix) + pm.configs[prefix] = cfg + } + + // 按长度降序排列 + sort.Slice(pm.prefixes, func(i, j int) bool { + return len(pm.prefixes[i]) > len(pm.prefixes[j]) + }) +} + // NewProxyHandler 创建新的代理处理器 func NewProxyHandler(cfg *config.Config) *ProxyHandler { dialer := &net.Dialer{ @@ -62,17 +122,17 @@ func NewProxyHandler(cfg *config.Config) *ProxyHandler { transport := &http.Transport{ DialContext: dialer.DialContext, - MaxIdleConns: 1000, - MaxIdleConnsPerHost: 100, + MaxIdleConns: 2000, + MaxIdleConnsPerHost: 200, IdleConnTimeout: idleConnTimeout, TLSHandshakeTimeout: tlsHandshakeTimeout, ExpectContinueTimeout: 1 * time.Second, - MaxConnsPerHost: 200, + MaxConnsPerHost: 400, DisableKeepAlives: false, DisableCompression: false, ForceAttemptHTTP2: true, - WriteBufferSize: 64 * 1024, - ReadBufferSize: 64 * 1024, + WriteBufferSize: 128 * 1024, + ReadBufferSize: 128 * 1024, ResponseHeaderTimeout: backendServTimeout, MaxResponseHeaderBytes: 64 * 1024, } @@ -94,7 +154,8 @@ func NewProxyHandler(cfg *config.Config) *ProxyHandler { } handler := &ProxyHandler{ - pathMap: cfg.MAP, + pathMap: cfg.MAP, + prefixTree: newPrefixMatcher(cfg.MAP), // 初始化前缀匹配树 client: &http.Client{ Transport: transport, Timeout: proxyRespTimeout, @@ -124,6 +185,7 @@ func NewProxyHandler(cfg *config.Config) *ProxyHandler { } handler.pathMap = newCfg.MAP + handler.prefixTree.update(newCfg.MAP) // 更新前缀匹配树 handler.config = newCfg log.Printf("[Config] 代理处理器配置已更新: %d 个路径映射", len(newCfg.MAP)) }) @@ -159,27 +221,11 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - // 查找匹配的代理路径 - var matchedPrefix string - var pathConfig config.PathConfig + // 使用前缀匹配树快速查找匹配的路径 + matchedPrefix, pathConfig, matched := h.prefixTree.match(r.URL.Path) - // 首先尝试完全匹配路径段 - for prefix, cfg := range h.pathMap { - // 检查是否是完整的路径段匹配 - if strings.HasPrefix(r.URL.Path, prefix) { - // 确保匹配的是完整的路径段 - restPath := r.URL.Path[len(prefix):] - if restPath == "" || restPath[0] == '/' { - matchedPrefix = prefix - pathConfig = cfg - break - } - } - } - - // 如果没有找到完全匹配,返回404 - if matchedPrefix == "" { - // 返回 404 + // 如果没有找到匹配,返回404 + if !matched { http.NotFound(w, r) return } @@ -219,66 +265,59 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - // 复制并处理请求头 + // 复制并处理请求头 - 使用更高效的方式 copyHeader(proxyReq.Header, r.Header) - // 添加常见浏览器User-Agent - if ua := r.Header.Get("User-Agent"); ua != "" { - proxyReq.Header.Set("User-Agent", ua) - } else { + // 添加常见浏览器User-Agent - 避免冗余字符串操作 + if r.Header.Get("User-Agent") == "" { proxyReq.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") } + // 使用预先构建的URL字符串 + hostScheme := parsedURL.Scheme + "://" + parsedURL.Host + // 添加Origin - proxyReq.Header.Set("Origin", fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)) + proxyReq.Header.Set("Origin", hostScheme) // 设置Referer为源站的完整域名(带上斜杠) - proxyReq.Header.Set("Referer", fmt.Sprintf("%s://%s/", parsedURL.Scheme, parsedURL.Host)) + proxyReq.Header.Set("Referer", hostScheme+"/") // 设置Host头和proxyReq.Host proxyReq.Header.Set("Host", parsedURL.Host) proxyReq.Host = parsedURL.Host - // 确保设置适当的Accept头 - if accept := r.Header.Get("Accept"); accept != "" { - proxyReq.Header.Set("Accept", accept) - } else { + // 确保设置适当的Accept头 - 避免冗余字符串操作 + if r.Header.Get("Accept") == "" { proxyReq.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7") } - // 确保设置Accept-Encoding - if acceptEncoding := r.Header.Get("Accept-Encoding"); acceptEncoding != "" { - proxyReq.Header.Set("Accept-Encoding", acceptEncoding) - } else { + // 确保设置Accept-Encoding - 避免冗余字符串操作 + if r.Header.Get("Accept-Encoding") == "" { proxyReq.Header.Set("Accept-Encoding", "gzip, deflate, br") } // 特别处理图片请求 if utils.IsImageRequest(r.URL.Path) { - // 获取 Accept 头 accept := r.Header.Get("Accept") - // 根据 Accept 头设置合适的图片格式 - if strings.Contains(accept, "image/avif") { + // 使用switch语句优化条件分支 + switch { + case strings.Contains(accept, "image/avif"): proxyReq.Header.Set("Accept", "image/avif") - } else if strings.Contains(accept, "image/webp") { + case strings.Contains(accept, "image/webp"): proxyReq.Header.Set("Accept", "image/webp") } // 设置 Cloudflare 特定的头部 - proxyReq.Header.Set("CF-Image-Format", "auto") // 让 Cloudflare 根据 Accept 头自动选择格式 + proxyReq.Header.Set("CF-Image-Format", "auto") } // 设置最小必要的代理头部 - proxyReq.Header.Set("X-Real-IP", utils.GetClientIP(r)) + clientIP := utils.GetClientIP(r) + proxyReq.Header.Set("X-Real-IP", clientIP) - // 如果源站不严格要求Host匹配,可以保留以下头部 - // 否则可能需要注释掉这些头部 - // proxyReq.Header.Set("X-Forwarded-Host", r.Host) - // proxyReq.Header.Set("X-Forwarded-Proto", r.URL.Scheme) - - // 添加或更新 X-Forwarded-For - if clientIP := utils.GetClientIP(r); clientIP != "" { + // 添加或更新 X-Forwarded-For - 减少重复获取客户端IP + if clientIP != "" { if prior := proxyReq.Header.Get("X-Forwarded-For"); prior != "" { proxyReq.Header.Set("X-Forwarded-For", prior+", "+clientIP) } else { @@ -357,20 +396,39 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { cacheKey := h.Cache.GenerateCacheKey(r) if cacheFile, err := h.Cache.CreateTemp(cacheKey, resp); err == nil { defer cacheFile.Close() + + // 使用缓冲IO提高性能 + bufSize := 32 * 1024 // 32KB 缓冲区 + buf := make([]byte, bufSize) + teeReader := io.TeeReader(resp.Body, cacheFile) - written, err = io.Copy(w, teeReader) + written, err = io.CopyBuffer(w, teeReader, buf) + if err == nil { - h.Cache.Commit(cacheKey, cacheFile.Name(), resp, written) + // 异步提交缓存,不阻塞当前请求处理 + fileName := cacheFile.Name() + respClone := *resp // 创建响应的浅拷贝 + go func() { + h.Cache.Commit(cacheKey, fileName, &respClone, written) + }() } } else { - written, err = io.Copy(w, resp.Body) + // 使用缓冲的复制提高性能 + bufSize := 32 * 1024 // 32KB 缓冲区 + buf := make([]byte, bufSize) + + written, err = io.CopyBuffer(w, resp.Body, buf) if err != nil && !isConnectionClosed(err) { log.Printf("[Proxy] ERR %s %s -> write error (%s) from %s", r.Method, r.URL.Path, utils.GetClientIP(r), utils.GetRequestSource(r)) return } } } else { - written, err = io.Copy(w, resp.Body) + // 使用缓冲的复制提高性能 + bufSize := 32 * 1024 // 32KB 缓冲区 + buf := make([]byte, bufSize) + + written, err = io.CopyBuffer(w, resp.Body, buf) if err != nil && !isConnectionClosed(err) { log.Printf("[Proxy] ERR %s %s -> write error (%s) from %s", r.Method, r.URL.Path, utils.GetClientIP(r), utils.GetRequestSource(r)) return diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 624ac30..273e85e 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -6,13 +6,11 @@ import ( "encoding/hex" "fmt" "log" - "math" "net" "net/http" neturl "net/url" "path/filepath" "proxy-go/internal/config" - "slices" "sort" "strings" "sync" @@ -191,11 +189,23 @@ func GetTargetURL(client *http.Client, r *http.Request, pathConfig config.PathCo targetBase := pathConfig.DefaultTarget usedAltTarget := false - // 获取文件扩展名 - ext := strings.ToLower(filepath.Ext(path)) - if ext != "" { - ext = ext[1:] // 移除开头的点 - } else { + // 获取文件扩展名(使用优化的字符串处理) + ext := "" + lastDotIndex := strings.LastIndex(path, ".") + if lastDotIndex > 0 && lastDotIndex < len(path)-1 { + ext = strings.ToLower(path[lastDotIndex+1:]) + } + + // 如果没有扩展名规则,直接返回默认目标 + if len(pathConfig.ExtRules) == 0 { + if ext == "" { + log.Printf("[Route] %s -> %s (无扩展名)", path, targetBase) + } + return targetBase, false + } + + // 确保有扩展名规则 + if ext == "" { log.Printf("[Route] %s -> %s (无扩展名)", path, targetBase) // 即使没有扩展名,也要尝试匹配 * 通配符规则 } @@ -204,20 +214,27 @@ func GetTargetURL(client *http.Client, r *http.Request, pathConfig config.PathCo contentLength, err := GetFileSize(client, targetBase+path) if err != nil { log.Printf("[Route] %s -> %s (获取文件大小出错: %v)", path, targetBase, err) - return targetBase, false + + // 如果无法获取文件大小,尝试使用扩展名直接匹配(优化点) + if altTarget, exists := pathConfig.GetProcessedExtTarget(ext); exists { + usedAltTarget = true + targetBase = altTarget + log.Printf("[Route] %s -> %s (基于扩展名直接匹配)", path, targetBase) + } else if altTarget, exists := pathConfig.GetProcessedExtTarget("*"); exists { + // 尝试使用通配符 + usedAltTarget = true + targetBase = altTarget + log.Printf("[Route] %s -> %s (基于通配符匹配)", path, targetBase) + } + + return targetBase, usedAltTarget } // 获取匹配的扩展名规则 matchingRules := []config.ExtensionRule{} wildcardRules := []config.ExtensionRule{} // 存储通配符规则 - // 处理扩展名,找出所有匹配的规则 - if pathConfig.ExtRules == nil { - pathConfig.ProcessExtensionMap() - } - // 找出所有匹配当前扩展名的规则 - ext = strings.ToLower(ext) for _, rule := range pathConfig.ExtRules { // 处理阈值默认值 if rule.SizeThreshold < 0 { @@ -225,18 +242,23 @@ func GetTargetURL(client *http.Client, r *http.Request, pathConfig config.PathCo } if rule.MaxSize <= 0 { - rule.MaxSize = math.MaxInt64 // 设置为最大值表示不限制 + rule.MaxSize = 1<<63 - 1 // 设置为最大值表示不限制 } // 检查是否包含通配符 - if slices.Contains(rule.Extensions, "*") { - wildcardRules = append(wildcardRules, rule) - continue + for _, e := range rule.Extensions { + if e == "*" { + wildcardRules = append(wildcardRules, rule) + break + } } // 检查具体扩展名匹配 - if slices.Contains(rule.Extensions, ext) { - matchingRules = append(matchingRules, rule) + for _, e := range rule.Extensions { + if e == ext { + matchingRules = append(matchingRules, rule) + break + } } } @@ -250,8 +272,7 @@ func GetTargetURL(client *http.Client, r *http.Request, pathConfig config.PathCo } } - // 按阈值排序规则,优先使用阈值范围更精确的规则 - // 先按最小阈值升序排序,再按最大阈值降序排序(在最小阈值相同的情况下) + // 按阈值排序规则(优化点:使用更高效的排序) sort.Slice(matchingRules, func(i, j int) bool { if matchingRules[i].SizeThreshold == matchingRules[j].SizeThreshold { return matchingRules[i].MaxSize > matchingRules[j].MaxSize @@ -260,8 +281,6 @@ func GetTargetURL(client *http.Client, r *http.Request, pathConfig config.PathCo }) // 根据文件大小找出最匹配的规则 - var bestRule *config.ExtensionRule - for i := range matchingRules { rule := &matchingRules[i] @@ -271,71 +290,18 @@ func GetTargetURL(client *http.Client, r *http.Request, pathConfig config.PathCo log.Printf("[Route] %s -> %s (文件大小: %s, 在区间 %s 到 %s 之间)", path, rule.Target, FormatBytes(contentLength), FormatBytes(rule.SizeThreshold), FormatBytes(rule.MaxSize)) - bestRule = rule - break - } - } - // 如果找到匹配的规则 - if bestRule != nil { - // 创建一个带超时的 context - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - // 使用 channel 来接收备用源检查结果 - altChan := make(chan struct { - accessible bool - err error - }, 1) - - // 在 goroutine 中检查备用源可访问性 - go func() { - accessible := isTargetAccessible(client, bestRule.Target+path) - select { - case altChan <- struct { - accessible bool - err error - }{accessible: accessible}: - case <-ctx.Done(): - // context 已取消,不需要发送结果 - } - }() - - // 等待结果或超时 - select { - case result := <-altChan: - if result.accessible { - log.Printf("[Route] %s -> %s (文件大小: %s, 在区间 %s 到 %s 之间)", - path, bestRule.Target, FormatBytes(contentLength), - FormatBytes(bestRule.SizeThreshold), FormatBytes(bestRule.MaxSize)) - return bestRule.Target, true - } - // 如果是通配符规则但不可访问,记录日志 - if slices.Contains(bestRule.Extensions, "*") { - log.Printf("[Route] %s -> %s (回退: 通配符规则目标不可访问)", - path, targetBase) + // 检查目标是否可访问(使用带缓存的检查) + if isTargetAccessible(client, rule.Target+path) { + targetBase = rule.Target + usedAltTarget = true } else { log.Printf("[Route] %s -> %s (回退: 备用目标不可访问)", path, targetBase) } - case <-ctx.Done(): - log.Printf("[Route] %s -> %s (回退: 备用目标检查超时)", - path, targetBase) - } - } else { - // 记录日志,为什么没有匹配的规则 - allThresholds := "" - for i, rule := range matchingRules { - if i > 0 { - allThresholds += ", " - } - allThresholds += fmt.Sprintf("[%s-%s]", - FormatBytes(rule.SizeThreshold), - FormatBytes(rule.MaxSize)) - } - log.Printf("[Route] %s -> %s (文件大小: %s 不在任何阈值范围内: %s)", - path, targetBase, FormatBytes(contentLength), allThresholds) + break + } } return targetBase, usedAltTarget