Files

200 lines
7.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package common
import (
"context"
"net/http"
"net/url"
"sync"
"sync/atomic"
"time"
"github.com/gogf/gf/v2/frame/g"
)
// defaultResponseHeaderTimeout 是普通同步请求等待上游响应头的最长时间。
// 用于在上游「连接已建立却迟迟不回响应头」(假死)时尽早失败,保护短超时请求。
//
// 但对总超时本身就较长的请求(图片/音频生成等:上游需先完成生成才发送响应头),
// 这个头超时会反过来误杀正常的长耗时请求——因此 NewPooledClient 在
// timeoutSeconds > defaultResponseHeaderTimeout 时改用禁用头超时的 longRun 传输层,
// 完全交由 http.Client.Timeout 兜底(见 longRunSharedTransport)。
const defaultResponseHeaderTimeout = 180 * time.Second
// sharedTransport 全局共享的 HTTP 传输层,所有渠道适配器共用一个连接池。
// http.Client 本身是轻量的,真正持有 TCP 连接池的是 Transport。
var sharedTransport = &http.Transport{
MaxIdleConns: 500,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
DisableKeepAlives: false,
ForceAttemptHTTP2: true,
DisableCompression: false,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: defaultResponseHeaderTimeout,
}
// longRunSharedTransport 用于总超时 > defaultResponseHeaderTimeout 的长耗时同步请求
// (如图片/音频生成:上游在生成完成前不会发送响应头)。禁用 ResponseHeaderTimeout,
// 避免被 180s 头超时在中途误杀连接,改由 http.Client.Timeout 统一控制总时长。
var longRunSharedTransport = &http.Transport{
MaxIdleConns: 500,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
DisableKeepAlives: false,
ForceAttemptHTTP2: true,
DisableCompression: false,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 0,
}
// nonStreamClient / streamClient 预创建的单例 Client(无代理)。
// Transport 统一经 WrapDebugTransport 包装:渠道调试开关开启的请求经 ctx 捕获器
// 镜像上游报文,其余请求一次 ctx 查找后直接透传(连接池/H2 仍由底层 Transport 管理)。
var (
nonStreamClient = &http.Client{
Transport: WrapDebugTransport(sharedTransport),
Timeout: 300 * time.Second,
}
streamClient = &http.Client{
Transport: WrapDebugTransport(sharedTransport),
}
)
// proxiedState 管理带代理的 HTTP 客户端,按需初始化并在代理 URL 变更时重建。
var proxiedState struct {
mu sync.RWMutex
proxyURL string
transport *http.Transport // ResponseHeaderTimeout=defaultResponseHeaderTimeout(普通)
longRun *http.Transport // ResponseHeaderTimeout=0(长耗时同步)
nonStream *http.Client
stream *http.Client
proxyURLValue atomic.Value // string,系统代理 URL(sys_options channel_proxy_url,由 internal 注入)
}
// SetSystemProxyURL 注入系统代理 URL(sys_options 的 channel_proxy_url)。
// relay 包不 import internal(会循环依赖),由 internal 侧在启动时与配置变更时
// (OnSettingsChanged,免重启)注入,模式同 SetGlobalRequestTimeoutSeconds。
func SetSystemProxyURL(proxyURL string) {
proxiedState.proxyURLValue.Store(proxyURL)
}
// GetSystemProxyURL 返回系统代理 URL(sys_options 的 channel_proxy_url,空串=未配置)。
// 导出供 WebSocket 拨号器等非 HTTP 客户端调用方使用。
func GetSystemProxyURL() string {
if v, ok := proxiedState.proxyURLValue.Load().(string); ok {
return v
}
return ""
}
// buildProxiedTransport 构建一个带(可选)代理的传输层,ResponseHeaderTimeout 由 rht 决定。
func buildProxiedTransport(proxyURL string, rht time.Duration) *http.Transport {
transport := &http.Transport{
MaxIdleConns: 500,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
DisableKeepAlives: false,
ForceAttemptHTTP2: true,
DisableCompression: false,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: rht,
}
if proxyURL != "" {
parsed, err := url.Parse(proxyURL)
if err != nil {
g.Log().Warningf(context.Background(), "proxy: invalid proxy URL %q: %v", proxyURL, err)
} else {
transport.Proxy = http.ProxyURL(parsed)
}
}
return transport
}
// getProxiedClients 返回基于当前代理配置的 (nonStream, stream) 客户端。
// 代理 URL 变更时重建传输层。同时维护一个禁用响应头超时的 longRun 传输层,
// 供 NewPooledClient 在长超时请求时取用。
func getProxiedClients() (*http.Client, *http.Client) {
proxyURL := GetSystemProxyURL()
proxiedState.mu.RLock()
if proxiedState.transport != nil && proxiedState.proxyURL == proxyURL {
ns, s := proxiedState.nonStream, proxiedState.stream
proxiedState.mu.RUnlock()
return ns, s
}
proxiedState.mu.RUnlock()
proxiedState.mu.Lock()
defer proxiedState.mu.Unlock()
// 取得写锁后二次检查
if proxiedState.transport != nil && proxiedState.proxyURL == proxyURL {
return proxiedState.nonStream, proxiedState.stream
}
// 构建新的代理传输层(普通 + 长耗时同步各一份)
proxiedState.transport = buildProxiedTransport(proxyURL, defaultResponseHeaderTimeout)
proxiedState.longRun = buildProxiedTransport(proxyURL, 0)
proxiedState.nonStream = &http.Client{
Transport: WrapDebugTransport(proxiedState.transport),
Timeout: 300 * time.Second,
}
proxiedState.stream = &http.Client{
Transport: WrapDebugTransport(proxiedState.transport),
}
proxiedState.proxyURL = proxyURL
return proxiedState.nonStream, proxiedState.stream
}
// NewPooledClient 返回带连接池的 http.Client。
// useProxy=true:使用 channel_proxy_url 中配置的系统代理。
// isStream=true:不设置 Client.Timeout,由 StreamScanner 管理。
//
// 当 timeoutSeconds > defaultResponseHeaderTimeout(180s)时,使用禁用响应头超时的
// longRun 传输层——图片/音频等长耗时同步请求的上游在生成完成前不发响应头,
// 否则会被 180s 头超时误杀。该阈值由 GetTimeoutSeconds 对图片模式强制下限到 600s 自然满足,
// 因此各适配器无需改动即可受益。
func NewPooledClient(timeoutSeconds int, useProxy bool, isStream ...bool) *http.Client {
isStreamReq := len(isStream) > 0 && isStream[0]
longRun := timeoutSeconds > int(defaultResponseHeaderTimeout/time.Second)
if useProxy {
ns, s := getProxiedClients()
if isStreamReq {
return s
}
if timeoutSeconds <= 0 {
return ns
}
proxiedState.mu.RLock()
transport := proxiedState.transport
if longRun {
transport = proxiedState.longRun
}
proxiedState.mu.RUnlock()
return &http.Client{
Transport: WrapDebugTransport(transport),
Timeout: time.Duration(timeoutSeconds) * time.Second,
}
}
if isStreamReq {
return streamClient
}
if timeoutSeconds <= 0 {
return nonStreamClient
}
transport := sharedTransport
if longRun {
transport = longRunSharedTransport
}
return &http.Client{
Transport: WrapDebugTransport(transport),
Timeout: time.Duration(timeoutSeconds) * time.Second,
}
}