返回博客
指南
Suciu DanLast updated on May 8, 20264 min read

如何在 C# 中使用 HttpClient 代理

如何在 C# 中使用 HttpClient 代理
简而言之:要在 C# 中使用 HttpClient 时配置代理,请构建一个 WebProxy,将其附加到 HttpClientHandler (或 SocketsHttpHandler),并将该处理程序传递给 HttpClient 构造函数。在生产环境中,请将手动循环替换为 IHttpClientFactory,为需要身份验证的代理添加 NetworkCredential 用于经过身份验证的代理,并使用 Polly 将调用封装在重试机制中,以免失效的 IP 地址导致你的 worker 崩溃。

简介

如果你曾尝试过抓取网站、遭遇过区域限制的 API,或从多个出站 IP 对服务进行压力测试,你应该已经明白我们为何在此。本指南将详细讲解如何在 C# 中使用 HttpClient 配合代理,从仅需五行代码的 WebProxy 配置,到一个不会泄漏套接字的轮询池。

HttpClient 代理其实只是一个 HttpClient 其处理程序配置了一个 WebProxy,因此出站请求会通过中间 IP 隧道传输,而非直接发送到目标。这就是全部的抽象机制。其余的一切——身份验证、SOCKS5、SSL 验证、轮询、重试——都是围绕这一核心理念进行的配置。

我们将假设您已熟悉 async/await 以及 dotnet 在最新 .NET 版本上的命令行界面(CLI)已相当熟悉。我们不会假设您已经阅读过 SocketsHttpHandler。读完本文后,您将获得可直接复制粘贴的模式,涵盖未认证代理、认证代理、SOCKS5、配合 IHttpClientFactory、路径中存在代理时的安全 TLS 验证,以及针对生产环境中必然会遇到的错误的故障排除表。文末还附有决策矩阵,以便您在维护自有代理池不再值得投入时及时停止。如果您希望了解更全面的网络爬虫全景,我们关于使用 C# 构建网络爬虫的入门指南与本文配合使用效果极佳。

C# 中使用 HttpClient 配合代理的思维模型

在编写任何代码之前,请先理清架构层次。 HttpClient 是一个轻量级封装。实际的传输逻辑(包括代理解析)由其处理程序负责。在现代 .NET 环境中,这通常是 HttpClientHandler (兼容旧版框架的封装层)或 SocketsHttpHandler (底层引擎)。二者均暴露了一个 Proxy 类型为 IWebProxy,其内置实现为 WebProxy.

流程如下:

HttpClient
   |
   v
HttpMessageHandler  (HttpClientHandler / SocketsHttpHandler)
   |  Proxy = IWebProxy
   v
WebProxy  ->  proxy server  ->  upstream target

这种分层结构带来了两个结果。首先,代理与处理程序绑定,而非与客户端绑定。你无法修改 HttpClient.Proxy,因为不存在这样的属性。若需使用不同的代理,则需使用不同的处理程序,因此需要一个不同的 HttpClient (或者,更理想地,使用一个为你分配它们的工厂)。

其次,若未指定处理程序,.NET 将回退到系统的默认代理解析机制,包括 HTTPS_PROXY。这在开发者的笔记本电脑上很方便,但在容器环境中却令人意外,因此我们稍后会讨论如何将其排除在外。如果同事在 HttpClient.DefaultProxy :此后构建的每个客户端都会继承该设置,除非你重写该处理程序。

在本文中,我们将任何有效的代理视为 http://host:port,并可选凭据。下文中每当提到如何在 C# 中使用 HttpClient 配合代理时,我们配置的正是这一模式。

为代理测试设置一个最简 C# 项目

确认您的工具链环境,然后生成一个控制台应用程序。我们将在最新的 LTS .NET SDK 上运行所有内容(示例基于 .NET 8 编写,在本文撰写时,其在后续版本中的行为一致)。

dotnet --version          # expect 8.x or newer
mkdir httpclient-proxy && cd httpclient-proxy
dotnet new console

在任意编辑器中打开该项目文件夹。为便于理解,我们将文件结构保持为 Program.cs 中以保持清晰。将 Main 为 async,以便我们能够 await HTTP 调用时避免 .Result 陷阱:

