You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

439 lines
14 KiB
Plaintext

{
"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 类提供实现,使行为在所有平台上保持一致。"
]
},
4 months ago
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 准备工作先执行下面单元以启动WebApi及设置全局对象、方法及其它"
]
},
{
"cell_type": "code",
4 months ago
"execution_count": null,
4 months ago
"metadata": {
4 months ago
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
4 months ago
"vscode": {
"languageId": "polyglot-notebook"
}
},
4 months ago
"outputs": [],
4 months ago
"source": [
"//Nuget包\n",
"\n",
"//全局引用\n",
"#r \"./Publish/HttpClientStudy.Core/HttpClientStudy.Core.dll\"\n",
"\n",
"//全局对象\n",
4 months ago
"global using System.IO;\n",
4 months ago
"global using HttpClientStudy.Core;\n",
"global using HttpClientStudy.Core.Utilities;\n",
"\n",
4 months ago
"string dllFile = Path.GetFullPath(\"Publish\\\\HttpClientStudy.WebApp\\\\HttpClientStudy.WebApp.dll\",Directory.GetCurrentDirectory());\n",
"Console.WriteLine($\"指定的文件为:{dllFile}\");\n",
4 months ago
"//启动WebAPI项目\n",
4 months ago
"StartupUtility.StartWebApiDll(dllFile);"
4 months ago
]
},
{
"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": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"using System.Net.Http;\n",
"\n",
"var handler = new SocketsHttpHandler\n",
"{\n",
" // Recreate every 15 minutes\n",
" PooledConnectionLifetime = TimeSpan.FromMinutes(15) \n",
"};\n",
"var sharedClient = new HttpClient(handler);"
]
},
{
"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": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"using System.Net.Http;\n",
"var socketsHandler = new SocketsHttpHandler\n",
"{\n",
"\tPooledConnectionLifetime = TimeSpan.FromMinutes(10),\n",
"\tPooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),\n",
"\tMaxConnectionsPerServer = 10\n",
"};\n",
"\t\n",
"var client = new HttpClient(socketsHandler);"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"在前面的示例中对SocketsHttpHandler进行了配置以使连接将最多在10分钟后停止重新发出并关闭。如果闲置5分钟则连接将在池的清理过程中被更早地删除。我们还将最大连接数每个端点限制为十个。如果我们需要并行发出更多出站请求则某些请求可能会排队等待直到10个池中的连接可用为止。\n",
"要应用处理程序它将被传递到HttpClient的构造函数中。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 测试连接寿命"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"using System.Net;\n",
"using System.Net.Http;\n",
"\n",
4 months ago
"//注意:不能使用百度 hao123等站点可能是大厂服务器的设置问题会导致查不到效果\n",
"var ips = await Dns.GetHostAddressesAsync(\"soft.pwidc.cn\");\n",
"string firstIp = ips.FirstOrDefault().ToString();\n",
"\t\n",
"foreach (var ipAddress in ips)\n",
"{\n",
" Console.WriteLine(ipAddress.MapToIPv4().ToString());\n",
"}\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",
"for (var i = 0; i < 5; i++)\n",
"{\n",
4 months ago
" _ = await client.GetAsync(\"https://soft.pwidc.cn\");\n",
" await Task.Delay(TimeSpan.FromSeconds(2));\n",
"}\n",
"\n",
4 months ago
"Console.WriteLine(\"程序运行大约要10-20秒请在程序退出后执行下面命令行查看网络情况\");\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
4 months ago
"使用自定义设置依次向同一端点发出5个请求。在每个请求之间暂停两秒钟。输出从DNS检索到的网站服务器的IPv4地址。我们可以使用此IP地址来查看通过PowerShell中发出的netstat命令对其打开的连接"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
4 months ago
"# 如果没有查询到相关网络状态信息PowerShell不针对出错但.Net Interactive 会异常Command failed: SubmitCode: #!set --value @csharp:xxxx\n",
"#!set --value @csharp:firstIp --name queryIp\n",
"Write-Host \"请先执行上面的单元,再执行本单元\"\n",
4 months ago
"Write-Host \"网络状态\"\n",
"\n",
"netstat -ano | findstr $queryIp"
4 months ago
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"在这种情况下到远程端点的连接只有1个。在每个请求之后该连接将返回到池中因此在发出下一个请求时可以重新使用。\n",
"如果更改连接的生存期以使它们在1秒后过期测试这对行为的影响"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"using System.IO;\n",
"using System.Diagnostics;\n",
"using System.Net;\n",
"using System.Net.Http;\n",
"\n",
"var ips = await Dns.GetHostAddressesAsync(\"soft.pwidc.cn\");\n",
"string firstIp = ips.FirstOrDefault().ToString();\n",
"\t\n",
"foreach (var ipAddress in ips)\n",
"{\n",
" Console.WriteLine(ipAddress.MapToIPv4().ToString());\n",
"}\n",
"\n",
"//自定义行为\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",
4 months ago
"for (var i = 0; i < 5; i++)\n",
4 months ago
"{\n",
" if(i>0)\n",
" {\n",
" await Task.Delay(TimeSpan.FromSeconds(2));\n",
" }\n",
" _ = await client2.GetAsync(\"http://soft.pwidc.cn\");\n",
"}\n",
"\n",
"Console.WriteLine(\"程序运行大约要10-20请在程序退出后执行下面命令行查看网络情况\");\n",
"\n",
"//调用命令行,显示查看网络情况\n",
"string command = $\"netstat -ano | findstr {firstIp}\";\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",
"}"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"#!set --value @csharp:firstIp --name queryIp\n",
"netstat -ano | findstr $queryIp"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
4 months ago
"在这种情况下我们可以看到使用了五个连接。其中的前四个在1秒后从池中删除因此无法在下一个请求中重复使用。结果每个请求都打开了一个新连接。现在原始连接处于TIME_WAIT状态并且操作系统无法将其重新用于新的出站连接。最终连接显示为ESTABLISHED因为我在它过期之前就抓住了它。"
]
},
{
"cell_type": "markdown",
"metadata": {},
4 months ago
"source": [
"### 测试最大连接数"
]
},
{
4 months ago
"cell_type": "markdown",
"metadata": {},
"source": [
4 months ago
"## 3、推荐使用方式"
]
}
],
"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
}