增强路径匹配逻辑,添加前缀匹配器以提高性能,同时优化请求头设置和扩展名处理,确保代理请求的兼容性和稳定性。

This commit is contained in:
wood chen 2025-04-17 22:11:15 +08:00
parent c2266a60d6
commit 1d84c0c614
3 changed files with 185 additions and 144 deletions

View File

@ -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
}

View File

@ -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

View File

@ -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