using System.Net.Http;

static async Task Main()
{
    using var client = new HttpClient();
    var direct = await client.GetStringAsync("https://api.ipify.org");
    Console.WriteLine($"Direct IP: {direct}");
}

api.ipify.org 是目前成本最低的IP回显端点。运行 dotnet run,记下 IP 地址,并保留此基准值。一旦接入代理,相同的调用应显示代理的出站 IP 而非您的 IP。若未显示,则为配置错误,而非网络故障。

使用 HttpClientHandler 配置无需身份验证的 WebProxy

从最简单的情况开始:一个无需凭据的免费或本地代理。在 C# 中使用 HttpClient 配置代理的步骤包括三个对象,顺序如下: WebProxy, HttpClientHandler, HttpClient.

using System.Net;
using System.Net.Http;

var proxy = new WebProxy("http://203.0.113.10:8080")
{
    BypassProxyOnLocal = true,        // skip the proxy for localhost/loopback
    UseDefaultCredentials = false     // do not silently send Windows creds
};

var handler = new HttpClientHandler
{
    Proxy = proxy,
    UseProxy = true
};

using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(15) };
var ip = await client.GetStringAsync("https://api.ipify.org");
Console.WriteLine($"Proxied IP: {ip}");

有几个容易被忽略的细节。 WebProxy 构造函数接受 Uri 或字符串,且协议方案至关重要: http:// 用于使用 CONNECT 的 HTTP 和 HTTPS 代理,以及(如后文将看到) socks5:// 用于SOCKS代理。若将凭据嵌入URL中, WebProxy 将忽略这些凭据,因此无需费心。 BypassProxyOnLocal = true 是一个有用的默认设置;它能防止你无意中将健康检查通过外部 IP 进行隧道传输。 UseDefaultCredentials = false 可防止 Windows 自动将当前用户的身份信息发送给第三方代理,这正是那种只有在安全审查人员分析你的数据包捕获记录时才会发现的漏洞。

这是标准模式。本指南中的其他内容都是基于这三个对象的配方所做的变体。如果你想更深入地了解哪些代理类型特别适合抓取工作负载,关于网络抓取最佳代理类型的指南是一篇很好的补充阅读。

代理身份验证:NetworkCredential、407 错误与 PreAuthenticate

大多数付费代理都需要身份验证。在 .NET 中,通常通过 NetworkCredential,并将其附加到 WebProxy 本身,而非处理程序:

var proxy = new WebProxy("http://gateway.example.com:8080")
{
    Credentials = new NetworkCredential("my-user", "my-pass")
};

var handler = new HttpClientHandler { Proxy = proxy, UseProxy = true };
using var client = new HttpClient(handler);

有两个陷阱几乎会让所有初学者在第一次尝试时踩坑。

切勿将凭据放入 URL 中。 new WebProxy("http://user:pass@host:8080") 会静默地忽略 user:pass 部分。用户信息段是从 Uri ,但绝不会作为代理凭据使用。请始终传递 NetworkCredential.

PreAuthenticate 是针对目标的,而非代理。当代理拒绝连接时,会返回 HTTP 407 代理身份验证所需状态码。HttpClient 将其表现为 HttpRequestException。切换 HttpClientHandler.PreAuthenticate = true 不会改变此行为,因为该标志控制的是目标服务器是否在后续请求中收到预先 Authorization 标头。这与 Proxy-Authorization 标头无关,该标头由处理程序在您设置 Credentials.

如果凭据看似正确却仍持续收到 407 错误,请按以下顺序检查三点:是否将凭据发送到了正确的主机(部分提供商会将控制平面与网关分离)、密码是否在上游某处被 URL 编码,以及您的账户状态是否正常。如果您需要了解更广泛的代理错误情况,我们的文章《常见代理状态错误及其识别方法》提供了更深入的解析。

选择协议:C# 中的 HTTP、HTTPS 和 SOCKS5 代理

