找回密码
 立即注册
首页 业界区 业界 .NET 10了,HttpClient还是不能用using吗?我做了一个实 ...

.NET 10了,HttpClient还是不能用using吗?我做了一个实验

橘芜 2026-1-23 13:35:01
引言:这个“最佳实践”到底过时了吗?

每隔一段时间,就会看到类似问题反复出现:
“都 .NET 10 了,HttpClient 还不能 using 吗?我每次请求 new HttpClient(),用完 Dispose(),不是很合理?”
这类问题之所以经久不衰,是因为它在低并发下几乎永远跑得通;但一旦进入“高并发 + 短连接密集创建”的场景,就会突然变成玄学:有的人能跑,有的人会炸,有人说这是一个这是一个“bug”,在某某版本中会修复(其实并没有),有人说这是一个feature,设计就是如此……
所以我决定做一个实验,来重现一下10年前就有的现象,看这些现象是否有任何不同。
本文用一组可复现的压测(同机 server/client,Windows,requests=20000,parallel=200)对比:

  • 每请求 new HttpClient + Dispose()(也就是大家常说的“using 写法”)
  • 复用一个 HttpClient(静态/单例)
  • 使用 IHttpClientFactory
并观察关键指标:TIME_WAIT 数量,以及是否出现经典的端口耗尽错误:
通常每个套接字地址(协议/网络地址/端口)只允许使用一次。
实验目标

验证“每请求 new HttpClient 并 using 释放”在高并发下会导致 TIME_WAIT 激增,并对比复用 HttpClient / IHttpClientFactory 的表现。
实验环境与参数

<ul>OS: Windows
SDK: .NET SDK 10.0.102
服务器: HttpLeakServer(target net6,通过 roll-forward 运行)
客户端: net48 / net6 / net8 / net10
压测参数: requests=20000, parallel=200, timeoutSeconds=5
TIME_WAIT 统计: netstat -an 过滤端口 5055
隔离策略: 每轮结束后等待 TIME_WAIT {    options.SingleLine = true;    options.TimestampFormat = "HH:mm:ss ";});var app = builder.Build();app.MapGet("/", () => Results.Text("ok"));app.MapGet("/ping", () => Results.Text("ok"));app.MapGet("/slow", async () =>{    await Task.Delay(50);    return Results.Text("ok");});var url = Environment.GetEnvironmentVariable("HTTPLEAK_URL") ?? "http://localhost:5055";app.Urls.Add(url);app.Lifetime.ApplicationStarted.Register(() =>{    Console.WriteLine($"Listening on {url}");});await app.RunAsync();[/code]客户端:HttpLeakClient(net48/net6/net8/net10 共用一份)

Clients/HttpLeakClient/HttpLeakClient.csproj(示意:多目标 + 条件依赖)
  1. dotnet run --project Server/HttpLeakServer/HttpLeakServer.csproj
复制代码
Clients/HttpLeakClient/Program.cs(用 #if 表示差异)

[code]using System.Diagnostics;using System.Net.Http;#if NET48using System.Net;#endif#if NET10_0_OR_GREATERusing Microsoft.Extensions.DependencyInjection;#endifstatic string? GetArg(string[] args, string name){    for (var i = 0; i < args.Length - 1; i++)    {        if (string.Equals(args, name, StringComparison.OrdinalIgnoreCase))        {            return args[i + 1];        }    }    return null;}static int GetArgInt(string[] args, string name, int defaultValue){    var value = GetArg(args, name);    return int.TryParse(value, out var parsed) ? parsed : defaultValue;}var url = GetArg(args, "--url") ?? "http://localhost:5055/ping";var requests = GetArgInt(args, "--requests", 20000);var parallel = GetArgInt(args, "--parallel", 200);var logEvery = GetArgInt(args, "--logEvery", 1000);var timeoutSeconds = GetArgInt(args, "--timeoutSeconds", 5);#if NET10_0_OR_GREATERvar mode = GetArg(args, "--mode") ?? "new"; // new | static | factory#endifConsole.WriteLine($"url={url}");#if NET10_0_OR_GREATERConsole.WriteLine($"requests={requests}, parallel={parallel}, timeoutSeconds={timeoutSeconds}, mode={mode}");#elseConsole.WriteLine($"requests={requests}, parallel={parallel}, timeoutSeconds={timeoutSeconds}");#endif#if NET48ServicePointManager.DefaultConnectionLimit = 1000;ServicePointManager.Expect100Continue = false;#endifvar throttler = new SemaphoreSlim(parallel);var tasks = new List(requests);var sw = Stopwatch.StartNew();var success = 0;var failed = 0;#if NET10_0_OR_GREATERHttpClient? staticClient = null;if (string.Equals(mode, "static", StringComparison.OrdinalIgnoreCase)){    staticClient = new HttpClient    {        Timeout = TimeSpan.FromSeconds(timeoutSeconds)    };}IHttpClientFactory? httpClientFactory = null;ServiceProvider? serviceProvider = null;if (string.Equals(mode, "factory", StringComparison.OrdinalIgnoreCase)){    var services = new ServiceCollection();    services.AddHttpClient();    serviceProvider = services.BuildServiceProvider();    httpClientFactory = serviceProvider.GetRequiredService();}#endiffor (var i = 0; i < requests; i++){    await throttler.WaitAsync();    var index = i + 1;    tasks.Add(Task.Run(async () =>    {        try        {#if NET10_0_OR_GREATER            HttpClient client;            if (staticClient != null)            {                client = staticClient;            }            else if (httpClientFactory != null)            {                client = httpClientFactory.CreateClient();                client.Timeout = TimeSpan.FromSeconds(timeoutSeconds);            }            else            {                client = new HttpClient();                client.Timeout = TimeSpan.FromSeconds(timeoutSeconds);            }            using var response = await client.GetAsync(url);            response.EnsureSuccessStatusCode();            Interlocked.Increment(ref success);            if (staticClient == null && httpClientFactory == null)            {                client.Dispose();            }#else            using var client = new HttpClient();            client.Timeout = TimeSpan.FromSeconds(timeoutSeconds);            using var response = await client.GetAsync(url);            response.EnsureSuccessStatusCode();            Interlocked.Increment(ref success);#endif        }        catch (Exception ex)        {            var fail = Interlocked.Increment(ref failed);            if (fail

相关推荐

2026-1-24 05:15:02

举报

您需要登录后才可以回帖 登录 | 立即注册