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.
HttpClientStudy/Docs/1.3.0.基础使用.管理客户端.ipynb

1441 lines
48 KiB
Plaintext

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

{
"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": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"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<HttpResponseMessage> GetAsync(string url)\n",
" {\n",
" return await StaticClient.GetAsync(url);\n",
" }\n",
"\n",
" public static async Task<string> 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<HttpResponseMessage> 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<T>实现单例\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<HttpResponseMessage> GetAsync(string url)\n",
" {\n",
" return await Client.GetAsync(url);\n",
" }\n",
"\n",
" public async Task<string> 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<T>, 延迟实例化, 由.Net 框架保证线程安全\n",
" 3. 密封类,拒绝继承,保证不被子类破坏\n",
"*/\n",
"\n",
"// 由于静态初始化器是由 .NET 运行时在后台处理的,因此它是线程安全的,不需要额外的锁定机制。\n",
"public sealed class HttpClientSingleton2\n",
"{\n",
" private static readonly Lazy<HttpClient> _httpClientLazy = new Lazy<HttpClient>(() =>\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<HttpResponseMessage> GetAsync(string url)\n",
" {\n",
" return await _httpClientLazy.Value.GetAsync(url);\n",
" }\n",
"\n",
" public async Task<string> 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",
" ///// <summary>\n",
" /// 获取百度首页长度\n",
" /// </summary>\n",
" public async Task<int> 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",
" ///// <summary>\n",
" /// 获取百度首页长度\n",
" /// </summary>\n",
" public async Task<string> 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<Exception>(),\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<HttpClient>(new HttpClient()\n",
" {\n",
" //BaseAddress = new Uri(\"https://localhost:5001/\"),\n",
" Timeout = TimeSpan.FromSeconds(10),\n",
" });\n",
"\n",
" var client = services.BuildServiceProvider().GetRequiredService<HttpClient>();\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<HttpClient>(\"HttpClientA\",new HttpClient()\n",
" {\n",
" BaseAddress = new Uri(\"https://www.baidu.com/\"),\n",
" Timeout = TimeSpan.FromSeconds(10),\n",
" })\n",
" .AddKeyedSingleton<HttpClient>(\"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<HttpClient>(\"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<HttpClient>(\"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<HttpClientServerA>();\n",
" services.AddScoped<HttpClientServerB>();\n",
"\n",
" var provider = services.BuildServiceProvider();\n",
"\n",
" var clientA = provider.GetService<HttpClientServerA>();\n",
" var sumA = clientA.GetBaiduIndexLength();\n",
" Console.WriteLine($\"A: {sumA}\");\n",
"\n",
" var clientB = provider.GetService<HttpClientServerB>();\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": "markdown",
"metadata": {},
"source": [
"### 默认客户端"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"从使用推测,设计 IHttpClientFactory 时,重点应该是使用 “命名客户端” 或 “类型化客户端” 而不是默认客户端。 \n",
"\n",
"只有 AddHttpClient() 扩展方法返回 IServiceCollection其它相关扩展方法 AddHttpClient<HttpClient>())均返回 IHttpClientBuilder明显针对命名客户端。\n",
"AddHttpClient() 相当于注册了基本框架;而命名客户端中,名称为空(\"\"或string.Empty)的,相当于默认客户端。\n",
"\n",
"有一个 名为 `ConfigureHttpClientDefaults` 的 ServiceCollection 对象的扩展方法用于配置所有HttpClient实例并且只在初始化时执行一次。如果只使用一个默认客户端的话可以使用 ConfigureHttpClientDefaults 和 AddHttpClient() 配合使用,也能达到默认客户端的配置效果。 "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"//方式1默认客户端\n",
"{ \n",
" var services = new ServiceCollection();\n",
" /*\n",
" AddHttpClient() 返回 ServiceCollection可以继续添加其他客户端。\n",
" 其它方法则返回IHttpClientBuilder后结配置的扩展方法只能针对当前前端那个命名命令端。\n",
" */\n",
" services.AddHttpClient();\n",
"\n",
" var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();\n",
"\n",
" var client = factory.CreateClient();\n",
" //或者\n",
" var client2 = factory.CreateClient(\"\");\n",
" //或者 内部都是使用CreateClient(string.Empty),表示默认客户端。\n",
" var client3 = factory.CreateClient(string.Empty);\n",
"\n",
" var response = await client.GetAsync(webApiBaseUrl + \"/api/hello/index\");\n",
" response.EnsureSuccessStatusCode();\n",
" var data = await response.Content.ReadAsStringAsync();\n",
" data.Display();\n",
"}\n",
"\n",
"//方式2默认客户端 + 默认配置\n",
"{ \n",
" var services = new ServiceCollection();\n",
"\n",
" //默认客户端\n",
" services.AddHttpClient();\n",
"\n",
" //配置所有客户端\n",
" services.ConfigureHttpClientDefaults(builder => \n",
" {\n",
" //配置构建器\n",
" //builder.AddDefaultLogger();\n",
"\n",
" //配置客户端\n",
" builder.ConfigureHttpClient(c=>\n",
" {\n",
" c.BaseAddress = new Uri(webApiBaseUrl);\n",
" });\n",
" });\n",
"\n",
" var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();\n",
" var client = factory.CreateClient();\n",
" var response = await client.GetAsync(\"/api/hello/ping\");\n",
" response.EnsureSuccessStatusCode();\n",
" var data = await response.Content.ReadAsStringAsync();\n",
" data.Display();\n",
"}\n",
"\n",
"//方式3(推荐):默认客户端:直接使用名称为 string.empty 的命名客户端\n",
"{\n",
" var services = new ServiceCollection();\n",
"\n",
" //默认客户端\n",
" services\n",
" .AddHttpClient<HttpClient>(string.Empty)\n",
" //这样后续的配置,都是针对 string.empty 的客户端,可以使用全部配置功能\n",
" .ConfigureHttpClient(c=>c.BaseAddress = new Uri(webApiBaseUrl))\n",
" .AddDefaultLogger();\n",
"\n",
" var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();\n",
" var client = factory.CreateClient();\n",
" var response = await client.GetAsync(\"/api/hello/ping\");\n",
" response.EnsureSuccessStatusCode();\n",
" var data = await response.Content.ReadAsStringAsync();\n",
" data.Display();\n",
"}\n",
"\n",
"//错误用法\n",
"{\n",
" var services = new ServiceCollection();\n",
"\n",
" //默认客户端\n",
" services\n",
" //没有参数时,导致后面配置不起使用;\n",
" //参数必须为 空字符串或string.Empty后续的配置才能起使用\n",
"\n",
" .AddHttpClient<HttpClient>()\n",
" //没有参数时,导致后面配置不起使用\n",
" .ConfigureHttpClient(c=>c.BaseAddress = new Uri(webApiBaseUrl))\n",
" .AddDefaultLogger();\n",
"\n",
" var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();\n",
" var client = factory.CreateClient();\n",
"\n",
" try\n",
" {\n",
" var response = await client.GetAsync(\"/api/hello/ping\");\n",
" response.EnsureSuccessStatusCode();\n",
" var data = await response.Content.ReadAsStringAsync();\n",
" data.Display();\n",
" }\n",
" catch(InvalidOperationException ex)\n",
" {\n",
" Console.WriteLine($\"没有参数的配置AddHttpClient<HttpClient>(),因后续配置中,赋值 BaseAddress 不起使用,出现异常:{Environment.NewLine}{ex.Message}\");\n",
" }\n",
" catch(Exception ex)\n",
" {\n",
"\n",
" Console.WriteLine(ex.Message);\n",
" }\n",
" finally\n",
" {\n",
" client.Dispose();\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 默认全局配置"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"ConfigureHttpClientDefaults 扩展方法添加一个委托用于配置所有HttpClient实例。只执行一次。"
]
},
{
"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",
" //只执行一次而非每次CreateClient都会执行一次。\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",
" Console.WriteLine(\"ConfigureHttpClientDefaults 只执行一次!\");\n",
" });\n",
"\n",
" //配置命名客户端\n",
" services\n",
" .AddHttpClient<HttpClient>(\"client_a\")\n",
" .ConfigureHttpClient(hc => \n",
" {\n",
" hc.DefaultRequestHeaders.Add(\"client_a\", \"client_a\");\n",
"\n",
" //可以覆盖默认配置\n",
" //hc.BaseAddress = new Uri(\"http://www.qq.com\");\n",
"\n",
" Console.WriteLine(\"ConfigureHttpClient 每次 CreateClient 执行一次!\");\n",
" });\n",
"\n",
" \n",
" var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();\n",
"\n",
" //默认客户端\n",
" var defaultClient = factory.CreateClient();\n",
" var defaultResponse = await defaultClient.GetAsync(\"/api/hello/ping\");\n",
" var defaultData = await defaultResponse.Content.ReadAsStringAsync();\n",
" Console.WriteLine(defaultData);\n",
"\n",
" //命名客户端\n",
" var namedClient = factory.CreateClient(\"client_a\");\n",
" var namedResponse = await namedClient.GetAsync(\"/api/hello/get\");\n",
" var namedData = await namedResponse.Content.ReadAsStringAsync();\n",
" Console.WriteLine(namedData);\n",
"\n",
" _ = factory.CreateClient(\"client_a\");\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 命名客户端(推荐用法)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"命名客户端应该是官方推荐的方法。名称为空字符串或string.Empty时可以为是默认命名客户端factory.CreateClient()创建的就是这个默认客户端(或者factory.CreateClient(\"\"))。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"//命名客户端\n",
"{\n",
" var clientA =\"httpClientA\";\n",
" var clientB =\"httpClientB\";\n",
"\n",
" var services = new ServiceCollection();\n",
"\n",
" services.AddHttpClient<HttpClient>(string.Empty, (provider, client) => \n",
" {\n",
" client.BaseAddress = new Uri(webApiBaseUrl);\n",
" });\n",
"\n",
" services.AddHttpClient<HttpClient>(clientA, (provider, client) => \n",
" {\n",
" client.BaseAddress = new Uri(webApiBaseUrl);\n",
" });\n",
"\n",
" services.AddHttpClient<HttpClient>(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<IHttpClientFactory>();\n",
"\n",
" //name=string.Empty\n",
" var defaultClient = factory.CreateClient();\n",
" var defaultResponse = await defaultClient.GetAsync(\"/api/hello/ping\");\n",
" var defaultData = await defaultResponse.Content.ReadAsStringAsync();\n",
" Console.WriteLine(defaultData);\n",
"\n",
" //name=clientA\n",
" var httpClient_a = factory.CreateClient(clientA);\n",
" var responseA = await httpClient_a.GetAsync(\"/api/hello/ping\");\n",
" var dataA = await responseA.Content.ReadAsStringAsync();\n",
" dataA.Display();\n",
"\n",
" //name=clientB\n",
" var httpClient_B = factory.CreateClient(clientB);\n",
" var responseB = await httpClient_B.GetAsync(\"/api/hello/ping\");\n",
" var dataB = await responseB.Content.ReadAsStringAsync();\n",
" dataB.Display();\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 类型化客户端 (推荐)"
]
},
{
"cell_type": "markdown",
"metadata": {
"vscode": {
"languageId": "polyglot-notebook"
}
},
"source": [
"类型化的客户端,两种基本使用方式:\n",
"1、可以单独使用(直接IoC容器)\n",
"2、与IFactoryHttpClient配合使用(依赖注入),目的是:从统一的工厂配置中获取客户端,作为 HttpClient 类型的实参,传给类型化客户端的构造函数。\n",
" 换名话说从工厂获取HttpClient实例设置为 类型化客户端类的 HttpClient在其内部使用。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"// 类型化客户端 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<string> 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<string> 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",
"// 方式1(不推荐)类型化客户端直接注入IoC,并从中获取实例。优点是范围可以自己选择。\n",
"{\n",
" Console.WriteLine(\"方式1 -------------------------------------------------------------------\");\n",
" var services = new ServiceCollection();\n",
" services.AddSingleton<HttpClientServiceA>(b => \n",
" { \n",
" return new HttpClientServiceA(new HttpClient(){BaseAddress = new Uri(webApiBaseUrl)});\n",
" });\n",
" services.AddScoped<HttpClientServiceB>(b=> \n",
" {\n",
" return new HttpClientServiceB(new HttpClient(){BaseAddress = new Uri(webApiBaseUrl)});\n",
" });\n",
"\n",
" var builder = services.BuildServiceProvider();\n",
" var serverA = builder.GetRequiredService<HttpClientServiceA>();\n",
" var serverB = builder.GetRequiredService<HttpClientServiceB>();\n",
"\n",
" var dataA = await serverA.GetIndexAsync();\n",
" Console.WriteLine(dataA);\n",
"\n",
" var dataB = await serverB.PingAsync();\n",
" Console.WriteLine(dataB);\n",
"\n",
" Console.WriteLine(\"========================================================================\");\n",
"}\n",
"\n",
"// 方式2类型化客户端AddHttpClient<>() 设置\n",
"{\n",
" Console.WriteLine(\"方式2 -------------------------------------------------------------------\");\n",
" var services = new ServiceCollection();\n",
" services\n",
" .AddHttpClient<HttpClientServiceA>()\n",
" .ConfigureHttpClient(client=>\n",
" {\n",
" client.BaseAddress = new Uri(webApiBaseUrl);\n",
" });\n",
"\n",
" services\n",
" .AddHttpClient<HttpClientServiceB>()\n",
" .ConfigureHttpClient(client=>\n",
" {\n",
" client.BaseAddress = new Uri(webApiBaseUrl);\n",
" });\n",
"\n",
" var builder = services.BuildServiceProvider();\n",
" var serverA = builder.GetRequiredService<HttpClientServiceA>();\n",
" var serverB = builder.GetRequiredService<HttpClientServiceB>();\n",
"\n",
" var dataA = await serverA.GetIndexAsync();\n",
" Console.WriteLine(dataA);\n",
"\n",
" var dataB = await serverB.PingAsync();\n",
" Console.WriteLine(dataB);\n",
"\n",
" Console.WriteLine(\"========================================================================\");\n",
"}\n",
"\n",
"// 方式3类型化客户端结合工厂由工厂从统一配置中提供类型化客户端中使用的HttpClient实例。\n",
"{\n",
" Console.WriteLine(\"方式3 -------------------------------------------------------------------\");\n",
" var services = new ServiceCollection();\n",
" services.AddHttpClient<HttpClientServiceA>(client => \n",
" {\n",
" client.BaseAddress = new Uri(webApiBaseUrl);\n",
" Console.WriteLine(\"HttpClientServiceA => AddHttpClient 执行一次\");\n",
" })\n",
" .AddTypedClient<HttpClientServiceA>()\n",
" .ConfigureHttpClient(client=>\n",
" {\n",
" client.Timeout = TimeSpan.FromSeconds(1);\n",
" Console.WriteLine(\"HttpClientServiceA => ConfigureHttpClient 执行一次\");\n",
" });\n",
"\n",
" services.AddHttpClient<HttpClientServiceB>(client => \n",
" {\n",
" client.BaseAddress = new Uri(webApiBaseUrl);\n",
" Console.WriteLine(\"HttpClientServiceB => AddHttpClient 执行一次\");\n",
" })\n",
" .AddTypedClient<HttpClientServiceB>()\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<HttpClientServiceA>();\n",
" var serviceB = builder.GetRequiredService<HttpClientServiceB>();\n",
" //每获取一次类型化客户端,都会执行一交。\n",
" var serviceB2 = builder.GetRequiredService<HttpClientServiceB>();\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",
" Console.WriteLine(\"========================================================================\");\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 管道配置"
]
},
{
"cell_type": "code",
"execution_count": 150,
"metadata": {
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"LoggerDelegatingHandler -> SendAsync -> Before\n",
"LoggerDelegatingHandler -> SendAsync -> After\n",
"Pong\n"
]
}
],
"source": [
"//管道配置\n",
"\n",
"//日志中间件(管道类)\n",
"public class LoggerDelegatingHandler : DelegatingHandler\n",
"{\n",
" protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)\n",
" {\n",
" Console.WriteLine(\"LoggerDelegatingHandler -> Send -> Before\");\n",
"\n",
" HttpResponseMessage response = base.Send(request, cancellationToken);\n",
"\n",
" Console.WriteLine(\"LoggerDelegatingHandler -> Send -> After\");\n",
"\n",
" return response;\n",
" }\n",
"\n",
" protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n",
" {\n",
" Console.WriteLine(\"LoggerDelegatingHandler -> SendAsync -> Before\");\n",
"\n",
" HttpResponseMessage response = await base.SendAsync(request, cancellationToken);\n",
"\n",
" Console.WriteLine(\"LoggerDelegatingHandler -> SendAsync -> After\");\n",
"\n",
" return response;\n",
" }\n",
"}\n",
"\n",
"//使用日志中间件\n",
"{\n",
" var services = new ServiceCollection();\n",
"\n",
" //先注册\n",
" services.AddTransient<LoggerDelegatingHandler>();\n",
"\n",
" services.AddHttpClient<HttpClient>(string.Empty).ConfigureHttpClient(client =>\n",
" {\n",
" client.BaseAddress = new Uri(webApiBaseUrl);\n",
" })\n",
" //配置SocketsHttpHandler\n",
" .UseSocketsHttpHandler((handler,provider) =>\n",
" {\n",
" handler.ConnectTimeout = TimeSpan.FromSeconds(10);\n",
" handler.MaxConnectionsPerServer = 100;\n",
" handler.UseProxy = false;\n",
" handler.UseCookies = true;\n",
" handler.EnableMultipleHttp2Connections = true;\n",
" handler.SslOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;\n",
" })\n",
" //使用前先在AddTransient范围注册\n",
" //.AddHttpMessageHandler<LoggerDelegatingHandler>()\n",
" .AddHttpMessageHandler<LoggerDelegatingHandler>();\n",
"\n",
" var factory = services.BuildServiceProvider().GetService<IHttpClientFactory>();\n",
"\n",
" var client = factory.CreateClient();\n",
"\n",
" var response = await client.GetAsync(\"/api/hello/ping\");\n",
" response.EnsureSuccessStatusCode();\n",
"\n",
" var responseString = await response.Content.ReadAsStringAsync();\n",
" Console.WriteLine(responseString);\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 日志配置"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 7 工厂 + Polly V8"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 8、综合管理工厂 + 类型化客户端 请求管道 + 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
}