{ "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": 1, "metadata": { "dotnet_interactive": { "language": "csharp" }, "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "outputs": [ { "ename": "Error", "evalue": "System.IO.FileNotFoundException: The configuration file 'ConfigFiles/Config.json' was not found and is not optional. The expected physical path was 'C:\\Users\\asus\\.nuget\\packages\\microsoft.dotnet-interactive\\1.0.522904\\tools\\net8.0\\any\\ConfigFiles\\Config.json'.\r\n at Microsoft.Extensions.Configuration.FileConfigurationProvider.Load(Boolean reload)\r\n at Microsoft.Extensions.Configuration.ConfigurationRoot..ctor(IList`1 providers)\r\n at Microsoft.Extensions.Configuration.ConfigurationBuilder.Build()\r\n at HttpClientStudy.Config.WebApiConfigManager.GetWebApiConfigOption() in E:\\王高峰\\我的项目\\学习项目\\HttpClientStudy\\HttpClientStudy.Config\\WebApiConfigManager.cs:line 27\r\n at HttpClientStudy.Core.Utilities.StartupUtility.StartWebApiDll(String dllPath) in E:\\王高峰\\我的项目\\学习项目\\HttpClientStudy\\HttpClientStudy.Core\\Utilities\\StartupUtility.cs:line 71\r\n at Submission#2.<>d__0.MoveNext()\r\n--- End of stack trace from previous location ---\r\n at Microsoft.CodeAnalysis.Scripting.ScriptExecutionState.RunSubmissionsAsync[TResult](ImmutableArray`1 precedingExecutors, Func`2 currentExecutor, StrongBox`1 exceptionHolderOpt, Func`2 catchExceptionOpt, CancellationToken cancellationToken)", "output_type": "error", "traceback": [ "System.IO.FileNotFoundException: The configuration file 'ConfigFiles/Config.json' was not found and is not optional. The expected physical path was 'C:\\Users\\asus\\.nuget\\packages\\microsoft.dotnet-interactive\\1.0.522904\\tools\\net8.0\\any\\ConfigFiles\\Config.json'.\r\n", " at Microsoft.Extensions.Configuration.FileConfigurationProvider.Load(Boolean reload)\r\n", " at Microsoft.Extensions.Configuration.ConfigurationRoot..ctor(IList`1 providers)\r\n", " at Microsoft.Extensions.Configuration.ConfigurationBuilder.Build()\r\n", " at HttpClientStudy.Config.WebApiConfigManager.GetWebApiConfigOption() in E:\\王高峰\\我的项目\\学习项目\\HttpClientStudy\\HttpClientStudy.Config\\WebApiConfigManager.cs:line 27\r\n", " at HttpClientStudy.Core.Utilities.StartupUtility.StartWebApiDll(String dllPath) in E:\\王高峰\\我的项目\\学习项目\\HttpClientStudy\\HttpClientStudy.Core\\Utilities\\StartupUtility.cs:line 71\r\n", " at Submission#2.<>d__0.MoveNext()\r\n", "--- End of stack trace from previous location ---\r\n", " at Microsoft.CodeAnalysis.Scripting.ScriptExecutionState.RunSubmissionsAsync[TResult](ImmutableArray`1 precedingExecutors, Func`2 currentExecutor, StrongBox`1 exceptionHolderOpt, Func`2 catchExceptionOpt, CancellationToken cancellationToken)" ] } ], "source": [ "//Nuget包\n", "\n", "//全局引用\n", "#r \"./Publish/HttpClientStudy.Core/HttpClientStudy.Core.dll\"\n", "\n", "//全局对象\n", "global using HttpClientStudy.Core;\n", "global using HttpClientStudy.Core.Utilities;\n", "\n", "//启动WebAPI项目\n", "StartupUtility.StartWebApiDll(\"xxxxx\");" ] }, { "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", "//注意:不能使用百度 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", " _ = await client.GetAsync(\"https://soft.pwidc.cn\");\n", " await Task.Delay(TimeSpan.FromSeconds(2));\n", "}\n", "\n", "Console.WriteLine(\"程序运行大约要10-20秒,请在程序退出后,执行下面命令行查看网络情况\");\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "使用自定义设置,依次向同一端点发出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": [ "# 如果没有查询到相关网络状态信息,PowerShell不针对出错,但.Net Interactive 会异常:Command failed: SubmitCode: #!set --value @csharp:xxxx\n", "#!set --value @csharp:firstIp --name queryIp\n", "Write-Host \"请先执行上面的单元,再执行本单元\"\n", "Write-Host \"网络状态\"\n", "\n", "netstat -ano | findstr $queryIp" ] }, { "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", "for (var i = 0; i < 3; i++)\n", "{\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": [ "## 3、推荐使用方式" ] }, { "cell_type": "markdown", "metadata": {}, "source": [] }, { "cell_type": "code", "execution_count": null, "metadata": { "dotnet_interactive": { "language": "pwsh" }, "polyglot_notebook": { "kernelName": "pwsh" }, "vscode": { "languageId": "polyglot-notebook" } }, "outputs": [], "source": [ "#!set --value @csharp:ips --name ips\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 }