- errcheck: 修复类型断言未检查返回值的问题 - pool.go: 添加 sync.Map 类型断言安全检查 - req_client_pool.go: 添加 sync.Map 类型断言安全检查 - concurrency_cache_benchmark_test.go: 显式忽略断言返回值 - gateway_service.go: 显式忽略 WriteString 返回值 - gofmt: 修复代码格式问题 - redis.go: 注释对齐 - api_key_repo.go: 结构体字段对齐 - concurrency_cache.go: 字段对齐 - http_upstream.go: 注释对齐 - unused: 删除未使用的代码 - user_repo.go: 删除未使用的 sql 字段 - usage_service.go: 删除未使用的 calculateStats 函数 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
158 lines
4.4 KiB
Go
158 lines
4.4 KiB
Go
// Package httpclient 提供共享 HTTP 客户端池
|
||
//
|
||
// 性能优化说明:
|
||
// 原实现在多个服务中重复创建 http.Client:
|
||
// 1. proxy_probe_service.go: 每次探测创建新客户端
|
||
// 2. pricing_service.go: 每次请求创建新客户端
|
||
// 3. turnstile_service.go: 每次验证创建新客户端
|
||
// 4. github_release_service.go: 每次请求创建新客户端
|
||
// 5. claude_usage_service.go: 每次请求创建新客户端
|
||
//
|
||
// 新实现使用统一的客户端池:
|
||
// 1. 相同配置复用同一 http.Client 实例
|
||
// 2. 复用 Transport 连接池,减少 TCP/TLS 握手开销
|
||
// 3. 支持 HTTP/HTTPS/SOCKS5 代理
|
||
// 4. 支持严格代理模式(代理失败则返回错误)
|
||
package httpclient
|
||
|
||
import (
|
||
"context"
|
||
"crypto/tls"
|
||
"fmt"
|
||
"net"
|
||
"net/http"
|
||
"net/url"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"golang.org/x/net/proxy"
|
||
)
|
||
|
||
// Transport 连接池默认配置
|
||
const (
|
||
defaultMaxIdleConns = 100 // 最大空闲连接数
|
||
defaultMaxIdleConnsPerHost = 10 // 每个主机最大空闲连接数
|
||
defaultIdleConnTimeout = 90 * time.Second // 空闲连接超时时间
|
||
)
|
||
|
||
// Options 定义共享 HTTP 客户端的构建参数
|
||
type Options struct {
|
||
ProxyURL string // 代理 URL(支持 http/https/socks5)
|
||
Timeout time.Duration // 请求总超时时间
|
||
ResponseHeaderTimeout time.Duration // 等待响应头超时时间
|
||
InsecureSkipVerify bool // 是否跳过 TLS 证书验证
|
||
ProxyStrict bool // 严格代理模式:代理失败时返回错误而非回退
|
||
|
||
// 可选的连接池参数(不设置则使用默认值)
|
||
MaxIdleConns int // 最大空闲连接总数(默认 100)
|
||
MaxIdleConnsPerHost int // 每主机最大空闲连接(默认 10)
|
||
MaxConnsPerHost int // 每主机最大连接数(默认 0 无限制)
|
||
}
|
||
|
||
// sharedClients 存储按配置参数缓存的 http.Client 实例
|
||
var sharedClients sync.Map
|
||
|
||
// GetClient 返回共享的 HTTP 客户端实例
|
||
// 性能优化:相同配置复用同一客户端,避免重复创建 Transport
|
||
func GetClient(opts Options) (*http.Client, error) {
|
||
key := buildClientKey(opts)
|
||
if cached, ok := sharedClients.Load(key); ok {
|
||
if client, ok := cached.(*http.Client); ok {
|
||
return client, nil
|
||
}
|
||
}
|
||
|
||
client, err := buildClient(opts)
|
||
if err != nil {
|
||
if opts.ProxyStrict {
|
||
return nil, err
|
||
}
|
||
fallback := opts
|
||
fallback.ProxyURL = ""
|
||
client, _ = buildClient(fallback)
|
||
}
|
||
|
||
actual, _ := sharedClients.LoadOrStore(key, client)
|
||
if c, ok := actual.(*http.Client); ok {
|
||
return c, nil
|
||
}
|
||
return client, nil
|
||
}
|
||
|
||
func buildClient(opts Options) (*http.Client, error) {
|
||
transport, err := buildTransport(opts)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &http.Client{
|
||
Transport: transport,
|
||
Timeout: opts.Timeout,
|
||
}, nil
|
||
}
|
||
|
||
func buildTransport(opts Options) (*http.Transport, error) {
|
||
// 使用自定义值或默认值
|
||
maxIdleConns := opts.MaxIdleConns
|
||
if maxIdleConns <= 0 {
|
||
maxIdleConns = defaultMaxIdleConns
|
||
}
|
||
maxIdleConnsPerHost := opts.MaxIdleConnsPerHost
|
||
if maxIdleConnsPerHost <= 0 {
|
||
maxIdleConnsPerHost = defaultMaxIdleConnsPerHost
|
||
}
|
||
|
||
transport := &http.Transport{
|
||
MaxIdleConns: maxIdleConns,
|
||
MaxIdleConnsPerHost: maxIdleConnsPerHost,
|
||
MaxConnsPerHost: opts.MaxConnsPerHost, // 0 表示无限制
|
||
IdleConnTimeout: defaultIdleConnTimeout,
|
||
ResponseHeaderTimeout: opts.ResponseHeaderTimeout,
|
||
}
|
||
|
||
if opts.InsecureSkipVerify {
|
||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||
}
|
||
|
||
proxyURL := strings.TrimSpace(opts.ProxyURL)
|
||
if proxyURL == "" {
|
||
return transport, nil
|
||
}
|
||
|
||
parsed, err := url.Parse(proxyURL)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
switch strings.ToLower(parsed.Scheme) {
|
||
case "http", "https":
|
||
transport.Proxy = http.ProxyURL(parsed)
|
||
case "socks5", "socks5h":
|
||
dialer, err := proxy.FromURL(parsed, proxy.Direct)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||
return dialer.Dial(network, addr)
|
||
}
|
||
default:
|
||
return nil, fmt.Errorf("unsupported proxy protocol: %s", parsed.Scheme)
|
||
}
|
||
|
||
return transport, nil
|
||
}
|
||
|
||
func buildClientKey(opts Options) string {
|
||
return fmt.Sprintf("%s|%s|%s|%t|%t|%d|%d|%d",
|
||
strings.TrimSpace(opts.ProxyURL),
|
||
opts.Timeout.String(),
|
||
opts.ResponseHeaderTimeout.String(),
|
||
opts.InsecureSkipVerify,
|
||
opts.ProxyStrict,
|
||
opts.MaxIdleConns,
|
||
opts.MaxIdleConnsPerHost,
|
||
opts.MaxConnsPerHost,
|
||
)
|
||
}
|