.NET 10了,HttpClient还是不能用using吗?我做了一个实验
引言:这个“最佳实践”到底过时了吗?
每隔一段时间,就会看到类似问题反复出现:
“都 .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 的表现。
实验环境与参数
- 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 <= baseline + 200(每 10 秒检查,最长 300 秒)
如何运行(可复现)
完整项目我放在 GitHub:https://github.com/sdcb/http-client-exp
1)启动服务端(单独窗口)
dotnet run --project Server/HttpLeakServer/HttpLeakServer.csproj
2)运行实验脚本(另一个窗口)
scripts/run-experiment-external-server.ps1
本次日志目录:logs/run-20260119-095017
解读:为什么“using HttpClient”会把你推向端口耗尽?
很多人直觉会觉得:HttpClient 是托管对象,用完 Dispose(),不就释放资源了吗?
但这里有两个关键点经常被忽略:
HttpClient并不是“请求一次就关一次连接”的简单模型。HTTP Keep-Alive + 连接池的存在,意味着正确姿势应该是复用底层连接(或至少复用 handler 的连接池),让大量请求复用少量 TCP 连接。- 你频繁
new HttpClient+Dispose(),等价于频繁建立 TCP 连接并快速关闭。而 TCP 连接的关闭会进入 TIME_WAIT(具体哪一端进入 TIME_WAIT 与关闭时序有关),TIME_WAIT 存在的意义是保护网络不被“旧连接的残留包”污染。
在“200 并发 + 2 万次请求”这种参数下,如果你让每个请求都创建新连接,那么很容易短时间制造大量 TIME_WAIT;一旦本机可用的临时端口范围被 TIME_WAIT 占满(或接近占满),新连接就会开始失败,典型异常就是:
通常每个套接字地址(协议/网络地址/端口)只允许使用一次。
这也是为什么同样的代码:
- 在低并发下“看起来完全没问题”
- 在压力一上来就开始“玄学报错”
那到底怎么写才对?
本文不展开“所有场景的最佳实践”,只给两条最能落地的结论:
- 业务代码不要每次请求 new HttpClient。要么复用单例/静态
HttpClient,要么使用IHttpClientFactory。 - 可以 using 的是
HttpResponseMessage/HttpContent(它们确实应该及时释放),而不是“每个请求一个 HttpClient”。
下面我把这次实验的完整代码和原始日志全部贴出来,方便你自己复跑/改参数/做二次验证。
完整代码(精简版:一份源码 + 条件编译)
完整项目地址:https://github.com/sdcb/http-client-exp
为了避免同样的代码贴四遍,这里把 net48/net6/net8/net10 的客户端合并成一份,用 #if / #elif 表示差异(实验输出与原始日志仍按“多份结果”原样保留在后文)。
简单服务端:HttpLeakServer
Server/HttpLeakServer/HttpLeakServer.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Server/HttpLeakServer/Program.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddSimpleConsole(options =>
{
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();
客户端:HttpLeakClient(net48/net6/net8/net10 共用一份)
Clients/HttpLeakClient/HttpLeakClient.csproj(示意:多目标 + 条件依赖)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net48;net6.0;net8.0;net10.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
</ItemGroup>
</Project>
Clients/HttpLeakClient/Program.cs(用 #if 表示差异)
using System.Diagnostics;
using System.Net.Http;
#if NET48
using System.Net;
#endif
#if NET10_0_OR_GREATER
using Microsoft.Extensions.DependencyInjection;
#endif
static string? GetArg(string[] args, string name)
{
for (var i = 0; i < args.Length - 1; i++)
{
if (string.Equals(args[i], 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_GREATER
var mode = GetArg(args, "--mode") ?? "new"; // new | static | factory
#endif
Console.WriteLine($"url={url}");
#if NET10_0_OR_GREATER
Console.WriteLine($"requests={requests}, parallel={parallel}, timeoutSeconds={timeoutSeconds}, mode={mode}");
#else
Console.WriteLine($"requests={requests}, parallel={parallel}, timeoutSeconds={timeoutSeconds}");
#endif
#if NET48
ServicePointManager.DefaultConnectionLimit = 1000;
ServicePointManager.Expect100Continue = false;
#endif
var throttler = new SemaphoreSlim(parallel);
var tasks = new List<Task>(requests);
var sw = Stopwatch.StartNew();
var success = 0;
var failed = 0;
#if NET10_0_OR_GREATER
HttpClient? 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<IHttpClientFactory>();
}
#endif
for (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 <= 5)
{
Console.WriteLine($"ERR#{fail}: {ex.GetType().Name} {ex.Message}");
}
}
finally
{
throttler.Release();
}
}));
if (index % logEvery == 0)
{
Console.WriteLine($"queued: {index}/{requests}, success: {Volatile.Read(ref success)}, failed: {Volatile.Read(ref failed)}, elapsed: {sw.Elapsed}");
}
}
await Task.WhenAll(tasks);
Console.WriteLine($"done: success={success}, failed={failed}, elapsed={sw.Elapsed}");
#if NET10_0_OR_GREATER
staticClient?.Dispose();
serviceProvider?.Dispose();
#endif
PowerShell 脚本
脚本我就不在文章里全文贴了:
- 外部启动 Server 版本:
https://github.com/sdcb/http-client-exp/blob/main/scripts/run-experiment-external-server.ps1 - 脚本内启动 Server 版本:
https://github.com/sdcb/http-client-exp/blob/main/scripts/run-experiment.ps1
原始实验结果(日志,完整贴出)
日志目录:
logs/run-20260119-095017
experiment-summary.log
[2026-01-19T09:51:13.7180857+08:00] net48 ExitCode=0 OutLog=C:\Users\ZhouJie\source\repos\http-client-exp\logs\\run-20260119-095017\net48.out.log ErrLog=C:\Users\ZhouJie\source\repos\http-client-exp\logs\\run-20260119-095017\net48.err.log
[2026-01-19T09:53:33.0362523+08:00] net6 ExitCode=0 OutLog=C:\Users\ZhouJie\source\repos\http-client-exp\logs\\run-20260119-095017\net6.out.log ErrLog=C:\Users\ZhouJie\source\repos\http-client-exp\logs\\run-20260119-095017\net6.err.log
[2026-01-19T09:56:00.4574527+08:00] net8 ExitCode=0 OutLog=C:\Users\ZhouJie\source\repos\http-client-exp\logs\\run-20260119-095017\net8.out.log ErrLog=C:\Users\ZhouJie\source\repos\http-client-exp\logs\\run-20260119-095017\net8.err.log
[2026-01-19T09:58:27.2618139+08:00] net10-new ExitCode=0 OutLog=C:\Users\ZhouJie\source\repos\http-client-exp\logs\\run-20260119-095017\net10-new.out.log ErrLog=C:\Users\ZhouJie\source\repos\http-client-exp\logs\\run-20260119-095017\net10-new.err.log
[2026-01-19T10:00:53.0490472+08:00] net10-static ExitCode=0 OutLog=C:\Users\ZhouJie\source\repos\http-client-exp\logs\\run-20260119-095017\net10-static.out.log ErrLog=C:\Users\ZhouJie\source\repos\http-client-exp\logs\\run-20260119-095017\net10-static.err.log
[2026-01-19T10:01:10.2096164+08:00] net10-factory ExitCode=0 OutLog=C:\Users\ZhouJie\source\repos\http-client-exp\logs\\run-20260119-095017\net10-factory.out.log ErrLog=C:\Users\ZhouJie\source\repos\http-client-exp\logs\\run-20260119-095017\net10-factory.err.log
netstat.log
[2026-01-19T09:50:55.6627455+08:00] before-net48 TIME_WAIT=2 ESTABLISHED=0 CLOSE_WAIT=0
[2026-01-19T09:51:15.4686603+08:00] after-net48 TIME_WAIT=20002 ESTABLISHED=0 CLOSE_WAIT=0
[2026-01-19T09:53:13.9647485+08:00] before-net6 TIME_WAIT=0 ESTABLISHED=0 CLOSE_WAIT=0
[2026-01-19T09:53:33.8112294+08:00] after-net6 TIME_WAIT=20000 ESTABLISHED=0 CLOSE_WAIT=0
[2026-01-19T09:55:42.6435015+08:00] before-net8 TIME_WAIT=0 ESTABLISHED=0 CLOSE_WAIT=0
[2026-01-19T09:56:01.1900000+08:00] after-net8 TIME_WAIT=18361 ESTABLISHED=0 CLOSE_WAIT=0
[2026-01-19T09:58:08.8603129+08:00] before-net10-new TIME_WAIT=0 ESTABLISHED=0 CLOSE_WAIT=0
[2026-01-19T09:58:28.1500737+08:00] after-net10-new TIME_WAIT=18860 ESTABLISHED=0 CLOSE_WAIT=0
[2026-01-19T10:00:36.1038757+08:00] before-net10-static TIME_WAIT=0 ESTABLISHED=0 CLOSE_WAIT=0
[2026-01-19T10:00:53.1007259+08:00] after-net10-static TIME_WAIT=200 ESTABLISHED=0 CLOSE_WAIT=0
[2026-01-19T10:00:53.2458798+08:00] before-net10-factory TIME_WAIT=200 ESTABLISHED=0 CLOSE_WAIT=0
[2026-01-19T10:01:10.2574678+08:00] after-net10-factory TIME_WAIT=200 ESTABLISHED=0 CLOSE_WAIT=0
cooldown.log
[2026-01-19T09:51:16.2687666+08:00] cooldown-net48 TIME_WAIT=20002 baseline=2 limit=202
[2026-01-19T09:51:27.0427805+08:00] cooldown-net48 TIME_WAIT=20002 baseline=2 limit=202
[2026-01-19T09:51:37.8184382+08:00] cooldown-net48 TIME_WAIT=20002 baseline=2 limit=202
[2026-01-19T09:51:48.5404492+08:00] cooldown-net48 TIME_WAIT=20002 baseline=2 limit=202
[2026-01-19T09:51:59.2846968+08:00] cooldown-net48 TIME_WAIT=20002 baseline=2 limit=202
[2026-01-19T09:52:10.1562079+08:00] cooldown-net48 TIME_WAIT=20002 baseline=2 limit=202
[2026-01-19T09:52:21.0588622+08:00] cooldown-net48 TIME_WAIT=20002 baseline=2 limit=202
[2026-01-19T09:52:31.7777704+08:00] cooldown-net48 TIME_WAIT=20002 baseline=2 limit=202
[2026-01-19T09:52:42.5664350+08:00] cooldown-net48 TIME_WAIT=20002 baseline=2 limit=202
[2026-01-19T09:52:53.3299614+08:00] cooldown-net48 TIME_WAIT=20001 baseline=2 limit=202
[2026-01-19T09:53:03.8109734+08:00] cooldown-net48 TIME_WAIT=10752 baseline=2 limit=202
[2026-01-19T09:53:13.8560831+08:00] cooldown-net48 TIME_WAIT=0 baseline=2 limit=202
[2026-01-19T09:53:34.6370711+08:00] cooldown-net6 TIME_WAIT=20000 baseline=0 limit=200
[2026-01-19T09:53:45.5166974+08:00] cooldown-net6 TIME_WAIT=20000 baseline=0 limit=200
[2026-01-19T09:53:56.2899736+08:00] cooldown-net6 TIME_WAIT=20000 baseline=0 limit=200
[2026-01-19T09:54:07.0165428+08:00] cooldown-net6 TIME_WAIT=20000 baseline=0 limit=200
[2026-01-19T09:54:17.8872137+08:00] cooldown-net6 TIME_WAIT=20000 baseline=0 limit=200
[2026-01-19T09:54:28.7673529+08:00] cooldown-net6 TIME_WAIT=20000 baseline=0 limit=200
[2026-01-19T09:54:39.5200091+08:00] cooldown-net6 TIME_WAIT=20000 baseline=0 limit=200
[2026-01-19T09:54:50.3090659+08:00] cooldown-net6 TIME_WAIT=20000 baseline=0 limit=200
[2026-01-19T09:55:01.0638920+08:00] cooldown-net6 TIME_WAIT=20000 baseline=0 limit=200
[2026-01-19T09:55:11.7529528+08:00] cooldown-net6 TIME_WAIT=20000 baseline=0 limit=200
[2026-01-19T09:55:22.3886445+08:00] cooldown-net6 TIME_WAIT=12540 baseline=0 limit=200
[2026-01-19T09:55:32.4658786+08:00] cooldown-net6 TIME_WAIT=435 baseline=0 limit=200
[2026-01-19T09:55:42.5354878+08:00] cooldown-net6 TIME_WAIT=0 baseline=0 limit=200
[2026-01-19T09:56:01.8876493+08:00] cooldown-net8 TIME_WAIT=18361 baseline=0 limit=200
[2026-01-19T09:56:12.6341541+08:00] cooldown-net8 TIME_WAIT=18361 baseline=0 limit=200
[2026-01-19T09:56:23.2806604+08:00] cooldown-net8 TIME_WAIT=18361 baseline=0 limit=200
[2026-01-19T09:56:33.9425873+08:00] cooldown-net8 TIME_WAIT=18361 baseline=0 limit=200
[2026-01-19T09:56:44.6277561+08:00] cooldown-net8 TIME_WAIT=18361 baseline=0 limit=200
[2026-01-19T09:56:55.3013674+08:00] cooldown-net8 TIME_WAIT=18361 baseline=0 limit=200
[2026-01-19T09:57:06.0278128+08:00] cooldown-net8 TIME_WAIT=18361 baseline=0 limit=200
[2026-01-19T09:57:16.7315578+08:00] cooldown-net8 TIME_WAIT=18361 baseline=0 limit=200
[2026-01-19T09:57:27.3887925+08:00] cooldown-net8 TIME_WAIT=18361 baseline=0 limit=200
[2026-01-19T09:57:38.0442453+08:00] cooldown-net8 TIME_WAIT=18361 baseline=0 limit=200
[2026-01-19T09:57:48.6566839+08:00] cooldown-net8 TIME_WAIT=12989 baseline=0 limit=200
[2026-01-19T09:57:58.7347337+08:00] cooldown-net8 TIME_WAIT=1069 baseline=0 limit=200
[2026-01-19T09:58:08.7780660+08:00] cooldown-net8 TIME_WAIT=0 baseline=0 limit=200
[2026-01-19T09:58:29.0261291+08:00] cooldown-net10-new TIME_WAIT=18860 baseline=0 limit=200
[2026-01-19T09:58:39.7491043+08:00] cooldown-net10-new TIME_WAIT=18860 baseline=0 limit=200
[2026-01-19T09:58:50.4613796+08:00] cooldown-net10-new TIME_WAIT=18860 baseline=0 limit=200
[2026-01-19T09:59:01.2064545+08:00] cooldown-net10-new TIME_WAIT=18860 baseline=0 limit=200
[2026-01-19T09:59:11.8710153+08:00] cooldown-net10-new TIME_WAIT=18860 baseline=0 limit=200
[2026-01-19T09:59:22.5648486+08:00] cooldown-net10-new TIME_WAIT=18860 baseline=0 limit=200
[2026-01-19T09:59:33.2755958+08:00] cooldown-net10-new TIME_WAIT=18860 baseline=0 limit=200
[2026-01-19T09:59:43.9631547+08:00] cooldown-net10-new TIME_WAIT=18860 baseline=0 limit=200
[2026-01-19T09:59:54.6979243+08:00] cooldown-net10-new TIME_WAIT=18860 baseline=0 limit=200
[2026-01-19T10:00:05.3961397+08:00] cooldown-net10-new TIME_WAIT=18860 baseline=0 limit=200
[2026-01-19T10:00:15.8607378+08:00] cooldown-net10-new TIME_WAIT=11840 baseline=0 limit=200
[2026-01-19T10:00:25.9471407+08:00] cooldown-net10-new TIME_WAIT=614 baseline=0 limit=200
[2026-01-19T10:00:36.0111701+08:00] cooldown-net10-new TIME_WAIT=0 baseline=0 limit=200
[2026-01-19T10:00:53.1510649+08:00] cooldown-net10-static TIME_WAIT=200 baseline=0 limit=200
[2026-01-19T10:01:10.3057785+08:00] cooldown-net10-factory TIME_WAIT=200 baseline=200 limit=400
net48.out.log
url=http://localhost:5055/ping
requests=20000, parallel=200, timeoutSeconds=5
queued: 1000/20000, success: 803, failed: 0, elapsed: 00:00:00.7170409
queued: 2000/20000, success: 1803, failed: 0, elapsed: 00:00:01.5455465
queued: 3000/20000, success: 2800, failed: 0, elapsed: 00:00:02.3559601
queued: 4000/20000, success: 3801, failed: 0, elapsed: 00:00:03.1925172
queued: 5000/20000, success: 4802, failed: 0, elapsed: 00:00:04.0184093
queued: 6000/20000, success: 5802, failed: 0, elapsed: 00:00:04.8348862
queued: 7000/20000, success: 6800, failed: 0, elapsed: 00:00:05.6775563
queued: 8000/20000, success: 7801, failed: 0, elapsed: 00:00:06.5458377
queued: 9000/20000, success: 8802, failed: 0, elapsed: 00:00:07.4397727
queued: 10000/20000, success: 9803, failed: 0, elapsed: 00:00:08.3348468
queued: 11000/20000, success: 10802, failed: 0, elapsed: 00:00:09.2035416
queued: 12000/20000, success: 11804, failed: 0, elapsed: 00:00:10.0393223
queued: 13000/20000, success: 12806, failed: 0, elapsed: 00:00:10.8262272
queued: 14000/20000, success: 13804, failed: 0, elapsed: 00:00:11.6636597
queued: 15000/20000, success: 14802, failed: 0, elapsed: 00:00:12.4799716
queued: 16000/20000, success: 15819, failed: 0, elapsed: 00:00:13.4697834
queued: 17000/20000, success: 16822, failed: 0, elapsed: 00:00:14.5332049
queued: 18000/20000, success: 17818, failed: 0, elapsed: 00:00:15.5317397
queued: 19000/20000, success: 18816, failed: 0, elapsed: 00:00:16.5405963
queued: 20000/20000, success: 19819, failed: 0, elapsed: 00:00:17.6652081
done: success=20000, failed=0, elapsed=00:00:17.7541839
net6.out.log
url=http://localhost:5055/ping
requests=20000, parallel=200, timeoutSeconds=5
queued: 1000/20000, success: 803, failed: 0, elapsed: 00:00:00.7159800
queued: 2000/20000, success: 1803, failed: 0, elapsed: 00:00:01.6028433
queued: 3000/20000, success: 2802, failed: 0, elapsed: 00:00:02.4876237
queued: 4000/20000, success: 3802, failed: 0, elapsed: 00:00:03.3549543
queued: 5000/20000, success: 4802, failed: 0, elapsed: 00:00:04.2710795
queued: 6000/20000, success: 5801, failed: 0, elapsed: 00:00:05.1637653
queued: 7000/20000, success: 6802, failed: 0, elapsed: 00:00:06.0729777
queued: 8000/20000, success: 7802, failed: 0, elapsed: 00:00:07.0697555
queued: 9000/20000, success: 8801, failed: 0, elapsed: 00:00:08.0143162
queued: 10000/20000, success: 9800, failed: 0, elapsed: 00:00:08.9633860
queued: 11000/20000, success: 10802, failed: 0, elapsed: 00:00:09.9344239
queued: 12000/20000, success: 11802, failed: 0, elapsed: 00:00:10.8379783
queued: 13000/20000, success: 12801, failed: 0, elapsed: 00:00:11.6810601
queued: 14000/20000, success: 13801, failed: 0, elapsed: 00:00:12.5122642
queued: 15000/20000, success: 14800, failed: 0, elapsed: 00:00:13.3692282
queued: 16000/20000, success: 15803, failed: 0, elapsed: 00:00:14.2782829
queued: 17000/20000, success: 16801, failed: 0, elapsed: 00:00:15.2187642
queued: 18000/20000, success: 17802, failed: 0, elapsed: 00:00:16.0811154
queued: 19000/20000, success: 18810, failed: 0, elapsed: 00:00:16.9798536
queued: 20000/20000, success: 19817, failed: 0, elapsed: 00:00:17.8952478
done: success=20000, failed=0, elapsed=00:00:18.0175351
net8.out.log
url=http://localhost:5055/ping
requests=20000, parallel=200, timeoutSeconds=5
queued: 1000/20000, success: 800, failed: 0, elapsed: 00:00:00.8405997
queued: 2000/20000, success: 1802, failed: 0, elapsed: 00:00:01.6913251
queued: 3000/20000, success: 2800, failed: 0, elapsed: 00:00:02.6176324
queued: 4000/20000, success: 3802, failed: 0, elapsed: 00:00:03.4744051
queued: 5000/20000, success: 4801, failed: 0, elapsed: 00:00:04.2873345
queued: 6000/20000, success: 5800, failed: 0, elapsed: 00:00:05.0960137
queued: 7000/20000, success: 6803, failed: 0, elapsed: 00:00:05.8940500
queued: 8000/20000, success: 7802, failed: 0, elapsed: 00:00:06.7207568
queued: 9000/20000, success: 8803, failed: 0, elapsed: 00:00:07.5346954
queued: 10000/20000, success: 9800, failed: 0, elapsed: 00:00:08.3507870
queued: 11000/20000, success: 10802, failed: 0, elapsed: 00:00:09.1985154
queued: 12000/20000, success: 11803, failed: 0, elapsed: 00:00:10.0035216
queued: 13000/20000, success: 12803, failed: 0, elapsed: 00:00:10.7763067
queued: 14000/20000, success: 13802, failed: 0, elapsed: 00:00:11.6251384
queued: 15000/20000, success: 14802, failed: 0, elapsed: 00:00:12.4436652
queued: 16000/20000, success: 15807, failed: 0, elapsed: 00:00:13.3164599
queued: 17000/20000, success: 16801, failed: 0, elapsed: 00:00:14.2231530
ERR#1: HttpRequestException 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 (localhost:5055)
ERR#2: HttpRequestException 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 (localhost:5055)
ERR#3: HttpRequestException 通常每个套接���地址(协议/网络地址/端口)只允许使用一次。 (localhost:5055)
ERR#5: HttpRequestException 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 (localhost:5055)
ERR#4: HttpRequestException 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 (localhost:5055)
queued: 18000/20000, success: 17605, failed: 201, elapsed: 00:00:15.0862152
queued: 19000/20000, success: 17711, failed: 1090, elapsed: 00:00:15.6168905
queued: 20000/20000, success: 18165, failed: 1639, elapsed: 00:00:16.3332350
done: success=18361, failed=1639, elapsed=00:00:16.4268168
net10-new.out.log
url=http://localhost:5055/ping
requests=20000, parallel=200, timeoutSeconds=5, mode=new
queued: 1000/20000, success: 805, failed: 0, elapsed: 00:00:00.7099803
queued: 2000/20000, success: 1800, failed: 0, elapsed: 00:00:01.5324361
queued: 3000/20000, success: 2800, failed: 0, elapsed: 00:00:02.3573877
queued: 4000/20000, success: 3800, failed: 0, elapsed: 00:00:03.2069000
queued: 5000/20000, success: 4800, failed: 0, elapsed: 00:00:04.0313423
queued: 6000/20000, success: 5802, failed: 0, elapsed: 00:00:04.8687039
queued: 7000/20000, success: 6801, failed: 0, elapsed: 00:00:05.7252572
queued: 8000/20000, success: 7800, failed: 0, elapsed: 00:00:06.5624078
queued: 9000/20000, success: 8800, failed: 0, elapsed: 00:00:07.4244971
queued: 10000/20000, success: 9800, failed: 0, elapsed: 00:00:08.2911306
queued: 11000/20000, success: 10800, failed: 0, elapsed: 00:00:09.1755667
queued: 12000/20000, success: 11801, failed: 0, elapsed: 00:00:10.1160925
queued: 13000/20000, success: 12802, failed: 0, elapsed: 00:00:11.0165038
queued: 14000/20000, success: 13801, failed: 0, elapsed: 00:00:11.9382103
queued: 15000/20000, success: 14802, failed: 0, elapsed: 00:00:12.8002543
queued: 16000/20000, success: 15801, failed: 0, elapsed: 00:00:13.7185127
ERR#3: HttpRequestException 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 (localhost:5055)
ERR#2: HttpRequestException 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 (localhost:5055)
ERR#1: HttpRequestException 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 (localhost:5055)
ERR#5: HttpRequestException 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 (localhost:5055)
ERR#4: HttpRequestException 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 (localhost:5055)
queued: 17000/20000, success: 16682, failed: 118, elapsed: 00:00:14.7578539
queued: 18000/20000, success: 16684, failed: 1118, elapsed: 00:00:15.4297844
queued: 19000/20000, success: 17661, failed: 1140, elapsed: 00:00:16.2796589
queued: 20000/20000, success: 18661, failed: 1140, elapsed: 00:00:17.3258406
done: success=18860, failed=1140, elapsed=00:00:17.3996862
net10-static.out.log
url=http://localhost:5055/ping
requests=20000, parallel=200, timeoutSeconds=5, mode=static
queued: 1000/20000, success: 801, failed: 0, elapsed: 00:00:00.6551470
queued: 2000/20000, success: 1800, failed: 0, elapsed: 00:00:01.5355394
queued: 3000/20000, success: 2800, failed: 0, elapsed: 00:00:02.3480456
queued: 4000/20000, success: 3800, failed: 0, elapsed: 00:00:03.0999050
queued: 5000/20000, success: 4800, failed: 0, elapsed: 00:00:03.8470820
queued: 6000/20000, success: 5800, failed: 0, elapsed: 00:00:04.5759884
queued: 7000/20000, success: 6800, failed: 0, elapsed: 00:00:05.3461919
queued: 8000/20000, success: 7801, failed: 0, elapsed: 00:00:06.0916621
queued: 9000/20000, success: 8802, failed: 0, elapsed: 00:00:06.9085343
queued: 10000/20000, success: 9800, failed: 0, elapsed: 00:00:07.8274125
queued: 11000/20000, success: 10800, failed: 0, elapsed: 00:00:08.6757231
queued: 12000/20000, success: 11800, failed: 0, elapsed: 00:00:09.5154490
queued: 13000/20000, success: 12800, failed: 0, elapsed: 00:00:10.3306765
queued: 14000/20000, success: 13800, failed: 0, elapsed: 00:00:11.1493724
queued: 15000/20000, success: 14800, failed: 0, elapsed: 00:00:11.9658212
queued: 16000/20000, success: 15801, failed: 0, elapsed: 00:00:12.7510706
queued: 17000/20000, success: 16800, failed: 0, elapsed: 00:00:13.4735304
queued: 18000/20000, success: 17800, failed: 0, elapsed: 00:00:14.2777953
queued: 19000/20000, success: 18800, failed: 0, elapsed: 00:00:15.0219907
queued: 20000/20000, success: 19801, failed: 0, elapsed: 00:00:15.7699702
done: success=20000, failed=0, elapsed=00:00:15.8507052
net10-factory.out.log
url=http://localhost:5055/ping
requests=20000, parallel=200, timeoutSeconds=5, mode=factory
queued: 1000/20000, success: 801, failed: 0, elapsed: 00:00:00.7094395
queued: 2000/20000, success: 1801, failed: 0, elapsed: 00:00:01.5072383
queued: 3000/20000, success: 2800, failed: 0, elapsed: 00:00:02.3047647
queued: 4000/20000, success: 3800, failed: 0, elapsed: 00:00:03.0607252
queued: 5000/20000, success: 4800, failed: 0, elapsed: 00:00:03.8370598
queued: 6000/20000, success: 5800, failed: 0, elapsed: 00:00:04.6621606
queued: 7000/20000, success: 6800, failed: 0, elapsed: 00:00:05.4589104
queued: 8000/20000, success: 7800, failed: 0, elapsed: 00:00:06.2913588
queued: 9000/20000, success: 8800, failed: 0, elapsed: 00:00:07.0629536
queued: 10000/20000, success: 9800, failed: 0, elapsed: 00:00:07.8438472
queued: 11000/20000, success: 10800, failed: 0, elapsed: 00:00:08.5796209
queued: 12000/20000, success: 11800, failed: 0, elapsed: 00:00:09.3663975
queued: 13000/20000, success: 12801, failed: 0, elapsed: 00:00:10.1984757
queued: 14000/20000, success: 13800, failed: 0, elapsed: 00:00:11.0474925
queued: 15000/20000, success: 14800, failed: 0, elapsed: 00:00:11.9175753
queued: 16000/20000, success: 15800, failed: 0, elapsed: 00:00:12.7356429
queued: 17000/20000, success: 16801, failed: 0, elapsed: 00:00:13.4991148
queued: 18000/20000, success: 17800, failed: 0, elapsed: 00:00:14.2912941
queued: 19000/20000, success: 18800, failed: 0, elapsed: 00:00:15.0844828
queued: 20000/20000, success: 19800, failed: 0, elapsed: 00:00:15.8439387
done: success=20000, failed=0, elapsed=00:00:15.9485251
局限与备注(别把结论用错地方)
- 客户端与服务器同机:TIME_WAIT 统计包含两端连接,不能完全归因于“客户端端口耗尽”,但它足以说明“短时间制造大量短连接”这件事本身的风险。
- net48 设置了
ServicePointManager.DefaultConnectionLimit = 1000,与 net6+/net10 的连接管理策略存在差异。
关键结果汇总
TIME_WAIT 统计(端口 5055)
| 运行 | before TIME_WAIT | after TIME_WAIT | 耗时(秒) |
|---|---|---|---|
| net48 | 2 | 20002 | 17.754 |
| net6 | 0 | 20000 | 18.018 |
| net8 | 0 | 18361 | 16.427 |
| net10 | 0 | 18860 | 17.400 |
| net10-static | 0 | 200 | 15.851 |
| net10-factory | 200 | 200 | 15.949 |


来源:logs/run-20260119-095017/netstat.log
客户端执行结果(摘要)
- net48: success=20000, failed=0
- net6: success=20000, failed=0
- net8: success=18361, failed=1639(报错:“通常每个套接字地址只允许使用一次”)
- net10-new: success=18860, failed=1140(报错:“通常每个套接字地址只允许使用一次”)
- net10-static: success=20000, failed=0
- net10-factory: success=20000, failed=0
来源:logs/run-20260119-095017/*.out.log
结论
“.NET 10 了,HttpClient 还能不能 using?”——答案依然是:别把 HttpClient 当成一次性对象。
你可以 using 的是请求/响应相关的对象(例如 HttpResponseMessage),但 HttpClient 本身更像一个“连接池的门面”:它越复用,越稳定,越不容易把你推向 TIME_WAIT 地狱。
另外,从本次压测耗时来看,不做“每次请求都 using/new HttpClient” 的写法,速度其实还会稍微快一丢丢(当然差距很小)。
感谢阅读!如果你觉得这些实验分析有意思,或者对 .NET 高性能编程感兴趣,欢迎在评论区留言交流,也欢迎加入我的 .NET骚操作 QQ群:495782587,我们一起探索更多技术硬核玩法。

浙公网安备 33010602011771号