HttpClient C# 本身并不关心目标是 HTTP 还是 HTTPS,但代理协议至关重要,因为它会改变连接的建立方式。

  • HTTP 代理http://...):对于 HTTP 目标,代理可以读取并重写请求。对于 HTTPS 目标,客户端会发出一个 CONNECT 并通过代理建立端到端的 TLS 隧道。
  • HTTPS 终止代理:这是一种特殊情况,代理会向您的客户端出示其自身的 TLS 证书,并在上游建立独立的 TLS 连接。某些商业爬虫 API 在代理模式下就是这样工作的。我们在下文的专门章节中将探讨 SSL 的相关影响。
  • SOCKS 代理 (socks5://, socks4://, socks4a://):一种不支持 HTTP 的传输层隧道。任何可通过 TCP 套接字传输的内容均可通过。

在现代 .NET 环境中, SocketsHttpHandler 自带对 SOCKS4、SOCKS4a 和 SOCKS5 的原生支持(根据运行时问题跟踪器,SOCKS5 是在 .NET 6 中添加的;若您使用的是非 LTS 预览版,请对照 SocketsHttpHandler 文档进行核对)。配置方式与 WebProxy ,但方案有所不同:

var socks = new WebProxy("socks5://socks.example.com:1080")
{
    Credentials = new NetworkCredential("u", "p")
};

var handler = new SocketsHttpHandler { Proxy = socks, UseProxy = true };
using var client = new HttpClient(handler);

若需在 C# 中使用 HttpClient 连接 SOCKS 端点,请遵循此模式。带用户名/密码认证的 SOCKS5 是家庭网络服务商的事实标准;SOCKS4 主要属于旧版协议。

使用 IHttpClientFactory 干净地轮换代理

IP 轮换是大多数教程悄然崩溃的地方。简单的做法如下:

// DO NOT DO THIS in a real worker
foreach (var url in proxyUrls)
{
    var handler = new HttpClientHandler { Proxy = new WebProxy(url) };
    var client = new HttpClient(handler);   // never disposed
    var html  = await client.GetStringAsync(target);
}

该代码会导致套接字泄漏。每个 HttpClient 都会保持其处理程序存活,而每个处理程序又会占用底层的连接池。在循环中启动几千个这样的实例,你将耗尽临时端口,这在 Linux 上表现为 SocketException: Address already in use 而在 Windows 上则会产生极具“创意”的 WinHttpException 的跟踪信息。

解决方法是 IHttpClientFactory。它会为你管理处理程序的生命周期,按计划回收它们,并允许你为每个代理注册一个命名或类型的客户端。一个简单的依赖注入配置如下:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

foreach (var p in proxyPool)
{
    services.AddHttpClient(p.Name, c => c.Timeout = TimeSpan.FromSeconds(20))
            .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
            {
                Proxy    = new WebProxy(p.Url) { Credentials = p.Creds },
                UseProxy = true,
                PooledConnectionLifetime = TimeSpan.FromMinutes(2)
            });
}

var provider = services.BuildServiceProvider();
var factory  = provider.GetRequiredService<IHttpClientFactory>();

现在你可以为每个请求选择一个代理,而不会泄漏任何信息:

var rng = new Random();
async Task<string> FetchAsync(string url)
{
    var pick   = proxyPool[rng.Next(proxyPool.Count)];
    var client = factory.CreateClient(pick.Name);
    return await client.GetStringAsync(url);
}

轮询机制只需一行代码即可实现:维护一个 Interlocked.Increment 计数器,并将其与 proxyPool.Count。无论采用哪种方式,每个请求都会命中一个健康的、已加入处理程序池的处理程序,并且 IHttpClientFactory 并以 PooledConnectionLifetime,从而规避了下一节将要讨论的长期存在的 DNS 过期问题。如果您希望更全面地了解轮询模式及其适用场景,我们关于轮询代理的深度解析详细介绍了算法方面的内容。请注意, IHttpClientFactory API 细节(包括依赖注入生命周期和 Polly 集成)可能会在主要版本之间发生变化;如果您固定使用旧版运行时,请务必仔细查阅 Microsoft Learn 上的 IHttpClientFactory 页面

生产环境中的 SocketsHttpHandler 与 HttpClientHandler 对比

