{ "cells": [ { "cell_type": "markdown", "metadata": { "dotnet_interactive": { "language": "csharp" }, "polyglot_notebook": { "kernelName": "csharp" } }, "source": [ "# HttpClient 初始化与生命周期管理" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "HttpClient 旨在实例化一次,并在应用程序的整个生命周期内重复使用。\n", "\n", "为实现复用,HttpClient类库默认使用连接池和请求管道,可以手动管理(连接池、配置管道、使用Polly); 结合IoC容器、工厂模式(提供了IHttpClientFactory类库)、复原库Polly,可以更加方便、完善的使用,这也是推荐的方法。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 0、初始化与全局设置" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "dotnet_interactive": { "language": "csharp" }, "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "outputs": [ { "data": { "text/html": [ "
Installed Packages
" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "配置文件根目录:e:\\王高峰\\我的项目\\学习项目\\HttpClientStudy\\Docs\\Publish\\HttpClientStudy.Core\n", "启动WebApi项目\n", "程序[e:\\王高峰\\我的项目\\学习项目\\HttpClientStudy\\Docs\\Publish\\HttpClientStudy.WebApp\\HttpClientStudy.WebApp.exe]已在新的命令行窗口执行。如果未出现新命令行窗口,可能是程序错误造成窗口闪现!\n" ] } ], "source": [ "//全局设置,行运行一次,为后续准备\n", "#r \"nuget:System.Net.Http.Json\"\n", "#r \"nuget:Microsoft.Extensions.Http\"\n", "#r \"nuget:Microsoft.Extensions.DependencyInjection\"\n", "#r \"nuget:Polly\"\n", "#r \"nuget:Microsoft.Extensions.Http.Polly\"\n", "#r \"nuget:Refit\" \n", "#r \"nuget:Refit.HttpClientFactory\"\n", "#r \"./Publish/HttpClientStudy.Core/HttpClientStudy.Core.dll\"\n", "\n", "global using System;\n", "global using System.IO;\n", "global using System.IO.Enumeration;\n", "global using System.Buffers;\n", "\n", "global using System.Collections;\n", "global using System.Collections.Concurrent;\n", "global using System.Linq;\n", "global using System.Linq.Expressions;\n", "global using System.Threading;\n", "global using System.Threading.Tasks;\n", "\n", "global using System.Net.Http;\n", "global using System.Net.Http.Headers;\n", "\n", "global using Microsoft.Extensions.DependencyInjection;\n", "global using Microsoft.Extensions.DependencyInjection.Extensions;\n", "\n", "global using Polly;\n", "global using Polly.NoOp;\n", "global using Polly.Simmy;\n", "global using Polly.Retry;\n", "global using Polly.Hedging;\n", "global using Polly.Timeout;\n", "global using Polly.Bulkhead;\n", "global using Polly.Fallback;\n", "global using Polly.RateLimit;\n", "global using Polly.CircuitBreaker;\n", "global using Polly.Utilities;\n", "global using Polly.Extensions;\n", "global using Polly.Wrap;\n", "global using Polly.Registry;\n", "global using Polly.Telemetry;\n", "\n", "global using Refit;\n", "//global using Refit.HttpClientFactory;\n", "\n", "global using HttpClientStudy.Config;\n", "global using HttpClientStudy.Core;\n", "global using HttpClientStudy.Core.Utilities;\n", "\n", "//全局变量\n", "var webApiBaseUrl = WebApiConfigManager.GetWebApiConfig().BaseUrl;\n", "var workDir = Environment.CurrentDirectory;\n", "var fullPath = System.IO.Path.GetFullPath(\"./Publish/HttpClientStudy.WebApp/HttpClientStudy.WebApp.exe\", workDir);\n", "//fullPath.Display();\n", "\n", "//启动已发布的WebApi项目\n", "Console.WriteLine(\"启动WebApi项目\");\n", "var startMessage = AppUtility.RunWebApiExeFile(fullPath);\n", "Console.WriteLine(startMessage);\n" ] }, { "cell_type": "markdown", "metadata": { "dotnet_interactive": { "language": "csharp" }, "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "source": [ "## 1、手动管理:直接实例化-强烈不推荐" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "下面这种每次使用都实例化的用法是最常见、最`不推荐的`:\n", "\n", "因为HttpClient刚推出时不成熟及微软官方文档的示例代码是这种用法,再加上这种是最简单方便的使用方法,就造成很多人使用这种用法。\n", "这种方法有如下缺点:\n", " 1. 每次使用都实例化,造成性能开销大、容易内存泄露;\n", " 2. 并发量大、请求频繁时:网络端口会被耗尽 `Using包HttpClient,也只是在应用进程中释放了HttpClient实例,但http请求/响应是跨操作系统和网络的,而系统及网络问题在进程之上,不是进程所能处理的。`\n", " \n", "优点:\n", " 1. 使用简单,好学易用;\n", " 2. 并发量小且请求不频繁时,问题不大;" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "dotnet_interactive": { "language": "csharp" }, "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "outputs": [], "source": [ "/*\n", " 每次请求都实例化:并发量大、请求频繁进会耗尽套接字端口\n", "*/\n", "{ \n", " var baseUrl = WebApiConfigManager.GetWebApiConfig().BaseUrl;\n", "\n", " using(var client = new HttpClient())\n", " {\n", " //发送请求\n", " var response = await client.GetAsync(baseUrl);\n", " response.EnsureSuccessStatusCode();\n", " }\n", "\n", " //显示句柄\n", " var displayValue = display($\"第 1 次请求,成功!\");\n", "\n", " for(int i=0;i<10;i++)\n", " {\n", " using(var client = new HttpClient())\n", " {\n", " var response = await client.GetAsync(baseUrl);\n", " response.EnsureSuccessStatusCode();\n", " displayValue.Update($\"第 {i+1} 次/ 共 10 次请求,成功!\");\n", " }\n", " }\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2、手动管理:静态类或单例" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "相比于直接new,实现了HttpClient的重用,`不推荐的`:\n", "\n", "缺点:\n", " 1. 不够灵活、优雅:特别是有多个系列的请求时;\n", " \n", "优点:\n", " 1. 复用 HttpClient\n", " 2. 实现了HttpClient的重用,减少创建和销毁的开销" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "dotnet_interactive": { "language": "csharp" }, "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "outputs": [], "source": [ "/*\n", " 静态类/属性\n", "*/\n", "\n", "public class HttpClientHelper\n", "{\n", " public readonly static HttpClient StaticClient;\n", "\n", " static HttpClientHelper()\n", " {\n", " SocketsHttpHandler handler = new SocketsHttpHandler()\n", " {\n", " PooledConnectionLifetime = TimeSpan.FromSeconds(30),\n", " };\n", "\n", " StaticClient = new HttpClient(handler);\n", "\n", " //统一设置:请求头等\n", "\n", " //统一错误处理\n", "\n", " //当然这里也可以设置Pipline,不过这里就不演示了\n", " } \n", "\n", " public static async Task GetAsync(string url)\n", " {\n", " return await StaticClient.GetAsync(url);\n", " }\n", "\n", " public static async Task GetStringAsync(string url)\n", " {\n", " var response = await StaticClient.GetAsync(url);\n", " response.EnsureSuccessStatusCode();\n", " return await response.Content.ReadAsStringAsync();\n", " }\n", "\n", " public static async Task PostAsync(string url, HttpContent content)\n", " {\n", " return await StaticClient.PostAsync(url, content);\n", " }\n", "}\n", "\n", "{ //调用静态类\n", " var baseUrl = WebApiConfigManager.GetWebApiConfig().BaseUrl;\n", " var response = await HttpClientHelper.GetAsync(baseUrl+\"/api/Config/GetApiConfig\");\n", " var content = await response.Content.ReadAsStringAsync();\n", " Console.WriteLine(content);\n", "\n", " var response2 = await HttpClientHelper.GetStringAsync(baseUrl+\"/api/Normal/GetAllAccounts\");\n", " Console.WriteLine(response2);\n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "dotnet_interactive": { "language": "csharp" }, "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "outputs": [], "source": [ "/*\n", " 单例实现1:\n", " 1. 私有构造函数,防止外部实例化\n", " 2. 使用静态只读变量存储类的实例,由.Net框架保证实例不变且线程安全\n", " 3. 密封类,拒绝继承,保证不被子类破坏\n", "*/\n", "\n", "// 使用Lazy实现单例\n", "public sealed class HttpClientSingleton \n", "{\n", " // 私有静态变量,用于存储类的实例\n", " private static readonly HttpClientSingleton instance = new HttpClientSingleton();\n", " \n", " //公共静态属性,用于获取类的实例\n", " public static HttpClientSingleton Instance\n", " {\n", " get\n", " {\n", " return instance;\n", " }\n", " }\n", "\n", " private readonly HttpClient Client;\n", "\n", " //私有构造函数,防止外部实例化\n", " private HttpClientSingleton() \n", " {\n", " SocketsHttpHandler handler = new SocketsHttpHandler()\n", " {\n", " PooledConnectionLifetime = TimeSpan.FromSeconds(30),\n", " };\n", "\n", " Client = new HttpClient(handler);\n", "\n", " //统一设置:请求头等\n", "\n", " //统一错误处理\n", "\n", " //可以使用IoC容器来管理\n", "\n", " //当然这里也可以设置Pipline,不过这里就不演示了\n", " Console.WriteLine(\"HttpClientSingleton 初始化一次\");\n", " }\n", "\n", " public async Task GetAsync(string url)\n", " {\n", " return await Client.GetAsync(url);\n", " }\n", "\n", " public async Task GetStringAsync(string url)\n", " {\n", " var response = await Client.GetAsync(url);\n", " response.EnsureSuccessStatusCode();\n", " return await response.Content.ReadAsStringAsync();\n", " }\n", "}\n", "\n", "{ //调用示例\n", "\n", " var baseUrl = WebApiConfigManager.GetWebApiConfig().BaseUrl;\n", " var response = await HttpClientSingleton.Instance.GetAsync(baseUrl+\"/api/Config/GetApiConfig\");\n", " var content = await response.Content.ReadAsStringAsync();\n", " Console.WriteLine(content);\n", "\n", " var response2 = await HttpClientSingleton.Instance.GetStringAsync(baseUrl+\"/api/Normal/GetAllAccounts\");\n", " Console.WriteLine(response2);\n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "dotnet_interactive": { "language": "csharp" }, "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "outputs": [], "source": [ "/*\n", " 单例实现2:\n", " 1. 私有构造函数,防止外部实例化\n", " 2. 使用Lazy, 延迟实例化, 由.Net 框架保证线程安全\n", " 3. 密封类,拒绝继承,保证不被子类破坏\n", "*/\n", "\n", "// 由于静态初始化器是由 .NET 运行时在后台处理的,因此它是线程安全的,不需要额外的锁定机制。\n", "public sealed class HttpClientSingleton2\n", "{\n", " private static readonly Lazy _httpClientLazy = new Lazy(() =>\n", " {\n", " SocketsHttpHandler handler = new SocketsHttpHandler()\n", " {\n", " PooledConnectionLifetime = TimeSpan.FromSeconds(30)\n", " };\n", "\n", " var client = new HttpClient(handler)\n", " {\n", " // 可以在这里配置HttpClient的实例,例如设置超时时间、基地址等\n", " //Timeout = TimeSpan.FromSeconds(30),\n", " //BaseAddress = new Uri(\"https://api.example.com/\"),\n", " };\n", "\n", "\n", " //统一设置:请求头等\n", "\n", " //统一错误处理\n", "\n", " //可以使用IoC容器来管理\n", "\n", " //当然这里也可以设置Pipline,不过这里就不演示了\n", " Console.WriteLine(\"HttpClientSingleton2 初始化一次\");\n", "\n", " return client;\n", " });\n", "\n", " public static HttpClient Instance => _httpClientLazy.Value;\n", "\n", " // 私有构造函数,防止外部实例化\n", " private HttpClientSingleton2() { } \n", "\n", " public async Task GetAsync(string url)\n", " {\n", " return await _httpClientLazy.Value.GetAsync(url);\n", " }\n", "\n", " public async Task GetStringAsync(string url)\n", " {\n", " var response = await _httpClientLazy.Value.GetAsync(url);\n", " response.EnsureSuccessStatusCode();\n", " return await response.Content.ReadAsStringAsync();\n", " }\n", "}\n", "\n", "{ //调用示例\n", "\n", " var baseUrl = WebApiConfigManager.GetWebApiConfig().BaseUrl;\n", " var response = await HttpClientSingleton2.Instance.GetAsync(baseUrl+\"/api/Config/GetApiConfig\");\n", " var content = await response.Content.ReadAsStringAsync();\n", " Console.WriteLine(content);\n", "\n", " var response2 = await HttpClientSingleton2.Instance.GetStringAsync(baseUrl+\"/api/Normal/GetAllAccounts\");\n", " Console.WriteLine(response2);\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3、手动管理:多工具类(每类请求对应一种工具类或单例类)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "把不同类别的请求分成不同的工具类,业务类直接封装成工具类的方法。类似类型化的客户端。 简单使用的话,`比较推荐`\n", "\n", "优点:\n", " 1. 复用HttpClient\n", " 2. 可以灵活的进行统一配置\n", " 3. 不同类别不同工具类,方便定制\n", " 4. 业务直接封装成工具类方法,调用方便、快捷\n", "\n", "缺点:\n", " 1. 工具类比较多,需要手动维护\n", " 2. 工具类方法比较多且和业务直接相关,需要手动维护\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "dotnet_interactive": { "language": "csharp" }, "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "outputs": [], "source": [ "// 百度服务类\n", "public sealed class BaiduService \n", "{\n", " private readonly HttpClient _httpClient;\n", " public BaiduService()\n", " {\n", " //初始化httpClient\n", " var baseHander = new SocketsHttpHandler() \n", " { \n", " MaxConnectionsPerServer = 1000 \n", " };\n", "\n", " _httpClient = new HttpClient(baseHander)\n", " {\n", " Timeout = TimeSpan.FromSeconds(10),\n", " BaseAddress = new Uri(\"http://www.baidu.com\"),\n", " };\n", " }\n", "\n", " ///// \n", " /// 获取百度首页长度\n", " /// \n", " public async Task GetIndexLengthAsync(string url)\n", " {\n", " var response = await _httpClient.GetAsync(url);\n", " response.EnsureSuccessStatusCode();\n", " var result = await response.Content.ReadAsStringAsync();\n", " return result.Length;\n", " }\n", "}\n", "//调用示例\n", "{\n", " var service = new BaiduService();\n", " var result = await service.GetIndexLengthAsync(\"/\");\n", " Console.WriteLine(result);\n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "dotnet_interactive": { "language": "csharp" }, "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "outputs": [], "source": [ "// 本机服务类\n", "// 百度服务类\n", "public sealed class LocalService \n", "{\n", " private readonly HttpClient _httpClient;\n", " public LocalService()\n", " {\n", " //初始化httpClient\n", " var baseHander = new SocketsHttpHandler() \n", " { \n", " MaxConnectionsPerServer = 1000 \n", " };\n", "\n", " _httpClient = new HttpClient(baseHander)\n", " {\n", " Timeout = TimeSpan.FromSeconds(10),\n", " BaseAddress = new Uri(WebApiConfigManager.GetWebApiConfig().BaseUrl),\n", " };\n", " }\n", "\n", " ///// \n", " /// 获取百度首页长度\n", " /// \n", " public async Task GetIndexAsync(string url)\n", " {\n", " var response = await _httpClient.GetAsync(url);\n", " response.EnsureSuccessStatusCode();\n", " var result = await response.Content.ReadAsStringAsync();\n", " return result;\n", " }\n", "}\n", "//调用示例\n", "{\n", " var service2 = new LocalService();\n", " var result = await service2.GetIndexAsync(\"/api/Simple/GetAccount\");\n", " Console.WriteLine(result);\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4、手动管理:可复原(Polly)请求" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "dotnet_interactive": { "language": "csharp" }, "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "outputs": [], "source": [ "#r \"nuget:Polly\"\n", "#r \"nuget:Microsoft.Extensions.Http.Polly\"\n", "\n", "using Polly;\n", "using Polly.Simmy;\n", "using Polly.Retry;\n", "using Polly.Extensions;\n", "{\n", " var pipleLine = new ResiliencePipelineBuilder()\n", " .AddRetry(new RetryStrategyOptions()\n", " {\n", " ShouldHandle = new PredicateBuilder().Handle(),\n", " MaxRetryAttempts = 3, // Retry up to 3 times\n", " OnRetry = args =>\n", " {\n", " // Due to how we have defined ShouldHandle, this delegate is called only if an exception occurred.\n", " // Note the ! sign (null-forgiving operator) at the end of the command.\n", " var exception = args.Outcome.Exception!; // The Exception property is nullable\n", " Console.WriteLine(\"内部重试\");\n", " return default;\n", " }\n", " })\n", " .Build();\n", "\n", " var BaseUrl = WebApiConfigManager.GetWebApiConfig().BaseUrl;\n", " HttpClient client = new HttpClient(new SocketsHttpHandler(){})\n", " {\n", " BaseAddress = new Uri(BaseUrl),\n", " };\n", "\n", " try\n", " {\n", " await pipleLine.ExecuteAsync(async (inneerToken)=>\n", " {\n", " var response = await client.GetAsync(\"api/Polly8/Retry_Exception\",inneerToken);\n", " response.EnsureSuccessStatusCode();\n", " });\n", " }\n", " catch(Exception ex)\n", " {\n", " Console.WriteLine(ex.Message);\n", " }\n", " finally\n", " {\n", "\n", " }\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5、IoC容器管理" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 直接注册IoC" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "dotnet_interactive": { "language": "csharp" }, "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "outputs": [], "source": [ "/*\n", " 注意:\n", " 1、直接IoC管理:只能一个,不太方便;\n", " 2、可使用.NET 8+ 的 KeyedService, 可以管理多个。 老版只能用服务集合,勉强能用;\n", " 3、把HttpClient 放在多个类中,分别注册使用;不过这样,不如直接使用类型化客户端;\n", "*/\n", "{ // 直接使用\n", " var services = new ServiceCollection();\n", " services.AddSingleton(new HttpClient()\n", " {\n", " //BaseAddress = new Uri(\"https://localhost:5001/\"),\n", " Timeout = TimeSpan.FromSeconds(10),\n", " });\n", "\n", " var client = services.BuildServiceProvider().GetRequiredService();\n", "\n", " var resp = await client.GetAsync(\"https://www.baidu.com\");\n", " resp.EnsureSuccessStatusCode();\n", "\n", " var content = await resp.Content.ReadAsStringAsync();\n", " Console.WriteLine(content.Length);\n", "}\n", "\n", "{ // KeyService: .Net 8+ 才支持的功能\n", " var services = new ServiceCollection();\n", " services\n", " .AddKeyedSingleton(\"HttpClientA\",new HttpClient()\n", " {\n", " BaseAddress = new Uri(\"https://www.baidu.com/\"),\n", " Timeout = TimeSpan.FromSeconds(10),\n", " })\n", " .AddKeyedSingleton(\"HttpClientB\", new HttpClient()\n", " {\n", " BaseAddress = new Uri(\"https://www.qq.com/\"),\n", " Timeout = TimeSpan.FromSeconds(2),\n", " });\n", "\n", " var clientA = services.BuildServiceProvider().GetRequiredKeyedService(\"HttpClientA\");\n", " var responseA = await clientA.GetAsync(\"/\");\n", " responseA.EnsureSuccessStatusCode();\n", " var contentA = await responseA.Content.ReadAsStringAsync();\n", " Console.WriteLine(contentA.Length);\n", "\n", " var clientB = services.BuildServiceProvider().GetRequiredKeyedService(\"HttpClientB\");\n", "\n", " var responseB = await clientB.GetAsync(\"/\");\n", " responseB.EnsureSuccessStatusCode();\n", "\n", " var contentB= await responseB.Content.ReadAsStringAsync();\n", " Console.WriteLine(contentB.Length);\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### HttpClient 多服务类" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "dotnet_interactive": { "language": "csharp" }, "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "outputs": [], "source": [ "// IoC 多个HttpClient服务类\n", "\n", "public class HttpClientServerA\n", "{\n", " public static HttpClient Client = new HttpClient()\n", " {\n", " BaseAddress = new Uri(\"https://www.baidu.com/\"),\n", " Timeout = TimeSpan.FromSeconds(2),\n", " };\n", "\n", " public int GetBaiduIndexLength()\n", " {\n", " var requestMessage = new HttpRequestMessage(HttpMethod.Get, \"/\");\n", "\n", " var response = Client.Send(requestMessage);\n", "\n", " response.EnsureSuccessStatusCode();\n", "\n", " var s = response.Content.ReadAsStream();\n", " return (int)s.Length;\n", " }\n", "}\n", "\n", "public class HttpClientServerB\n", "{\n", " public static HttpClient Client = new HttpClient()\n", " {\n", " BaseAddress = new Uri(\"https://www.qq.com/\"),\n", " Timeout = TimeSpan.FromSeconds(2),\n", " };\n", "\n", " public int GetBaiduIndexLength()\n", " {\n", " var requestMessage = new HttpRequestMessage(HttpMethod.Get, \"/\");\n", "\n", " var response = Client.Send(requestMessage);\n", "\n", " response.EnsureSuccessStatusCode();\n", "\n", " var s = response.Content.ReadAsStream();\n", " return (int)s.Length;\n", " }\n", "}\n", "\n", "{\n", " var services = new ServiceCollection();\n", " services.AddScoped();\n", " services.AddScoped();\n", "\n", " var provider = services.BuildServiceProvider();\n", "\n", " var clientA = provider.GetService();\n", " var sumA = clientA.GetBaiduIndexLength();\n", " Console.WriteLine($\"A: {sumA}\");\n", "\n", " var clientB = provider.GetService();\n", " var sumB = clientB.GetBaiduIndexLength();\n", " Console.WriteLine($\"A: {sumB}\");\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6、客户端工厂管理:IHttpClientFactory(需要结合IoC) `强力推荐`" ] }, { "cell_type": "markdown", "metadata": { "vscode": { "languageId": "polyglot-notebook" } }, "source": [ "使用 IHttpClientFactory 创建和管理 `短期HttpClient` 是官方强力推荐的方式。特别是使用IoC或是 ASP.NET中后台调用其它接口的情况。\n", "\n", "IHttpClientFactory 综合使用了 HttpClient的多种特性:HttpClient的生命周期、HttpClient的配置、HttpClient的拦截器、HttpClient的缓存、HttpClient的依赖注入、Polly等等。\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "dotnet_interactive": { "language": "csharp" }, "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "outputs": [], "source": [ "//所有HttpClient配置\n", "{\n", " var services = new ServiceCollection();\n", " //添加一个委托,用于配置所有HttpClient实例。只执行一次。\n", " services.ConfigureHttpClientDefaults(builder => \n", " {\n", " //builder.UseSocketsHttpHandler();\n", " //builder.SetHandlerLifetime(TimeSpan.FromMinutes(5));\n", " \n", " builder.ConfigureHttpClient(hc =>\n", " {\n", " hc.BaseAddress = new Uri(webApiBaseUrl);\n", " });\n", " });\n", "\n", " var factory = services.BuildServiceProvider().GetRequiredService();\n", "\n", " var client = factory.CreateClient();\n", " var response = await client.GetAsync(\"/api/hello/ping\");\n", " var data = await response.Content.ReadAsStringAsync();\n", " Console.WriteLine(data);\n", "}\n", "\n", "/*\n", " 默认 HttpClient\n", " 1、配置默认HttpClient时,AddHttpClient(Action)的Action不会被执行,ConfigureHttpClient()也不执行。原因暂时不明\n", " 2、想配置默认HttpClient,可以使用ConfigureHttpClientDefaults方法,但此方法针对所有HttpClient实例(包括命名HttpClient)\n", "*/\n", "{\n", " var services = new ServiceCollection();\n", "\n", " //默认HttpClient\n", " services.AddHttpClient();\n", "\n", " //这种形式,不生效\n", " // services.AddHttpClient((provider, client) => \n", " // {\n", " // client.BaseAddress = new Uri(webApiBaseUrl);\n", " // });\n", "\n", " //这种形式,ConfigureHttpClient 也不生效\n", " // services.AddHttpClient()\n", " // .ConfigureHttpClient(config=>\n", " // {\n", " // config.BaseAddress = new Uri(webApiBaseUrl);\n", " // });\n", "\n", " var provider = services.BuildServiceProvider();\n", "\n", " var factory = provider.GetRequiredService();\n", "\n", " var response = await factory.CreateClient().GetAsync(webApiBaseUrl+\"/api/hello/index\");\n", " var content = await response.Content.ReadAsStringAsync();\n", " \n", " Console.WriteLine(content);\n", "}\n", "\n", "//命名 HttpClient\n", "{\n", " var clientA =\"httpClientA\";\n", " var clientB =\"httpClientB\";\n", "\n", " var services = new ServiceCollection();\n", " services.AddHttpClient(clientA, (provider, client) => \n", " {\n", " client.BaseAddress = new Uri(webApiBaseUrl);\n", " });\n", " services.AddHttpClient(clientB, (provider, client) => \n", " {\n", " client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;\n", " client.BaseAddress = new Uri(webApiBaseUrl);\n", " })\n", " .ConfigureHttpClient(client=>\n", " {\n", " client.Timeout = TimeSpan.FromSeconds(1);\n", " client.DefaultRequestVersion = new Version(1, 1);\n", " });\n", "\n", " var factory = services.BuildServiceProvider().GetRequiredService();\n", "\n", " var responseA = await factory.CreateClient(clientA).GetAsync(\"/api/hello/ping\");\n", " var contentA = await responseA.Content.ReadAsStringAsync();\n", " Console.WriteLine(contentA);\n", "\n", " var responseB = await factory.CreateClient(clientB).PostAsync(\"/api/hello/post\",null);\n", " var contentB = await responseB.Content.ReadAsStringAsync();\n", " Console.WriteLine(contentB);\n", "}\n", "\n", "// 类型化客户端 HttpClient\n", "public class HttpClientServiceA\n", "{\n", " public HttpClient Client { get; }\n", " public HttpClientServiceA(HttpClient client)\n", " {\n", " Client = client;\n", " Console.WriteLine(\"HttpClientServiceA => 构造函数执行一次\");\n", " }\n", "\n", " public async Task GetIndexAsync()\n", " {\n", " var response = await Client.GetAsync(\"/api/hello/index\");\n", " var content = await response.Content.ReadAsStringAsync();\n", " return content;\n", " }\n", "}\n", "\n", "public class HttpClientServiceB\n", "{\n", " public HttpClient Client { get; }\n", " public HttpClientServiceB(HttpClient client)\n", " {\n", " Client = client;\n", " Console.WriteLine(\"HttpClientServiceB => 构造函数执行一次\");\n", " }\n", "\n", " public async Task PingAsync()\n", " {\n", " var response = await Client.GetAsync(\"/api/hello/Ping\");\n", " var content = await response.Content.ReadAsStringAsync();\n", " return content;\n", " }\n", "}\n", "\n", "{\n", " var services = new ServiceCollection();\n", " services.AddHttpClient(client => \n", " {\n", " client.BaseAddress = new Uri(webApiBaseUrl);\n", " Console.WriteLine(\"HttpClientServiceA => AddHttpClient 执行一次\");\n", " })\n", " .ConfigureHttpClient(client=>\n", " {\n", " client.Timeout = TimeSpan.FromSeconds(1);\n", " Console.WriteLine(\"HttpClientServiceA => ConfigureHttpClient 执行一次\");\n", " });\n", "\n", " services.AddHttpClient(client => \n", " {\n", " client.BaseAddress = new Uri(webApiBaseUrl);\n", " Console.WriteLine(\"HttpClientServiceB => AddHttpClient 执行一次\");\n", " })\n", " .ConfigureHttpClient(client=>\n", " {\n", " client.Timeout = TimeSpan.FromSeconds(2);\n", " Console.WriteLine(\"HttpClientServiceB => ConfigureHttpClient 执行一次\");\n", " });\n", "\n", " var builder = services.BuildServiceProvider();\n", "\n", " var serviceA = builder.GetRequiredService();\n", " var serviceB = builder.GetRequiredService();\n", " //每获取一次类型化客户端,都会执行一交。\n", " var serviceB2 = builder.GetRequiredService();\n", "\n", " var dataA = await serviceA.GetIndexAsync();\n", " Console.WriteLine(dataA);\n", "\n", " var dataB = await serviceB.PingAsync();\n", " Console.WriteLine(dataB);\n", "\n", " var dataB2 = await serviceB2.PingAsync();\n", " Console.WriteLine(dataB2);\n", "}\n", "\n", "// 类型化客户端2\n", "{\n", " var services = new ServiceCollection();\n", " services.AddHttpClient(client => \n", " {\n", " client.BaseAddress = new Uri(webApiBaseUrl);\n", " Console.WriteLine(\"HttpClientServiceA => AddHttpClient 执行一次\");\n", " })\n", " .AddTypedClient()\n", " .ConfigureHttpClient(client=>\n", " {\n", " client.Timeout = TimeSpan.FromSeconds(1);\n", " Console.WriteLine(\"HttpClientServiceA => ConfigureHttpClient 执行一次\");\n", " });\n", "}\n", "\n", "//todo: 生成式 HttpClient: Refit库\n", "\n", "/*\n", " IFactoryHttpClient + Polly8\n", " 1、引用库 Polly 和 Microsoft.Extensions.Http.Polly\n", "*/\n", "\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 7、综合管理:工厂 + 类型化客户端 + 请求管道 + Polly(默认使用 连接池和IoC容器)" ] } ], "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 }