HttpClient应全局复用单个静态实例并精细配置HttpClientHandler,避免端口耗尽或连接失效;用SemaphoreSlim限流控制并发;IP池需预检、动态降权、按评分轮换;代理设置须禁用默认凭据、绕过本地地址。

为什么 HttpClient 不能全局复用又不能每次都 new
在 C# 爬虫里直接 new HttpClient() 十次,会快速耗尽本地端口(TIME_WAIT 状态堆积),而长期复用同一个 HttpClient 实例又可能因 DNS 缓存、连接池僵死或服务端连接关闭导致后续请求失败。正确做法是复用单个静态 HttpClient 实例,但必须配合 HttpClientHandler 的精细配置:
-
MaxConnectionsPerServer设为合理值(如 10–50),避免单域名压垮对方或触发风控 - 启用
UseProxy = false(除非你明确走代理)+AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate - 设置
Timeout(建议 15–30 秒),防止某个请求卡死拖垮整个并发队列
如何用 SemaphoreSlim 控制并发请求数量
SemaphoreSlim 是轻量级、支持 async/await 的信号量,比 Task.Run + lock 更适合爬虫这种 I/O 密集型场景。它不阻塞线程,只限制同时进入临界区的协程数。
private static readonly SemaphoreSlim _throttle = new SemaphoreSlim(5); // 同时最多 5 个请求public async Task
FetchAsync(string url) { await _throttle.WaitAsync(); try { return await _httpClient.GetStringAsync(url); } finally { _throttle.Release(); } }
注意:不要在 using 块里创建 SemaphoreSlim,它需跨请求复用;释放必须放在 finally,否则异常后信号量永远卡死。
IP 池不是“存一堆代理就完事”,关键在可用性验证和轮换策略
未经验证的代理列表基本不可用——超时、返回空、被目标站重定向到验证码页、甚至返回 403 伪装成成功。真实 IP 池管理必须包含:
- 启动时对每个代理执行预检:
HEAD或轻量GET到一个稳定响应的测试地址(如http://httpbin.org/ip),记录响应时间与状态码 - 运行中动态降权:某代理连续 2 次超时或返回非 2xx,将其权重设为 0,10 分钟后尝试恢复
- 轮换不等于随机:优先选响应快、错误率低、未被当前目标站封禁的代理;可用
SortedSet按Score排序,每次取First()
别把代理 IP 和端口拼成字符串存 List —— 定义 class ProxyItem { public string Address; public int Port; public double Score; },否则后期加字段、排序、过滤全得重写。
HttpClientHandler 的 Proxy 设置容易踩的坑
给 HttpClientHandler 赋值 Proxy 时,常见错误是传入 new WebProxy("http://127.0.0.1:8888") 却没关掉默认凭据:
-
UseDefaultCredentials = true会让请求带上 Windows 登录凭据,多数 HTTP 代理不认,直接 407 - 漏设
BypassProxyOnLocal = true,导致访问localhost或内网地址也走代理,超时失败 - 代理地址写成
"http://...",但实际要求不带协议("127.0.0.1:8888"),不同代理中间件要求不一致
安全写法:
var handler = new HttpClientHandler
{
Proxy = new WebProxy("127.0.0.1:8888"),
UseProxy = true,
UseDefaultCredentials = false,
BypassProxyOnLocal = true
};IP 池切换时,别反复 new HttpClientHandler —— 创建开销大,改用 handler.Proxy = new WebProxy(nextProxy) 动态赋值更高效。