在现代 .NET 环境中(Core 2.1 及以上版本以及当前所有 dotnet new 项目中), HttpClientHandler 主要是一个兼容性封装层,其底层委托给 SocketsHttpHandler 。对于大多数演示场景,二者可互换使用。但对于长期运行的工作进程和爬虫,建议直接使用 SocketsHttpHandler 直接使用,因为它提供了关键的配置选项:

var handler = new SocketsHttpHandler
{
    Proxy                     = new WebProxy("http://proxy:8080"),
    UseProxy                  = true,
    PooledConnectionLifetime  = TimeSpan.FromMinutes(2),   // recycle TCP/TLS
    PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30),
    ConnectTimeout            = TimeSpan.FromSeconds(10),  // fail fast on dead proxies
    AutomaticDecompression    = System.Net.DecompressionMethods.All
};

值得记住的两个设置:

  • PooledConnectionLifetime 控制连接池中连接的存活时长,超过该时限后连接将被关闭。设置此参数的真正原因在于其对代理/DNS刷新机制的影响。长存活 HttpClient 默认会永久保持单个 TCP 连接,因此上游的 DNS 变更(在轮换住宅端点时非常常见)将永远无法被检测到。对于爬虫而言,两分钟是一个合理的默认值。
  • ConnectTimeoutHttpClient.Timeout。后者涵盖整个请求,前者仅涵盖与代理的 TCP 握手。将其设置得较短(5 到 10 秒)是防止已死代理独占工作线程的最经济方式。

AutomaticDecompression 与代理无关,但因其实用性在此提及:因为大多数抓取端点都会对响应进行 gzip 压缩。 PooledConnectionLifetime 及其相关属性的语义会在主要运行时版本之间发生漂移,因此若您使用 .NET 6 或 7,请务必核对文档。

当请求路径中包含代理时,正确处理 SSL/TLS 验证

请求路径中的代理会增加 TLS 的复杂性,但规则很简单:默认情况下绝不禁用验证DangerousAcceptAnyServerCertificateValidator 该选项的存在是因为微软希望明确指出:若设置此选项,即表示您接受伪造证书。在免费或共享代理上,这实际上是一个等待代理运营者利用的中间人漏洞。

有两种截然不同的情况需要区分。

CONNECT 和 SOCKS 隧道会端到端传输您的 TLS 数据包。您看到的证书是目标网站的真实证书。验证必须保持开启,这一点毋庸置疑。如果在此处出现 SSL 握手失败,说明代理配置错误或上游证书确实无效。切勿对此视而不见。

TLS 终止代理(某些爬虫 API 运行于此模式)会刻意自行完成握手并出示其自有证书。在此情况下,接受未知 CA 是协议的一部分,但仅限于该特定代理。安全模式是使用拇指印或 CA 固定回调:

var expectedThumbprint = "AABBCCDDEEFF00112233445566778899AABBCCDD";

var handler = new SocketsHttpHandler
{
    Proxy = new WebProxy("http://tls-terminating-proxy:8080"),
    SslOptions = new System.Net.Security.SslClientAuthenticationOptions
    {
        RemoteCertificateValidationCallback = (sender, cert, chain, errors) =>
        {
            if (cert is null) return false;
            return string.Equals(cert.GetCertHashString(),
                expectedThumbprint,
                StringComparison.OrdinalIgnoreCase);
        }
    }
};

这虽仍是一种放宽,但具有明确范围:仅接受与固定指纹匹配的证书,而其他所有情况仍需通过常规证书链验证。若您正在进行大规模抓取,且需要使用 C# 中的 HttpClient 配合代理连接到 TLS 终止网关,这便是生产环境中安全的实现方式。

使用 Polly 实现重试、超时和指数退避

代理会失败。家庭IP可能在会话中途断线,数据中心IP段可能被归零路由,上游目标可能对你进行十分钟的速率限制,随后又恢复正常。正确的应对方式是采用退避策略进行重试,而不是让工作线程崩溃。

在现代 Polly(v8+)中,API 如下: ResiliencePipelineBuilder。将较短的超时与较小的重试次数配对,这样死掉的代理能快速失败,而不稳定的代理则能获得第二次机会:

using Polly;
using Polly.Retry;
using Polly.Timeout;

