为什么 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 个请求
<p>public async Task<string> 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<proxyitem></proxyitem>按
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)动态赋值更高效。
