{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
}
},
"source": [
"# HttpClient 使用准则\n",
"System.Net.Http.HttpClient 类用于发送 HTTP 请求以及从 URI 所标识的资源接收 HTTP 响应。 HttpClient 实例是应用于该实例执行的所有请求的设置集合,每个实例使用自身的连接池,该池将其请求与其他请求隔离开来。 \n",
"\n",
"从 .NET Core 2.1 开始,SocketsHttpHandler 类提供实现,使行为在所有平台上保持一致。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 准备工作:先执行下面单元,以启动WebApi及设置全局对象、方法及其它"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [
{
"data": {
"text/markdown": [
"## 初始化\n",
"这是全局共用文件,包括Nuget包引用、全局类库引用、全局文件引用、全局命名空间引用、全局变量、全局方法、全局类定义等功能。\n",
"\n",
"在业务笔记中引用,执行其它单元格之前先执行一次。"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"
Installed Packages- Microsoft.Extensions.DependencyInjection, 9.0.3
- Microsoft.Extensions.Http, 9.0.3
- Microsoft.Extensions.Http.Polly, 9.0.3
- Microsoft.Extensions.Logging, 9.0.3
- Microsoft.Extensions.Logging.Console, 9.0.3
- Microsoft.Net.Http.Headers, 9.0.3
- Polly, 8.5.2
- Refit, 8.0.0
- Refit.HttpClientFactory, 8.0.0
- System.Net.Http.Json, 9.0.3
"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"配置文件根目录:d:\\软件项目\\学习项目\\HttpClientStudy\\Docs\\Publish\\HttpClientStudy.Core\n",
"配置文件根目录:d:\\软件项目\\学习项目\\HttpClientStudy\\Docs\\Publish\\HttpClientStudy.Core\n",
"程序[d:\\软件项目\\学习项目\\HttpClientStudy\\Docs\\Publish\\HttpClientStudy.WebApp\\HttpClientStudy.WebApp.exe]已在新的命令行窗口执行。如果未出现新命令行窗口,可能是程序错误造成窗口闪现!\n",
"已启动WebApi项目,保持窗口打开状态!\n",
"初始化完成!\n"
]
}
],
"source": [
"//初始化:必须先执行一次\n",
"#!import ./ini.ipynb"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"统一使用示例"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"http://127.0.0.1:5189\r\n"
]
}
],
"source": [
"{ //大括号: 1、作用域隔离 2、方便整体代码折叠\n",
" Console.WriteLine(global_api_config.BaseUrl);\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 启动WebApi"
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"#启动已发布的WebApi项目\n",
"# 使用dotnet命令启动的程序,进程名均为 dotnet,不好关闭\n",
"# Start-Process -FilePath dotnet -ArgumentList \".\\Publish\\HttpClientStudy.WebApp\\HttpClientStudy.WebApp.dll\"\n",
"\n",
"# 此种,进程名固定\n",
"Start-Process -FilePath \".\\Publish\\HttpClientStudy.WebApp\\HttpClientStudy.WebApp.exe\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 关闭WebApi"
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"HttpClientStudy.WebApp 进程已退出\r\n"
]
}
],
"source": [
"# 关闭项目进程\n",
"$WebAppProcName =\"HttpClientStudy.WebApp\";\n",
"$WebAppProc = Get-Process $WebAppProcName -ErrorAction Ignore\n",
"if($null -eq $WebAppProc)\n",
"{\n",
" Write-Host \"进程没有找到,可能已经关闭\"\n",
"}\n",
"else {\n",
" $WebAppProc.Kill();\n",
" Write-Host \"$WebAppProcName 进程已退出\"\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"source": [
"## 1、DNS 行为\n",
"HttpClient 仅在创建连接时解析 DNS。它不跟踪 DNS 服务器指定的任何生存时间 (TTL)。 \n",
"\n",
"如果 DNS 条目定期更改(这可能在某些方案中发生),客户端将不会遵循这些更新。 要解决此问题,可以通过设置 PooledConnectionLifetime 属性来限制连接的生存期,以便在替换连接时重复执行 DNS 查找。"
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [
{
"data": {
"text/html": [
"System.Net.Http.HttpClient
|
DefaultRequestHeaders | [ ]
|
Accept | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| AcceptCharset | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| AcceptEncoding | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| AcceptLanguage | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| Authorization | | ExpectContinue | | From | | Host | | IfMatch | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| IfModifiedSince | | IfNoneMatch | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| IfRange | | IfUnmodifiedSince | | MaxForwards | | Protocol | | ProxyAuthorization | | Range | | Referrer | | TE | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| UserAgent | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| Expect | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| CacheControl | | Connection | [ ]
| ConnectionClose | | Date | | Pragma | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| Trailer | [ ]
| TransferEncoding | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| TransferEncodingChunked | | Upgrade | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| Via | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| Warning | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| NonValidated | [ ]
| (values) | (empty) |
|
DefaultRequestVersion | 1.1
|
Major | | Minor | | Build | | Revision | | MajorRevision | | MinorRevision | |
|
DefaultVersionPolicy | RequestVersionOrLower |
BaseAddress | |
Timeout | 00:01:40 |
MaxResponseContentBufferSize | |
"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"using System.Net.Http;\n",
"{\n",
" var handler = new SocketsHttpHandler\n",
" {\n",
" // 15分钟\n",
" PooledConnectionLifetime = TimeSpan.FromMinutes(15) \n",
" };\n",
" var sharedClient = new HttpClient(handler);\n",
"\n",
" sharedClient.Display();\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"上述 HttpClient 配置为重复使用连接 15 分钟。 PooledConnectionLifetime 指定的时间范围过后,系统会关闭连接,然后创建一个新连接。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2、共用连接(底层自动管理连接池)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"HttpClient 的连接池链接到其基础 SocketsHttpHandler。 \n",
"释放 HttpClient 实例时,它会释放池中的所有现有连接。 如果稍后向同一服务器发送请求,则必须重新创建一个新连接。 \n",
"因此,创建不必要的连接会导致性能损失。 \n",
"此外,TCP 端口不会在连接关闭后立即释放。 (有关这一点的详细信息,请参阅 RFC 9293 中的 TCP TIME-WAIT。)如果请求速率较高,则可用端口的操作系统限制可能会耗尽。 \n",
"\n",
"为了避免端口耗尽问题,建议将 HttpClient 实例重用于尽可能多的 HTTP 请求。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 什么是连接池\n",
"SocketsHttpHandler为每个唯一端点建立连接池,您的应用程序通过HttpClient向该唯一端点发出出站HTTP请求。在对端点的第一个请求上,当不存在现有连接时,将建立一个新的HTTP连接并将其用于该请求。该请求完成后,连接将保持打开状态并返回到池中。\n",
"\n",
"对同一端点的后续请求将尝试从池中找到可用的连接。如果没有可用的连接,并且尚未达到该端点的连接限制,则将建立新的连接。达到连接限制后,请求将保留在队列中,直到连接可以自由发送它们为止。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 如何控制连接池"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"有三个主要设置可用于控制连接池的行为。\n",
"\n",
"+ PooledConnectionLifetime,定义连接在池中保持活动状态的时间。此生存期到期后,将不再为将来的请求而合并或发出连接。\n",
"\n",
"+ PooledConnectionIdleTimeout,定义闲置连接在未使用时在池中保留的时间。一旦此生存期到期,空闲连接将被清除并从池中删除。\n",
"\n",
"+ MaxConnectionsPerServer,定义每个端点将建立的最大出站连接数。每个端点的连接分别池化。例如,如果最大连接数为2,则您的应用程序将请求发送到两个www.github.com和www.google.com,总共可能最多有4个打开的连接。\n",
"\n",
"默认情况下,从.NET Core 2.1开始,更高级别的HttpClientHandler将SocketsHttpHandler用作内部处理程序。没有任何自定义配置,将应用连接池的默认设置。\n",
"\n",
"该**PooledConnectionLifetime默认是无限的,因此,虽然经常使用的请求,连接可能会无限期地保持打开状态。该PooledConnectionIdleTimeout默认为2分钟,如果在连接池中长时间未使用将被清理。MaxConnectionsPerServer**默认为int.MaxValue,因此连接基本上不受限制。\n",
"\n",
"如果希望控制这些值中的任何一个,则可以手动创建SocketsHttpHandler实例,并根据需要进行配置。"
]
},
{
"cell_type": "code",
"execution_count": 26,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [
{
"data": {
"text/html": [
"System.Net.Http.HttpClient
|
DefaultRequestHeaders | [ ]
|
Accept | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| AcceptCharset | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| AcceptEncoding | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| AcceptLanguage | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| Authorization | | ExpectContinue | | From | | Host | | IfMatch | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| IfModifiedSince | | IfNoneMatch | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| IfRange | | IfUnmodifiedSince | | MaxForwards | | Protocol | | ProxyAuthorization | | Range | | Referrer | | TE | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| UserAgent | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| Expect | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| CacheControl | | Connection | [ ]
| ConnectionClose | | Date | | Pragma | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| Trailer | [ ]
| TransferEncoding | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| TransferEncodingChunked | | Upgrade | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| Via | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| Warning | [ ]
|
Count | | IsReadOnly | | (values) | (empty) |
| NonValidated | [ ]
| (values) | (empty) |
|
DefaultRequestVersion | 1.1
|
Major | | Minor | | Build | | Revision | | MajorRevision | | MinorRevision | |
|
DefaultVersionPolicy | RequestVersionOrLower |
BaseAddress | |
Timeout | 00:01:40 |
MaxResponseContentBufferSize | |
"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"//手动配置 SocketsHttpHandler\n",
"{\n",
"\tvar socketsHandler = new SocketsHttpHandler\n",
"\t{\n",
"\t\tPooledConnectionLifetime = TimeSpan.FromMinutes(10),\n",
"\t\tPooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),\n",
"\t\tMaxConnectionsPerServer = 10\n",
"\t};\n",
"\t\t\n",
"\tvar client = new HttpClient(socketsHandler);\n",
"\n",
"\tclient.Display();\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"在前面的示例中,对SocketsHttpHandler进行了配置,以使连接将最多在10分钟后停止重新发出并关闭。如果闲置5分钟,则连接将在池的清理过程中被更早地删除。我们还将最大连接数(每个端点)限制为十个。如果我们需要并行发出更多出站请求,则某些请求可能会排队等待,直到10个池中的连接可用为止。\n",
"要应用处理程序,它将被传递到HttpClient的构造函数中。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 测试连接寿命"
]
},
{
"cell_type": "code",
"execution_count": 36,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"程序运行大约要10-20秒,请在程序退出后,执行下面命令行查看网络情况\r\n"
]
},
{
"data": {
"text/plain": [
"第5次请求完成"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"//测试连接寿命\n",
"{\n",
" Console.WriteLine(\"程序运行大约要10-20秒,请在程序退出后,执行下面命令行查看网络情况\");\n",
"\n",
" //自定义行为\n",
" var socketsHandler = new SocketsHttpHandler\n",
" {\n",
" //连接池生命周期为10分钟:连接在池中保持活动时间为10分钟\n",
" PooledConnectionLifetime = TimeSpan.FromMinutes(10),\n",
"\n",
" //池化链接的空闲超时时间为5分钟: 5分钟内连接不被重用,则被释放后销毁\n",
" PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),\n",
" \n",
" //每端点的最大连接数设置为10个\n",
" MaxConnectionsPerServer = 10\n",
" };\n",
"\n",
" var client = new HttpClient(socketsHandler)\n",
" {\n",
" BaseAddress = new Uri(global_api_config.BaseUrl)\n",
" };\n",
"\n",
" var displayer = \"\".Display();\n",
"\n",
" for (var i = 0; i < 5; i++)\n",
" {\n",
" if(i>0)\n",
" {\n",
" await Task.Delay(TimeSpan.FromSeconds(2));\n",
" }\n",
" _ = await client.GetAsync(global_default_page);\n",
" displayer.Update(($\"第{i+1}次请求完成\"));\n",
" \n",
" await Task.Delay(TimeSpan.FromSeconds(2));\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"使用自定义设置,依次向同一端点发出5个请求。在每个请求之间,暂停两秒钟。输出从DNS检索到的网站服务器的IPv4地址。我们可以使用此IP地址来查看通过PowerShell中发出的netstat命令对其打开的连接:"
]
},
{
"cell_type": "code",
"execution_count": 37,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"请先执行上面的单元,再执行本单元\n",
"网络状态\n"
]
}
],
"source": [
"# 若查询不到,则异常\n",
"#!set --value @csharp:global_netstat_filter --name queryFilter\n",
"\n",
"Write-Host \"请先执行上面的单元,再执行本单元\"\n",
"Write-Host \"网络状态\"\n",
"netstat -ano | findstr $queryFilter"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"在这种情况下,到远程端点的连接只有1个。在每个请求之后,该连接将返回到池中,因此在发出下一个请求时可以重新使用。\n",
"如果更改连接的生存期,以使它们在1秒后过期,测试这对行为的影响:"
]
},
{
"cell_type": "code",
"execution_count": 38,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"程序运行大约要10-20,请在程序退出后,执行下面命令行查看网络情况\r\n"
]
},
{
"data": {
"text/plain": [
"第5次请求完成"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
" TCP 127.0.0.1:45262 127.0.0.1:5189 TIME_WAIT 0\r\n",
" TCP 127.0.0.1:45269 127.0.0.1:5189 TIME_WAIT 0\r\n",
" TCP 127.0.0.1:45274 127.0.0.1:5189 TIME_WAIT 0\r\n",
" TCP 127.0.0.1:45283 127.0.0.1:5189 TIME_WAIT 0\r\n",
" TCP 127.0.0.1:45296 127.0.0.1:5189 TIME_WAIT 0\r\n",
"\r\n"
]
}
],
"source": [
"//程序池设置\n",
"{ \n",
" //自定义行为\n",
" Console.WriteLine(\"程序运行大约要10-20,请在程序退出后,执行下面命令行查看网络情况\");\n",
" var socketsHandler2 = new SocketsHttpHandler\n",
" {\n",
" PooledConnectionLifetime = TimeSpan.FromSeconds(1),\n",
" PooledConnectionIdleTimeout = TimeSpan.FromSeconds(1),\n",
" MaxConnectionsPerServer = 1\n",
" };\n",
"\n",
" var client2 = new HttpClient(socketsHandler2)\n",
" {\n",
" BaseAddress = new Uri(global_api_config.BaseUrl)\n",
" };\n",
"\n",
" var displayer = \"\".Display();\n",
"\n",
" for (var i = 0; i < 5; i++)\n",
" {\n",
" if(i>0)\n",
" {\n",
" await Task.Delay(TimeSpan.FromSeconds(2));\n",
" }\n",
" _ = await client2.GetAsync(global_default_page);\n",
" displayer.Update(($\"第{i+1}次请求完成\"));\n",
" \n",
" await Task.Delay(TimeSpan.FromSeconds(2));\n",
" }\n",
" \n",
" //调用命令行,显示查看网络情况\n",
" string command = $\"netstat -ano | findstr {global_netstat_filter}\";\n",
" \n",
" // 创建一个新的ProcessStartInfo对象\n",
" ProcessStartInfo startInfo = new ProcessStartInfo(\"cmd\", $\"/c {command}\")\n",
" {\n",
" RedirectStandardOutput = true, // 重定向标准输出\n",
" UseShellExecute = false, // 不使用系统外壳程序启动\n",
" CreateNoWindow = true // 不创建新窗口\n",
" };\n",
" \n",
" // 启动进程\n",
" using (Process process = Process.Start(startInfo))\n",
" {\n",
" // 读取cmd的输出\n",
" using (StreamReader reader = process.StandardOutput)\n",
" {\n",
" string stdoutLine = reader.ReadToEnd();\n",
" Console.WriteLine(stdoutLine);\n",
" }\n",
" }\n",
"}"
]
},
{
"cell_type": "code",
"execution_count": 39,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
" TCP 127.0.0.1:45262 127.0.0.1:5189 TIME_WAIT 0\n",
" TCP 127.0.0.1:45269 127.0.0.1:5189 TIME_WAIT 0\n",
" TCP 127.0.0.1:45274 127.0.0.1:5189 TIME_WAIT 0\n",
" TCP 127.0.0.1:45283 127.0.0.1:5189 TIME_WAIT 0\n",
" TCP 127.0.0.1:45296 127.0.0.1:5189 TIME_WAIT 0\n"
]
}
],
"source": [
"#!set --value @csharp:global_netstat_filter --name queryFilter\n",
"netstat -ano | findstr $queryFilter"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"在这种情况下,我们可以看到使用了五个连接。其中的前四个在1秒后从池中删除,因此无法在下一个请求中重复使用。结果,每个请求都打开了一个新连接。现在,原始连接处于TIME_WAIT状态,并且操作系统无法将其重新用于新的出站连接。最终连接显示为ESTABLISHED,因为我在它过期之前就抓住了它。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 测试最大连接数"
]
},
{
"cell_type": "code",
"execution_count": 40,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"开始请求网络...\n",
"共请求了200次,耗时 55 毫秒\n",
"当前网络状态\n",
" TCP 127.0.0.1:5189 127.0.0.1:45299 ESTABLISHED 42312\n",
" TCP 127.0.0.1:5189 127.0.0.1:45300 ESTABLISHED 42312\n",
" TCP 127.0.0.1:45262 127.0.0.1:5189 TIME_WAIT 0\n",
" TCP 127.0.0.1:45269 127.0.0.1:5189 TIME_WAIT 0\n",
" TCP 127.0.0.1:45274 127.0.0.1:5189 TIME_WAIT 0\n",
" TCP 127.0.0.1:45283 127.0.0.1:5189 TIME_WAIT 0\n",
" TCP 127.0.0.1:45296 127.0.0.1:5189 TIME_WAIT 0\n",
" TCP 127.0.0.1:45299 127.0.0.1:5189 ESTABLISHED 37024\n",
" TCP 127.0.0.1:45300 127.0.0.1:5189 ESTABLISHED 37024\n",
"\n"
]
}
],
"source": [
"/*\n",
"\t功能:将MaxConnectionsPerServer限制为2。然后启动200个任务,每个任务都向同一端点发出HTTP请求。这些任务将同时运行。所有请求竞争所花费的时间将写入控制台。\n",
"\t\t 随即调用用netstat命令查看连接:则根据定义的限制,我们可以看到两个已建立的连接。\n",
"*/\n",
"{\n",
"\tConsole.WriteLine(\"开始请求网络...\");\n",
"\tvar socketsHandler = new SocketsHttpHandler\n",
"\t{\n",
"\t\tPooledConnectionLifetime = TimeSpan.FromSeconds(60),\n",
"\t\tPooledConnectionIdleTimeout = TimeSpan.FromMinutes(20),\n",
"\t\tMaxConnectionsPerServer = 2\n",
"\t};\n",
"\n",
"\tvar client = new HttpClient(socketsHandler)\n",
"\t{\n",
"\t\tBaseAddress = new Uri(global_api_config.BaseUrl)\n",
"\t};\n",
"\n",
"\tvar sw = Stopwatch.StartNew();\n",
"\n",
"\tvar tasks = Enumerable.Range(0, 200).Select(i => client.GetAsync(global_default_page));\n",
"\n",
"\tawait Task.WhenAll(tasks);\n",
"\n",
"\tsw.Stop();\n",
"\t\t\n",
"\tConsole.WriteLine($\"共请求了200次,耗时 {sw.ElapsedMilliseconds} 毫秒\");\n",
"\t\t\n",
"\t//执行查看网络状态方法\n",
"\tConsole.WriteLine(\"当前网络状态\");\n",
"\tvar message = HttpClientStudy.Core.Utilities.AppUtility.RunCmd($\"netstat -ano | findstr {global_netstat_filter}\");\n",
"\tConsole.WriteLine(message);\n",
"}"
]
},
{
"cell_type": "code",
"execution_count": 41,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
" TCP 127.0.0.1:5189 127.0.0.1:45299 ESTABLISHED 42312\n",
" TCP 127.0.0.1:5189 127.0.0.1:45300 ESTABLISHED 42312\n",
" TCP 127.0.0.1:45262 127.0.0.1:5189 TIME_WAIT 0\n",
" TCP 127.0.0.1:45269 127.0.0.1:5189 TIME_WAIT 0\n",
" TCP 127.0.0.1:45274 127.0.0.1:5189 TIME_WAIT 0\n",
" TCP 127.0.0.1:45283 127.0.0.1:5189 TIME_WAIT 0\n",
" TCP 127.0.0.1:45296 127.0.0.1:5189 TIME_WAIT 0\n",
" TCP 127.0.0.1:45299 127.0.0.1:5189 ESTABLISHED 37024\n",
" TCP 127.0.0.1:45300 127.0.0.1:5189 ESTABLISHED 37024\n"
]
}
],
"source": [
"# 重新查询当前网络状态\n",
"#!set --value @csharp:global_netstat_filter --name queryFilter\n",
"netstat -ano | findstr $queryFilter"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"如果我们调整此代码以允许MaxConnectionsPerServer = 10,则可以重新运行该应用程序。耗时将减少大约4倍。"
]
},
{
"cell_type": "code",
"execution_count": 42,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"开始请求网络...\n",
"共请求了200次,耗时 39 毫秒\n",
"当前网络状态\n",
" TCP 127.0.0.1:5189 127.0.0.1:45299 ESTABLISHED 42312\n",
" TCP 127.0.0.1:5189 127.0.0.1:45300 ESTABLISHED 42312\n",
" TCP 127.0.0.1:5189 127.0.0.1:45307 ESTABLISHED 42312\n",
" TCP 127.0.0.1:5189 127.0.0.1:45308 ESTABLISHED 42312\n",
" TCP 127.0.0.1:5189 127.0.0.1:45309 ESTABLISHED 42312\n",
" TCP 127.0.0.1:5189 127.0.0.1:45310 ESTABLISHED 42312\n",
" TCP 127.0.0.1:5189 127.0.0.1:45311 ESTABLISHED 42312\n",
" TCP 127.0.0.1:5189 127.0.0.1:45312 ESTABLISHED 42312\n",
" TCP 127.0.0.1:5189 127.0.0.1:45313 ESTABLISHED 42312\n",
" TCP 127.0.0.1:5189 127.0.0.1:45314 ESTABLISHED 42312\n",
" TCP 127.0.0.1:5189 127.0.0.1:45315 ESTABLISHED 42312\n",
" TCP 127.0.0.1:5189 127.0.0.1:45316 ESTABLISHED 42312\n",
" TCP 127.0.0.1:45262 127.0.0.1:5189 TIME_WAIT 0\n",
" TCP 127.0.0.1:45269 127.0.0.1:5189 TIME_WAIT 0\n",
" TCP 127.0.0.1:45274 127.0.0.1:5189 TIME_WAIT 0\n",
" TCP 127.0.0.1:45283 127.0.0.1:5189 TIME_WAIT 0\n",
" TCP 127.0.0.1:45296 127.0.0.1:5189 TIME_WAIT 0\n",
" TCP 127.0.0.1:45299 127.0.0.1:5189 ESTABLISHED 37024\n",
" TCP 127.0.0.1:45300 127.0.0.1:5189 ESTABLISHED 37024\n",
" TCP 127.0.0.1:45307 127.0.0.1:5189 ESTABLISHED 37024\n",
" TCP 127.0.0.1:45308 127.0.0.1:5189 ESTABLISHED 37024\n",
" TCP 127.0.0.1:45309 127.0.0.1:5189 ESTABLISHED 37024\n",
" TCP 127.0.0.1:45310 127.0.0.1:5189 ESTABLISHED 37024\n",
" TCP 127.0.0.1:45311 127.0.0.1:5189 ESTABLISHED 37024\n",
" TCP 127.0.0.1:45312 127.0.0.1:5189 ESTABLISHED 37024\n",
" TCP 127.0.0.1:45313 127.0.0.1:5189 ESTABLISHED 37024\n",
" TCP 127.0.0.1:45314 127.0.0.1:5189 ESTABLISHED 37024\n",
" TCP 127.0.0.1:45315 127.0.0.1:5189 ESTABLISHED 37024\n",
" TCP 127.0.0.1:45316 127.0.0.1:5189 ESTABLISHED 37024\n",
"\n"
]
}
],
"source": [
"{ //MaxConnectionsPerServer 设置为10:网络连接将增加到10个,耗时将减少到1/4\n",
"\tConsole.WriteLine(\"开始请求网络...\");\n",
"\tvar socketsHandler = new SocketsHttpHandler\n",
"\t{\n",
"\t\tPooledConnectionLifetime = TimeSpan.FromSeconds(60),\n",
"\t\tPooledConnectionIdleTimeout = TimeSpan.FromMinutes(20),\n",
"\t\tMaxConnectionsPerServer = 10\n",
"\t};\n",
"\n",
"\tvar client = new HttpClient(socketsHandler)\n",
"\t{\n",
"\t\tBaseAddress = new Uri(global_api_config.BaseUrl)\n",
"\t};\n",
"\n",
"\t//client.Display();\n",
"\n",
"\tvar sw = Stopwatch.StartNew();\n",
"\n",
"\tvar tasks = Enumerable.Range(0, 200).Select(i => client.GetAsync(global_default_page));\n",
"\n",
"\tawait Task.WhenAll(tasks);\n",
"\n",
"\tsw.Stop();\n",
"\t\t\n",
"\tConsole.WriteLine($\"共请求了200次,耗时 {sw.ElapsedMilliseconds} 毫秒\");\n",
"\t\t\n",
"\t//执行查看网络状态方法\n",
"\tConsole.WriteLine(\"当前网络状态\");\n",
"\tvar message = AppUtility.RunCmd($\"netstat -ano | findstr {global_netstat_filter}\");\n",
"\tConsole.WriteLine(message);\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3、推荐使用方式"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"总则:\n",
"\n",
" 一、 应使用长期客户端(静态对象、单例等),并设置 PooledConnectionLifetime。这能解决DNS问题和套接字耗尽问题。\n",
" \n",
" 二、 使用 IHttpClientFactory 创建的短期客户端:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"+ 在 .NET Core 和 .NET 5+ 中:\n",
" \n",
" + 根据预期的 DNS 更改,使用 static 或 singletonHttpClient 实例,并将 PooledConnectionLifetime 设置为所需间隔(例如 2 分钟)。 这可以解决端口耗尽和 DNS 更改两个问题,而且不会增加 IHttpClientFactory 的开销。 如果需要模拟处理程序,可以单独注册它。\n",
" \n",
" + 使用 IHttpClientFactory,可以针对不同的用例使用多个以不同方式配置的客户端。 但请注意,工厂创建的客户端生存期较短,一旦创建客户端,工厂就不再可以控制它。\n",
" 工厂合并 HttpMessageHandler 实例,如果其生存期尚未过期,则当工厂创建新的 HttpClient 实例时,可以从池中重用处理程序。 这种重用避免了任何套接字耗尽问题。\n",
" 如果需要 IHttpClientFactory 提供的可配置性,我们建议使用类型化客户端方法。\n",
"\n",
"+ 在 .NET Framework 中,使用 IHttpClientFactory 管理 HttpClient 实例。 如果不使用工厂,而是改为自行为每个请求创建新的客户端实例,则可能耗尽可用的端口。 \n",
"\n",
"`提示`: 如果应用需要 Cookie,请考虑禁用自动 Cookie 处理或避免使用 IHttpClientFactory。 共用 HttpMessageHandler 实例会导致共享 CookieContainer 对象。 意外的 CookieContainer 对象共享通常会导致错误的代码。"
]
},
{
"cell_type": "code",
"execution_count": 43,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"{ //不推荐的示例\n",
" int requestCount =0;\n",
"\n",
" //这会建立10个 HttpClient \n",
" //尽管使用了Using,不过Using只保证应用进程释放实例;但是http请求是跨操作系统、跨网络的操作,调用Using的进程管不了操作系统,更管不了网络。\n",
" //如果把循环次数加大到 65535 就会一定导致夏套接字耗尽(2000以很可能就会出现)。\n",
" Parallel.For(0,10,async (a,b)=>\n",
" {\n",
" using (var client = new HttpClient())\n",
" {\n",
" _ = await client.GetAsync (global_api_config.BaseUrl + global_default_page);\n",
" } \n",
" Interlocked.Add(ref requestCount, 1);\n",
" });\n",
"}\n",
"\n",
"{ //使用长期客户端\n",
" using (var client = new HttpClient())\n",
" {\n",
" client.BaseAddress = new Uri(global_api_config.BaseUrl);\n",
" \n",
" for(int i=0; i<10; i++)\n",
" {\n",
" //n次调用,均使用同一个 HttpClient 实例\n",
" _ = await client.GetAsync(global_default_page);\n",
" }\n",
" }// 所有调用完成,才释放 HttpClient 实例\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4、静态客户端的复原能力"
]
},
{
"cell_type": "code",
"execution_count": 44,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [
{
"data": {
"text/html": [
"Installed Packages- Microsoft.Extensions.Http.Resilience, 9.3.0
- Polly, 8.5.2
"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"共有1195个字符\r\n"
]
}
],
"source": [
"#r \"nuget:Polly\"\n",
"#r \"nuget:Microsoft.Extensions.Http.Resilience\"\n",
"using System;\n",
"using System.Net.Http;\n",
"using Microsoft.Extensions.Http;\n",
"using Microsoft.Extensions.Http.Resilience;\n",
"using Polly;\n",
"\n",
"{\n",
" var retryPipeline = new ResiliencePipelineBuilder()\n",
" .AddRetry(new HttpRetryStrategyOptions\n",
" {\n",
" BackoffType = DelayBackoffType.Exponential,\n",
" MaxRetryAttempts = 3\n",
" })\n",
" .Build();\n",
"\n",
" var socketHandler = new SocketsHttpHandler\n",
" {\n",
" PooledConnectionLifetime = TimeSpan.FromMinutes(15)\n",
" };\n",
"\n",
" #pragma warning disable EXTEXP0001\n",
" var resilienceHandler = new ResilienceHandler(retryPipeline)\n",
" {\n",
" InnerHandler = socketHandler,\n",
" };\n",
" #pragma warning restore EXTEXP0001\n",
"\n",
" var httpClient = new HttpClient(resilienceHandler);\n",
" httpClient.BaseAddress = new Uri(global_api_config.BaseUrl);\n",
"\n",
" var response = await httpClient.GetAsync(global_default_page);\n",
" var htmlText = await response.Content.ReadAsStringAsync();\n",
" Console.WriteLine($\"共有{htmlText.Length}个字符\");\n",
"}\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".NET (C#)",
"language": "C#",
"name": ".net-csharp"
},
"language_info": {
"name": "python"
},
"polyglot_notebook": {
"kernelInfo": {
"defaultKernelName": "csharp",
"items": [
{
"aliases": [],
"name": "csharp"
}
]
}
}
},
"nbformat": 4,
"nbformat_minor": 2
}