var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
    .AddRetry(new RetryStrategyOptions<HttpResponseMessage>
    {
        MaxRetryAttempts = 3,
        Delay            = TimeSpan.FromMilliseconds(500),
        BackoffType      = DelayBackoffType.Exponential,
        UseJitter        = true,
        ShouldHandle     = new PredicateBuilder<HttpResponseMessage>()
            .Handle<HttpRequestException>()
            .Handle<TaskCanceledException>()
            .HandleResult(r => (int)r.StatusCode >= 500 || (int)r.StatusCode == 408)
    })
    .AddTimeout(TimeSpan.FromSeconds(15))
    .Build();

var response = await pipeline.ExecuteAsync(
    async ct => await client.GetAsync(target, ct));

三个调优建议。保持 MaxRetryAttempts 数量精简(三个就足够);一个不稳定的代理通常不值得进行第四次尝试。 UseJitter = true 当你运行数百个并行 worker 时,这很重要,否则它们会同步重试并轰炸同一个后端。并且不要将 407 状态码纳入可重试列表,因为如果凭证第一次错误,下次尝试时也会错误,你只会更快地耗尽重试配额。如果你是从 v7 升级,请对照 Polly 文档验证 v8 接口,因为多个类名已更改,且 v7 Policy.HandleAsync 的风格无法在新构建器上编译。

按请求的代理选择与绕过规则

对于业余项目,单个静态代理已足够。但一旦开始混合内部和外部流量,或通过不同的出站 IP 路由不同域名,你就需要按请求进行代理选择。 WebProxy 为您提供了两个控制选项: BypassList 以及自定义 IWebProxy 实现方案。

BypassList 支持正则表达式模式。任何匹配项将完全绕过代理,这就是您将内部主机名和私有 CIDR 范围从外部跳转中排除的方式:

var proxy = new WebProxy("http://proxy:8080")
{
    BypassProxyOnLocal = true,
    BypassList = new[] { @"^.*\.internal\.example\.com$", @"^10\.0\.0\..*$" }
};

若要实现真正的按主机路由,请自行实现 IWebProxy

sealed class HostBasedProxy : IWebProxy
{
    public ICredentials? Credentials { get; set; }
    public Uri? GetProxy(Uri destination) =>
        destination.Host.EndsWith("google.com") ? new Uri("http://us-proxy:8080")
      : destination.Host.EndsWith("yandex.ru")  ? new Uri("http://eu-proxy:8080")
      : null;
    public bool IsBypassed(Uri host) => GetProxy(host) is null;
}

仅此便足以通过单个 HttpClient。该处理程序会 GetProxy ,因此路由决策是动态的,且无需为每个区域单独配置客户端。

调试常见的 HttpClient 代理错误

当出现问题时,异常信息很少能自明。解决问题的最快途径是“症状优先”。

症状(您所观察到的现象)

可能原因

一行代码修复

HttpRequestException: 407 Proxy Authentication Required

缺少或错误的代理凭据

设置 WebProxy.CredentialsNetworkCredential,切勿在 user:pass@ 在 URL 中

状态 502 Bad Gateway504 Gateway Timeout

代理服务器正常,上游服务器已断开或对你进行限速

使用退避策略重试;第二次失败后切换到其他 IP

HttpRequestException: The tunnel through the proxy could not be established

代理拒绝 CONNECT,通常是防火墙或方案错误

确认 http:///socks5:// 协议,并确认代理支持 HTTPS 目标

AuthenticationException:SSL握手失败

未绑定有效验证器的 TLS 终止代理,或证书确实无效

使用 RemoteCertificateValidationCallback;请勿启用 DangerousAcceptAnyServerCertificateValidator 盲目启用

SocketException: No such host is known

代理内部或代理自身发生 DNS 查询失败

验证主机名;对于长期运行的客户端,设置 PooledConnectionLifetime 以便 DNS 能重新解析

请求将一直挂起,直到 HttpClient.Timeout 触发

代理死机、无限重定向或卡住 CONNECT

SocketsHttpHandler.ConnectTimeout 设置为 5 到 10 秒,并为每个请求 CancellationToken

