Skip to content

代理与代理池

无论是穿越企业网络、为采集任务轮换出口 IP,还是规避目标站点的 IP 封锁,代理都是 HTTP 客户端的高频需求。HTTPC 内置四种代理模式——单代理、系统代理检测、代理池轮换、状态码触发轮换——覆盖从「固定出口」到「每请求换 IP」的完整谱系,并与 SSRF 防护、TLS 校验、重试引擎协同工作。全部代理配置集中在 ConnectionConfig

代理模式总览

四种模式按优先级自动生效,同时配置多种时只有最高优先级者生效:

优先级配置行为典型场景
1(最高)ProxyURL始终使用指定代理(单代理模式)企业网络出口、本地 VPN 端口
2ProxyPool在代理池中轮换,含熔断与恢复采集、负载分散、IP 轮换
3EnableSystemProxy自动检测系统代理设置桌面应用跟随用户配置
4(最低)直连默认行为

TIP

若同时设置 ProxyURLProxyPoolProxyURL 生效。要用代理池,请清空 ProxyURL

单代理配置

ProxyURL 指定一个固定代理,支持四种协议:

协议写法说明
HTTPhttp://proxy:8080最常见;HTTPS 请求经 CONNECT 隧道转发
HTTPShttps://proxy:8443与代理服务器本身也走 TLS
SOCKS5socks5://proxy:1080本地解析目标域名后经代理连接
SOCKS5hsocks5h://proxy:1080域名解析交给代理端,规避本地 DNS 污染
go
package main

import (
    "log"

    "github.com/cybergodev/httpc"
)

func main() {
    cfg := httpc.DefaultConfig()
    cfg.Connection.ProxyURL = "socks5://proxy.example.com:1080"

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    result, err := client.Get("https://api.example.com/data")
    if err != nil {
        log.Fatal(err)
    }
    // 输出:状态: 200, 代理: socks5://proxy.example.com:1080
    log.Printf("状态: %d, 代理: %s", result.StatusCode(), result.Meta.ProxyURL)
}

认证与脱敏

代理凭据直接写在 URL 的 userinfo 部分:

go
cfg := httpc.DefaultConfig()
cfg.Connection.ProxyURL = "http://user:[email protected]:8080"

凭据自动脱敏

Config.String() 会把代理 URL 中的用户名和密码替换为 ***:***;错误信息与日志中的 URL 同样自动脱敏(凭据与敏感查询参数打码)。凭据不会泄漏进日志,但配置本身仍需妥善保管。

系统代理检测与 NO_PROXY

启用后自动检测操作系统的代理设置,无需手动指定 ProxyURL

go
cfg := httpc.DefaultConfig()
cfg.Connection.EnableSystemProxy = true

平台差异

