惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
雷峰网
雷峰网
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Spread Privacy
Spread Privacy
H
Hacker News: Front Page
PCI Perspectives
PCI Perspectives
Webroot Blog
Webroot Blog
罗磊的独立博客
H
Heimdal Security Blog
TaoSecurity Blog
TaoSecurity Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
大猫的无限游戏
大猫的无限游戏
月光博客
月光博客
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
Last Week in AI
Last Week in AI
美团技术团队
Help Net Security
Help Net Security
The Hacker News
The Hacker News
C
Cisco Blogs
T
The Blog of Author Tim Ferriss
J
Java Code Geeks
The Register - Security
The Register - Security
IT之家
IT之家
WordPress大学
WordPress大学
Jina AI
Jina AI
Recent Commits to openclaw:main
Recent Commits to openclaw:main
H
Help Net Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Threat Research - Cisco Blogs
P
Proofpoint News Feed
NISL@THU
NISL@THU
爱范儿
爱范儿
The GitHub Blog
The GitHub Blog
Scott Helme
Scott Helme
V
Vulnerabilities – Threatpost
B
Blog
T
Tenable Blog
博客园 - 三生石上(FineUI控件)
T
The Exploit Database - CXSecurity.com
S
Security Affairs
小众软件
小众软件
Hacker News: Ask HN
Hacker News: Ask HN
Security Latest
Security Latest
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
W
WeLiveSecurity
A
Arctic Wolf
L
LINUX DO - 热门话题
Google DeepMind News
Google DeepMind News
M
MIT News - Artificial intelligence

博客园 - .NET骚操作

.NET 10 的正则性能现在什么水平?我拿它和 Go、Python、C++、PCRE2 测了一轮 C++ vs .NET 数组原地反转实测:小数组 C++ 碾压,大数组 .NET 反杀? Sdcb Chats 1.10 私有化代码执行器部署教程 复刻 ChatGPT 高级数据分析!Sdcb Chats 1.10 重磅发布:能分析Excel、做PPT 两天烧掉200美元!我AI大模型网关终于支持了Claude模型 Sdcb Chats 1.8:又一次底层重构,彻底将模型提供商解耦 没有前端后,我把 MCP 做进了 Chats 1.7.0 AI 网关 不服跑个分?.NET 10 大整数计算对阵 Java,结果令人意外 从 OpenAI 兼容到 Anthropic 崛起:大模型“交错思考”协议的演进与变局 当加密ID需要变成Guid:为什么我选择了AES-CBC而非GCM? AES 加密模式演进:从 ECB、CBC 到 GCM 的 C# 深度实践 美丽而脆弱的天体运动:当C#遇见宇宙混沌 我是不是很有钱? Chats 1.6.8 发布:全面支持最新的 gpt-5 模型等 Sdcb Chats 1.6.6 发布,彻底移除 Azure.AI.OpenAI 专用包 那些被推迟的 C# 14 特性及其背后的故事 我最喜欢的 C# 14 新特性 更复杂的代码,为何跑得快了10倍?一次Draw Call优化引发的思考 一个被BCL遗忘的高性能集合:C# CircularBuffer<T>深度解析 当物理定律与高精度计算相遇:我的新开源项目 N-Body
.NET 10了,HttpClient还是不能用using吗?我做了一个实验
.NET骚操作 · 2026-01-23 · via 博客园 - .NET骚操作

引言:这个“最佳实践”到底过时了吗?

每隔一段时间,就会看到类似问题反复出现:

“都 .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(),不就释放资源了吗?

但这里有两个关键点经常被忽略:

  1. HttpClient 并不是“请求一次就关一次连接”的简单模型。HTTP Keep-Alive + 连接池的存在,意味着正确姿势应该是复用底层连接(或至少复用 handler 的连接池),让大量请求复用少量 TCP 连接。
  2. 你频繁 new HttpClient + Dispose(),等价于频繁建立 TCP 连接并快速关闭。而 TCP 连接的关闭会进入 TIME_WAIT(具体哪一端进入 TIME_WAIT 与关闭时序有关),TIME_WAIT 存在的意义是保护网络不被“旧连接的残留包”污染。

在“200 并发 + 2 万次请求”这种参数下,如果你让每个请求都创建新连接,那么很容易短时间制造大量 TIME_WAIT;一旦本机可用的临时端口范围被 TIME_WAIT 占满(或接近占满),新连接就会开始失败,典型异常就是:

通常每个套接字地址(协议/网络地址/端口)只允许使用一次。

这也是为什么同样的代码:

  • 在低并发下“看起来完全没问题”
  • 在压力一上来就开始“玄学报错”

那到底怎么写才对?

本文不展开“所有场景的最佳实践”,只给两条最能落地的结论:

  1. 业务代码不要每次请求 new HttpClient。要么复用单例/静态 HttpClient,要么使用 IHttpClientFactory
  2. 可以 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

局限与备注(别把结论用错地方)

  1. 客户端与服务器同机:TIME_WAIT 统计包含两端连接,不能完全归因于“客户端端口耗尽”,但它足以说明“短时间制造大量短连接”这件事本身的风险。
  2. 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

elapsed_seconds
time_wait_delta

来源: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,我们一起探索更多技术硬核玩法。