间歇性 SocketException: Address already in use 在高负载下

从 new-HttpClient-per-request 循环

迁移至 IHttpClientFactory 为每个代理指定客户端

如有疑问,请在记录异常的同时记录代理 URL。一旦能看到是哪台 IP 实际失败,而不是在五十台 IP 池中猜测,一半的代理 bug 就会消失。

为工作负载选择合适的代理策略

关于如何在 C# 中大规模使用 HttpClient 配合代理,并没有放之四海皆准的最佳答案。唯一需要考虑的是,你希望在代理层与数据层之间投入多少工程资源。选择能满足可靠性门槛的最低配置即可。

策略

可靠性

维护开销

地理定位

何时选用

免费公共代理

极低;其中许多是蜜罐

高;用户流失率持续高

不适用

绝不用于生产环境。仅限本地实验。

静态认证数据中心代理

针对良性目标尚可

有限

B2B API、内部工具、轻量级地理限制解除

通过住宅IP池实现自建轮换

若设计得当则效果显著

高;需自行处理重试、健康检查、粘性会话及计费

是,前提是您的服务商提供了国家标签

有能力自行管理池并具备相关工程预算的团队

托管式抓取/代理 API

高;服务商承担故障风险

低;您仅需调用一个端点

是,通常按国家/地区计费

大规模抓取、反机器人目标、小型团队

一个有用的直觉检验:如果你的代码库中代理代码的增长速度超过了解析代码,那就意味着你正在花钱雇工程师来做一个比托管服务商还差的版本。请向上层架构迁移。反之,如果你只需少量静态 IP 地址来调用合作伙伴的 API,就不要过度设计;一个经过身份验证的 WebProxy 就足够了。

超越 DIY 的扩展:通过 WebScrapingAPI 代理模式路由 HttpClient

当 DIY 的成本效益不再成立时,最干净利落的解决方案是保留你的 HttpClient 并将其指向托管代理端点。WebScrapingAPI 提供了一个代理模式网关,它接受相同的 WebProxy + NetworkCredential 配方,由服务器端自动处理IP轮换、地理定位和反机器人机制。

var proxy = new WebProxy("http://proxy.webscrapingapi.com:80")
{
    Credentials = new NetworkCredential(
        "YOUR_API_KEY",                  // username slot
        "render_js=false.country=us"     // password slot carries options
    )
};

var handler = new SocketsHttpHandler
{
    Proxy                     = proxy,
    UseProxy                  = true,
    PooledConnectionLifetime  = TimeSpan.FromMinutes(2),
    ConnectTimeout            = TimeSpan.FromSeconds(15)
};

using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(60) };
var html = await client.GetStringAsync("https://example.com/product/42");

其架构与本文前文提到的“认证代理”模式完全一致;API密钥位于用户名槽位,请求选项则位于密码槽位。 由于网关会为每次请求选择一个新的出口 IP,因此无需轮询循环;由于失败的响应不会产生费用,因此无需重试管道;由于国家选择仅需设置标志,因此您的代码中无需包含地理定位逻辑。如果您希望实现深度防御,仍可保留 Polly 管道,但您需要维护的系统接触面将大幅缩减。请将此视为上表中的一个选项,而非最终结论;当您的团队规模较小且目标环境充满敌意时,这是正确的选择。

关键要点

  • 配置处理程序,而非客户端。代理始终存在 HttpClientHandlerSocketsHttpHandler 通过 WebProxy. HttpClient 本身不具备 Proxy 属性,因此无法在运行时对其进行修改。
  • 始终通过 NetworkCredentialuser:pass@ 在代理 URL 中会被静默忽略,这也是导致莫名其妙的 407 错误的最常见原因。
  • 请使用 IHttpClientFactory 进行轮换。一个 foreach 循环会为每个代理建立 HttpClient 会因负载过高导致套接字泄漏。为每个代理指定命名客户端,并 PooledConnectionLifetime,可解决此问题。
  • 建议 SocketsHttpHandler 直接在生产环境中使用。它支持 ConnectTimeout, PooledConnectionLifetime,并支持 SOCKS5,这些功能你最终都会用到。
  • 请勿关闭 TLS 验证。对于 TLS 终止代理,请固定一个指纹。对于 CONNECT 或 SOCKS 隧道,请保持验证开启;此类验证失败代表真实故障,而非误报。