平台检测来源
Windows注册表 Internet Settings(ProxyEnable / ProxyServer
macOSnetworksetup 命令读取首选网络服务的 Web/Secure Web Proxy
Linux环境变量 HTTP_PROXY / HTTPS_PROXY

Meta.ProxyURL 不含系统代理

系统代理的选择不记录到 Result.Meta.ProxyURL(该字段仅在显式 Connection.ProxyURLProxyPool 下有值,直连与系统代理均为空)。需要逐请求确认出口代理时,请改用显式配置。

检测顺序与细节

  1. 环境变量优先(全平台):先读 HTTP_PROXY / HTTPS_PROXY / NO_PROXY(大小写均识别),有值则直接使用,不再查系统设置。
  2. 平台检测兜底:无环境变量时读取平台设置。Linux 桌面(GNOME/KDE)的代理通常已由会话导出为环境变量,引擎不直接读 gsettings/dconf。
  3. 请求分流:HTTPS 请求优先用 HTTPS_PROXY,未设置回退 HTTP_PROXY;HTTP 请求只用 HTTP_PROXY,未设置时也回退 HTTPS_PROXY——与 net/http 的解析顺序一致。
  4. CGI 环境直连:来自环境变量的代理在 CGI 环境(REQUEST_METHOD 已设置)下不生效,与 net/http 行为一致。
  5. 裸地址自动补全HTTP_PROXY=proxy:8080 这类无协议头的值自动按 http:// 处理。
  6. 缓存:检测结果在客户端生命周期内缓存,不随环境变量变化热更新;需要感知变化时新建客户端。

NO_PROXY 绕过规则

NO_PROXY 指定不走代理的主机,语义与 net/http 的 httpproxy 包一致:

规则示例匹配范围
全部绕过*所有主机直连
域名后缀example.com.example.com该域名及其全部子域
通配子域*.example.com等价于 .example.com
IP 字面量10.0.0.5精确匹配该 IP
CIDR 网段10.0.0.0/8网段内全部 IP
主机 + 端口example.com:443主机匹配且端口精确一致

多个规则用逗号分隔;localhost 始终直连,无需写入 NO_PROXY

bash
# Linux/macOS 设置示例
export HTTPS_PROXY=http://proxy.corp.example.com:8080
export NO_PROXY=localhost,127.0.0.1,.internal.corp.com,10.0.0.0/8
powershell
# Windows (PowerShell) 设置示例
$env:HTTPS_PROXY = "http://proxy.corp.example.com:8080"
$env:NO_PROXY = "localhost,127.0.0.1,.internal.corp.com"

动态 localhost 代理的限制

系统代理模式下,SSRF 豁免名单只在客户端构建时探测一次。若运行期间系统代理切换到新的内网/回环地址(如 127.0.0.1),该地址可能被 SSRF 防护拦截。动态场景请显式设置 Connection.ProxyURL(代理地址始终豁免 SSRF 校验)或配置 SSRFExemptCIDRs

代理池

当需要跨多个代理 IP 分发请求(采集、负载分散、IP 轮换),代理池提供自动轮换、被动熔断与按状态码换代理——无需任何外部组件。

基本用法

go
package main

import (
    "log"

    "github.com/cybergodev/httpc"
)

func main() {
    cfg := httpc.DefaultConfig()
    cfg.Connection.ProxyPool = []string{
        "http://proxy1:8080",
        "http://proxy2:8080",
        "http://proxy3:8080",
    }
    cfg.Connection.ProxyPoolStrategy = httpc.ProxyStrategyRoundRobin // 默认值,可省略

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    result, err := client.Get("https://api.example.com/data")
    if err != nil {
        log.Fatal(err)
    }
    // 输出:状态: 200, 代理: http://proxy1:8080(下一次请求自动轮到 proxy2)
    log.Printf("状态: %d, 代理: %s", result.StatusCode(), result.Meta.ProxyURL)
}

池内条目支持 httphttpssocks5socks5h 协议,可以混用。

配置字段

字段类型默认说明
ProxyPool[]stringnil代理 URL 列表
ProxyPoolStrategyProxyStrategyProxyStrategyRoundRobin选择策略
ProxyFailureThresholdint3(0 回退)连续连接失败熔断阈值
ProxyCooldowntime.Duration30s(0 回退)熔断代理冷却时间
ProxyRotatePerRequestboolfalse每次独立请求强制换代理(关闭空闲连接复用)
ProxyRotateOnStatus[]intnil触发换代理重试的状态码(每项须在 100–599)

选择策略

策略常量说明
轮询(默认)ProxyStrategyRoundRobin按顺序循环选择,每次选择推进游标
随机ProxyStrategyRandom从健康代理中均匀随机选取

轮询 + 重试 = 自动换 IP

轮询策略在每次选择时推进游标,因此重试再次触发选择时天然落到下一个代理,无需任何额外配置。

被动熔断

代理池内置被动健康检查。只有连接层失败(dial/TLS)才会触发熔断,HTTP 状态码不会:

text
代理连接失败

失败计数 +1

连续失败 ≥ ProxyFailureThreshold → 熔断(移出轮换)

等待 ProxyCooldown → 半开探测(恢复轮换)

成功 → 重置计数,关闭熔断
首次失败 → 重新熔断
go
cfg.Connection.ProxyFailureThreshold = 5        // 更宽容,容忍偶发抖动
cfg.Connection.ProxyCooldown = 60 * time.Second // 冷却更久

当所有代理都被熔断时,返回冷却时间最短(最接近恢复)的代理作为兜底,而非直接失败。

状态码轮换

用于 Cloudflare/WAF 等 IP 封锁场景——返回特定状态码时自动换代理重试:

go
package main

import (
    "log"

    "github.com/cybergodev/httpc"
)

func main() {
    cfg := httpc.DefaultConfig()
    cfg.Connection.ProxyPool = []string{
        "http://proxy1:8080",
        "http://proxy2:8080",
        "http://proxy3:8080",
    }
    cfg.Connection.ProxyRotateOnStatus = []int{403} // 收到 403 时换代理重试
    cfg.Retry.MaxRetries = 3                        // 必须启用重试

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    result, err := client.Get("https://protected-site.example.com/data")
    if err != nil {
        log.Fatal(err)
    }
    // 输出:状态: 200, 代理: http://proxy2:8080, 尝试: 2
    log.Printf("状态: %d, 代理: %s, 尝试: %d",
        result.StatusCode(), result.Meta.ProxyURL, result.Meta.Attempts)
}

状态码轮换 ≠ 熔断

ProxyRotateOnStatus 触发的轮换不会熔断代理——IP 封锁往往目标特定(一个代理在 A 站被封,在 B 站可能正常)。熔断仅由连接层失败触发。需 Retry.MaxRetries > 0 才生效。

ProxyRotateOnStatus 设置且代理池有多个代理时,重试预算自动提升至 len(ProxyPool) - 1(受 MaxRetries 上限 10 约束),确保每个代理都有机会被尝试。

每请求轮换

ProxyRotatePerRequest 解决的是连接复用导致代理隧道固化的问题:HTTP 连接池会复用已建立的 TCP 连接,包括其代理隧道。这意味着对同一主机的连续请求会复用前一次请求的代理,即使 ProxyPoolStrategy 已轮换选择器游标。

启用后,每次请求开始时关闭所有空闲连接,强制 Transport 重新评估代理池——代价是无连接复用(每次请求新建连接+代理隧道),但保证按请求轮换:

go
package main

import (
    "log"

    "github.com/cybergodev/httpc"
)

func main() {
    cfg := httpc.DefaultConfig()
    cfg.Connection.ProxyPool = []string{
        "http://proxy1:8080",
        "http://proxy2:8080",
        "http://proxy3:8080",
    }
    cfg.Connection.ProxyRotatePerRequest = true // 每次请求换代理

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    for i := 0; i < 3; i++ {
        result, err := client.Get("https://api.example.com/data")
        if err != nil {
            log.Fatal(err)
        }
        // 输出(依次):http://proxy1:8080 / http://proxy2:8080 / http://proxy3:8080
        log.Printf("第 %d 次请求经由: %s", i+1, result.Meta.ProxyURL)
    }
}

适用场景

适用于对同一主机的采集——每次请求的源 IP 不同,降低被目标站点 IP 封锁的风险。对于不同主机的请求,连接复用不会绑定同一代理,通常无需启用。

ProxyRotateOnStatus 一样,ProxyRotatePerRequest 也会在代理池有多个代理时自动提升重试预算至 len(ProxyPool) - 1(上限 10),确保每个代理至少被尝试一次。

确定性轮换

状态码轮换的重试与普通重试不同:引擎为每个请求预留一个基准代理索引,第 N 次重试固定使用「基准 + N」对应的代理,带来三个保证:

  • 重试必换代理:同一请求内,第 N 次重试与第 N-1 次必然落在不同代理上;
  • 重定向链不脱轨:同一次尝试内的多次跳转共享同一索引,不会因跟随重定向多消费了几次代理选择而错位;
  • 跨请求持续轮换:不同请求的基准索引递增,整体保持轮询/随机分布。

此外,代理本身的连接失败(拨号/TLS 失败)在轮换激活时总是可重试——即使常规分类判定不可重试(如永久性地址错误),下一次也会换代理再试,配合被动熔断把坏代理自然淘汰。重试侧细节见重试与容错

查询本次请求使用的代理

代理池场景常需要审计「这次请求到底用了哪个出口」。两个工具配合:

  • Result.Meta.ProxyURL:报告产生最终响应的那次尝试所使用的代理;直连时为空字符串,系统代理(EnableSystemProxy)同样不记录(仍为空)。轮换场景下每次重试可能用不同代理,该字段对应最终那次尝试。
  • WithOnResponse 回调:每次尝试(含换代理的重试)都会触发,可观察逐次尝试的状态码与尝试次数。
go
package main

import (
    "log"

    "github.com/cybergodev/httpc"
)

func main() {
    cfg := httpc.DefaultConfig()
    cfg.Connection.ProxyPool = []string{
        "http://proxy1:8080",
        "http://proxy2:8080",
        "http://proxy3:8080",
    }
    cfg.Connection.ProxyRotateOnStatus = []int{403}

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    result, err := client.Get("https://protected-site.example.com/data",
        httpc.WithOnResponse(func(resp httpc.ResponseMutator) error {
            log.Printf("第 %d 次尝试收到 %d", resp.Attempts(), resp.StatusCode())
            return nil
        }),
    )
    if err != nil {
        log.Fatal(err)
    }

    // 输出:最终代理: http://proxy2:8080, 总尝试: 2
    log.Printf("最终代理: %s, 总尝试: %d", result.Meta.ProxyURL, result.Meta.Attempts)
}

回调按尝试执行,中间件按请求执行

WithOnResponse 在引擎内部触发,每次尝试(含重试)都会执行;中间件链则包裹整个重试周期,一个逻辑请求只执行一次。逐次尝试的观测用回调,整请求粒度的审计用中间件

代理与重试的交互

代理轮换与重试引擎深度联动,两条规则值得记住。

1. 重试预算自动提升。 设置 ProxyRotateOnStatusProxyRotatePerRequest 且代理池多于 1 个时:

text
有效 MaxRetries = max(配置的 MaxRetries, len(ProxyPool) - 1)(上限 10)

例如 5 个代理、配置 MaxRetries = 3:预算提升为 4(= 5 - 1),首次请求用 proxy1,收到 403 后依次换 proxy2…proxy5,每个代理各尝试一次。

2. 代理连接失败强制重试。 轮换激活时,代理本身的连接失败(拨号失败、TLS 失败)绕过常规可重试分类,直接进入下一次重试并换到下一个代理——永久失效的代理(端口错误、主机不可达)无需人工预先剔除,强制重试 + 连续失败熔断会自然淘汰它们。

重试条件、退避数学与跨重试共享的总超时预算详见重试与容错

代理场景的安全注意

代理相关功能自动处理以下安全细节,无需手动配置:

  • SSRF 豁免:代理主机地址(ProxyURLProxyPool 全部条目)自动加入 SSRF 豁免列表,不会被私有 IP 检查阻断——本地代理(如 127.0.0.1:7890)也能正常工作。
  • 去重:代理池中相同 host:port 的条目自动合并,避免轮换偏斜和重复计数。
  • URL 验证:所有代理 URL 经安全校验(协议白名单 http/https/socks5/socks5h、host 非空、注入防护),非法值在 New() 即返回错误而非静默忽略。

与 TLS 的关系:HTTPS 请求经 HTTP 代理的 CONNECT 隧道仍是端到端 TLS——证书校验、最低 TLS 版本、证书固定均针对目标站点照常生效,代理只能转发密文。担心本地 DNS 污染时优先用 socks5h(域名解析交给代理端);启用 DoH 时业务域名走加密解析,但代理地址本身不经过 DoH(代理由开发者显式配置,直接拨代理主机)。SSRF 防护全貌见 SSRF 防护

常见问题

问题原因解决方案
代理不生效ProxyURLProxyPool 同时设置,ProxyURL 优先清空 ProxyURL,仅用 ProxyPool
对同一主机连续请求出口 IP 不变连接复用绑定了上一次的代理隧道启用 ProxyRotatePerRequest
代理频繁熔断ProxyFailureThreshold 太低增大阈值或 ProxyCooldown
状态码轮换没生效Retry.MaxRetries = 0,或代理池只有 1 个代理MaxRetries > 0;池内至少 2 个代理
全部代理熔断会怎样全池连续失败引擎兜底返回最接近恢复的代理,不会直接失败;检查代理可用性与阈值
系统代理没被检测到环境变量缺失且平台设置未开启确认 HTTP_PROXY/HTTPS_PROXY 或系统代理开关
localhost 代理被 SSRF 拦截系统代理的豁免名单只在构建时探测一次显式设置 Connection.ProxyURL(始终豁免)或配置 SSRFExemptCIDRs
收到 403 换代理后代理被熔断了吗误以为状态码会触发熔断不会——状态码轮换不熔断代理,仅连接层失败才熔断
Meta.ProxyURL 一直为空仅系统代理生效——EnableSystemProxy 不记录到该字段需要观测出口时改用显式 ProxyURLProxyPool

完整的字段说明见 配置 API — 代理池

最佳实践

场景建议配置
企业固定出口ProxyURL(http/https/socks5 按需)
跟随用户系统设置EnableSystemProxy + NO_PROXY 放行内网段
多代理负载分散ProxyPool + 默认轮询策略
同一主机的采集ProxyPool + ProxyRotatePerRequest
CF/WAF IP 封锁ProxyPool + ProxyRotateOnStatus: []int{403}
代理质量参差增大 ProxyFailureThreshold(容忍抖动)与 ProxyCooldown
需要审计出口 IP读取 Result.Meta.ProxyURL,配合 WithOnResponse 观察逐次尝试
代理凭据管理写在 URL userinfo,日志自动脱敏;配置文件单独保管

轮换的性能代价

ProxyRotatePerRequest 与状态码轮换的重试路径都会关闭空闲连接,牺牲连接复用换取出口轮换。对性能敏感且无轮换需求的场景保持默认(复用连接)即可,空闲连接管理详见连接池与 DNS

下一步