常见问题:开发者实际提出的 HttpClient 代理问题

HttpClient 会自动检测系统代理或 HTTPS_PROXY 环境变量吗?如何禁用此功能?

是的。若未分配处理程序, HttpClient 将使用系统默认代理,在 .NET Core 3.1 及以上版本中,该代理还会读取 HTTP_PROXY, HTTPS_PROXYNO_PROXY 在 Linux 和 macOS 上。若要禁用此功能,请通过 UseProxy = false,或在 HttpClient.DefaultProxy = new WebProxy()

我可以修改现有 HttpClient 实例的代理设置吗,还是需要创建一个新的实例?

您需要创建一个新的客户端。代理在构造时已与处理程序绑定,且 HttpClient 不提供 Proxy setter。请使用由 IHttpClientFactory,或者使用自定义的 IWebProxy ,其 GetProxy(Uri) 在处理程序保持不变的情况下动态决定。

为什么即使我设置了凭据,请求仍返回 407 代理身份验证所需?

通常有三个原因:凭据嵌入在 URL 中(被 WebProxy),上游某处将密码进行了两次 URL 编码,或者凭据被分配给了 HttpClientHandler.Credentials 而非 WebProxy.Credentials。只有后一种情况会将凭据传递给代理。 PreAuthenticate 在此情况下无济于事;该标志仅控制目标服务器。

HttpClient 在 .NET 6 及更高版本中是否支持 SOCKS5 代理?

支持。 SocketsHttpHandler 从 .NET 6 开始,已添加对原生 SOCKS4、SOCKS4a 和 SOCKS5 的支持。请使用 socks5://host:port URI WebProxy ,并通过 NetworkCredential

取消一个缓慢的代理请求而不泄露套接字的正确方法是什么?

传递一个 CancellationTokenCancellationTokenSource 并设置合理的超时时间,让请求在 OperationCanceledException上自然结束。将该令牌与 SocketsHttpHandler.ConnectTimeout ,这样TCP握手会快速失败,套接字也会返回池中,而不是悬空。

结论

以上就是关于如何在 C# 中使用 HttpClient 配合代理,同时避免陷入死胡同,你需要了解的大部分内容。从五行的演示代码到生产环境中的工作线程,解决方案的结构几乎没有变化:一个 WebProxy、一个处理程序、一个 HttpClient。真正发生变化的是其周边的实现。生产环境代码使用 IHttpClientFactory ,因此处理程序会被回收;设置了严格的 ConnectTimeout ,确保失效的代理能快速报错,固定 TLS 指纹而非禁用验证,并将请求封装在 Polly 管道中,以免临时故障在凌晨 3 点打扰任何人。

文中前面的决策矩阵是本文最重要的收获。一旦将工程时间纳入成本考量,免费代理其实并不免费。静态数据中心代理在目标网站未部署严密的反机器人防护机制前效果极佳。自建轮换代理虽然构建过程充满成就感,但运行成本高昂。托管代理 API 则以信用额度为代价,换取了您原本需要花费在健康检查、重试和滥用处理上的时间。

如果你的团队已达到代理层耗时超过解析层的程度,WebScrapingAPI 代理端点可无缝融入你现有的 WebProxy + NetworkCredential 现有架构,且仅需为成功响应付费。无论选择哪种路径,请保持抽象层清晰:底层是处理程序,中间是重试机制,顶层是业务逻辑。未来你会感谢现在的自己做出这种分离。

关于作者
Suciu Dan, 联合创始人 @ WebScrapingAPI
Suciu Dan联合创始人

Suciu Dan 是 WebScrapingAPI 的联合创始人,他撰写了关于 Python 网页抓取、Ruby 网页抓取以及代理基础设施的实用指南,这些指南专为开发者而设计。

开始构建

准备好扩展您的数据收集规模了吗?

加入2,000多家企业,使用WebScrapingAPI在无需任何基础设施开销的情况下,以企业级规模提取网络数据。