diff --git a/Docs/0.目录结构.ipynb b/Docs/0.目录结构.ipynb index 6c4f041..a9d4c40 100644 --- a/Docs/0.目录结构.ipynb +++ b/Docs/0.目录结构.ipynb @@ -84,14 +84,11 @@ "id": "746fd7e5", "metadata": {}, "source": [ - "+ [概述](./1.4.0.高级使用.概述.ipynb)\n", - "+ [初始化](./1.4.1.高级使用.初始化.ipynb)\n", - "+ [连接池](./1.4.2.高级使用.连接池.ipynb)\n", - "+ [重复使用](./1.4.3.高级使用.重复使用.ipynb)\n", - "+ [使用管道](./1.4.4.高级使用.使用管道.ipynb)\n", - "+ [类型化客户端](./1.4.5.高级使用.类型化客户端.ipynb)\n", - "+ [工厂模式](./1.4.6.高级使用.工厂模式.ipynb)\n", - "+ [Polly库](./1.4.7.高级使用.Polly.ipynb)" + "+ [概述](./1.4.1.高级使用.概述.ipynb)\n", + "+ [使用管道](./1.4.2.高级使用.使用管道.ipynb)\n", + "+ [类型化客户端](./1.4.3.高级使用.类型化客户端.ipynb)\n", + "+ [工厂模式](./1.4.4.高级使用.工厂模式.ipynb)\n", + "+ [Polly库](./1.4.5.高级使用.Polly.ipynb)" ] }, { diff --git a/Docs/1.1.概述.ipynb b/Docs/1.1.概述.ipynb index e7fe202..6d2a3e7 100644 --- a/Docs/1.1.概述.ipynb +++ b/Docs/1.1.概述.ipynb @@ -89,26 +89,70 @@ } }, "source": [ - "## HttpClient架构图\n", - "\n", - "### .NetFramework 4及4.5 架构\n", + "## HttpClient架构图" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### .NetFramework 4及4.5 架构" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ "HttpClient最初是作为NuGet包提供的,该包可以选择包含在.NET Framework 4.0项目中。在.NET Framework 4.5中,它作为BCL(基本类库)的一部分在框中提供。它建立在预先存在的HttpWebRequest实现之上。在.NET Framework中,ServicePoint API可用于控制和管理HTTP连接,包括通过为端点配置ConnectionLeaseTimeout来设置连接寿命。\n", "\n", - "![.NetFramework](./Assets/架构.001.png)\n", - "\n", - "### .NET Core 1.0及1.1 架构\n", + "![.NetFramework](./Assets/架构.001.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### .NET Core 1.0及1.1 架构" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ ".NET Core 1.0最初于2016年6月发布。与.NET Framework中可用的版本相比,此第一个版本的API接口要小得多,主要用于构建ASP.NET Core Web应用程序。由于.NET Core 1.0是HttpClient,因此提供了API。但是,不包括用于HttpWebRequest和ServicePoint的API。.NET Core 1.0中的HttpClient直接建立在使用非托管代码的OS平台API之上,Windows API使用WinHTTP,Linux和Mac使用LibCurl。\n", "\n", "值得注意的是:到2016年8月,很快就注意到,重新使用HttpClient实例以防止套接字耗尽的建议有一个相当麻烦的副作用:DNS\n", "\n", - "![.NetFramework](./Assets/架构.002.png)\n", - "\n", - "### .NET Core 2.0 架构\n", + "![.NetFramework](./Assets/架构.002.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### .NET Core 2.0 架构" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ "在.NET Core 2.0中,添加了HttpWebRequest以支持.NET Standard 2.0。它位于HttpClient实现的顶层,这与.NET Framework 4.5+中的工作原理相反。还添加了ServicePoint,尽管它的许多API接口要么要么会抛出未实现的异常,要么根本就没有实现。\n", "\n", - "![.NetFramework](./Assets/架构.003.png)\n", - "\n", - "### .NET CORE 2.1及后续 架构\n", + "![.NetFramework](./Assets/架构.003.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### .NET CORE 2.1及后续 架构" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ "这种有问题的行为导致团队不同团队进行了两项工作。\n", "\n", "ASP.NET团队开始研究**Microsoft.Extensions.Http包,该包的主要功能是IHttpClientFactory**。这个针对HttpClient实例自用的工厂还包括基础HttpMessageHandler链的生命周期管理。如果您想了解有关此功能的更多信息,可以查看我的系列博客文章,我将在此介绍。\n", @@ -117,8 +161,13 @@ "\n", "在同一时间范围内,.NET团队正在研究自己的解决方案。该团队也在.NET Core 2.1中发布,在HttpClient的处理程序链的核心引入了一个新的**SocketsHttpHandler**。该处理程序直接建立在Socket API之上,并在托管代码中实现HTTP。这项工作的一部分包括连接池系统以及为这些连接设置最大生存期的能力。\n", "\n", - "![.NetFramework](./Assets/架构.004.png)\n", - "\n", + "![.NetFramework](./Assets/架构.004.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ "说明:虽然默认情况下从.NET Core 2.1启用了SocketsHttpHandler,但实现仅限于HTTP / 1.1通信。那些需要HTTP / 2的用户必须禁用该功能并使用较旧的处理程序链,该处理程序链像以前一样依赖非托管代码,并且不包括连接池。\n", "\n", "幸运的是,.NET Core 3.0中已消除了此限制,并且现在提供了HTTP/2支持。这应该使用基于适合所有对象的SocketsHttpHandler链的HttpClient。\n", diff --git a/Docs/1.4.3.高级使用.重复使用.ipynb b/Docs/1.4.1.高级使用.概述.ipynb similarity index 69% rename from Docs/1.4.3.高级使用.重复使用.ipynb rename to Docs/1.4.1.高级使用.概述.ipynb index 7656438..8b0a685 100644 --- a/Docs/1.4.3.高级使用.重复使用.ipynb +++ b/Docs/1.4.1.高级使用.概述.ipynb @@ -11,7 +11,15 @@ } }, "source": [ - "# HttpClient 使用原则" + "HttpClient高级特性-概述\n", + "======================" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 常见问题" ] }, { @@ -28,7 +36,21 @@ } }, "source": [ - "## 1、复用" + "## 使用原则" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 初始化" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 连接池" ] } ], diff --git a/Docs/1.4.2.高级使用.使用管道.ipynb b/Docs/1.4.2.高级使用.使用管道.ipynb new file mode 100644 index 0000000..b5e305e --- /dev/null +++ b/Docs/1.4.2.高级使用.使用管道.ipynb @@ -0,0 +1,710 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "dotnet_interactive": { + "language": "csharp" + }, + "polyglot_notebook": { + "kernelName": "csharp" + } + }, + "source": [ + "# HttpClient 管道黑科技,让你的请求效率飞起来!🚀" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Pipeline(管道)是HttpClient网络请求的`隐形加速器`。\n", + "\n", + "HttpClient 采用了与 ASP.NET Core 管道机制相同的设计,通过组合 `HttpMessageHandler` 和 `中间件模式` 形成请求/响应链来实现。允许你在请求和响应之间插入多个处理步骤,这些步骤可以按顺序执行,类似于管道。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- 提高性能\n", + " + 异步流水线式处理:利用async/await串联管道,实现非阻塞的请求-响应流水线,提升吞吐量;\n", + " + 资源高效使用:结合IHttpClientFactory自动管理连接池,高效使用HttpClient实例;\n", + "\n", + "- 优化程序设计\n", + " + 职责分离:将请求处理拆分为多个独立管道(如认证、日志、重试),每个管道专注单一功能;\n", + " + 可插拔性‌:通过增减管道,动态调整处理流程(如临时添加请求加密步骤),无需修改核心逻辑;\n", + " + 统一扩展点:为整个请求流程,形成统一的扩展点;结合AOP模式,更容易实现统一日志、权限、拦截、自定义流程等扩展功能。\n", + "\n", + "- 增加可维护\n", + " + 基于管道,形成模块化、可扩展化程序设计,提高可维护性;\n", + " + 调试友好:每个管道可单独测试、跟踪;" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 初始化" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "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
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "配置文件根目录:c:\\Users\\ruyu\\Desktop\\HttpClientStudy\\Docs\\Publish\\HttpClientStudy.Core\n", + "配置文件根目录:c:\\Users\\ruyu\\Desktop\\HttpClientStudy\\Docs\\Publish\\HttpClientStudy.Core\n", + "启动WebApi项目...\n", + "程序[c:\\Users\\ruyu\\Desktop\\HttpClientStudy\\Docs\\Publish\\HttpClientStudy.WebApp\\HttpClientStudy.WebApp.exe]已在新的命令行窗口执行。如果未出现新命令行窗口,可能是程序错误造成窗口闪现!\n", + "已启动WebApi项目,保持窗口打开状态!\n", + "初始化完成!\n" + ] + } + ], + "source": [ + "#!import \"./Ini.ipynb\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "dotnet_interactive": { + "language": "csharp" + }, + "polyglot_notebook": { + "kernelName": "csharp" + }, + "vscode": { + "languageId": "polyglot-notebook" + } + }, + "source": [ + "## 1、创建自定义 HttpMessageHandler" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "polyglot_notebook": { + "kernelName": "csharp" + }, + "vscode": { + "languageId": "polyglot-notebook" + } + }, + "outputs": [], + "source": [ + "/// \n", + "/// 日志管道(中间件)\n", + "/// \n", + "public class LoggingHandler : DelegatingHandler\n", + "{\n", + " public LoggingHandler()\n", + " {\n", + " //防止成为最后一个管道时异常\n", + " this.InnerHandler = new SocketsHttpHandler();\n", + " }\n", + " \n", + " protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)\n", + " {\n", + " Console.WriteLine(\"日志管道: LoggingHandler -> Send -> Before\");\n", + "\n", + " HttpResponseMessage response = base.Send(request, cancellationToken);\n", + " response.EnsureSuccessStatusCode();\n", + "\n", + " Console.WriteLine(\"日志管道: LoggingHandler -> Send -> After\");\n", + "\n", + " return response;\n", + " }\n", + "\n", + " protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n", + " {\n", + " // 记录请求信息\n", + " Console.WriteLine($\"日志管道: LoggingHandler -> Send -> Before Request: {request.Method} {request.RequestUri}\");\n", + " // 调用管道中的下一个处理器,并获取响应\n", + " var response = await base.SendAsync(request, cancellationToken);\n", + " // 记录响应信息\n", + " Console.WriteLine($\"日志管道: LoggingHandler -> Send -> After Response: {response.StatusCode}\");\n", + " return response;\n", + " }\n", + "}\n", + "\n", + "/// \n", + "/// 令牌验证管道(中间件)\n", + "/// \n", + "public class TokenDelegatingHandler : DelegatingHandler \n", + "{\n", + " public TokenDelegatingHandler()\n", + " {\n", + " //防止成为最后一个管道时异常\n", + " this.InnerHandler = new SocketsHttpHandler();\n", + " }\n", + "\n", + " protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)\n", + " {\n", + " Console.WriteLine(\"令牌验证管道: TokenDelegatingHandler -> Send -> Added Token\");\n", + "\n", + " if (!request.Headers.Contains(Microsoft.Net.Http.Headers.HeaderNames.Authorization)) \n", + " {\n", + " Console.WriteLine(\"没有 Token, TokenDelegatingHandler 添加之\");\n", + " request.Headers.Add(Microsoft.Net.Http.Headers.HeaderNames.Authorization, \"Bearer \" + \"a.b.c\");\n", + " }\n", + " else\n", + " {\n", + " Console.WriteLine($\"已有Token, {request.Headers.Authorization}\");\n", + " }\n", + "\n", + " HttpResponseMessage response = base.Send(request, cancellationToken);\n", + "\n", + " Console.WriteLine(\"令牌验证管道: TokenDelegatingHandler -> Send -> After\");\n", + "\n", + " return response;\n", + " }\n", + "\n", + " protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n", + " {\n", + " Console.WriteLine(\"令牌验证管道: TokenDelegatingHandler -> SendAsync -> Before\");\n", + "\n", + " HttpResponseMessage response = await base.SendAsync(request, cancellationToken);\n", + "\n", + " Console.WriteLine(\"令牌验证管道: TokenDelegatingHandler -> SendAsync -> After\");\n", + "\n", + " return response;\n", + " }\n", + "}\n", + "\n", + "///\n", + "/// 异常处理管道(中间件)\n", + "/// \n", + "public class ExceptionDelegatingHandler : DelegatingHandler\n", + "{\n", + " public ExceptionDelegatingHandler()\n", + " {\n", + " //防止成为最后一个管道时异常\n", + " this.InnerHandler = new SocketsHttpHandler();\n", + " }\n", + "\n", + " protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)\n", + " {\n", + " Console.WriteLine(\"异常处理管道: ExceptionDelegatingHandler -> Send -> Before\");\n", + "\n", + " HttpResponseMessage response;\n", + " try \n", + " {\n", + " response = base.Send(request, cancellationToken);\n", + " response.EnsureSuccessStatusCode();\n", + " }\n", + " catch(Exception ex)\n", + " {\n", + " Console.WriteLine($\"异常管道中间件,监控到异常:{ex.Message}\");\n", + "\n", + " response = new HttpResponseMessage(HttpStatusCode.ExpectationFailed);\n", + " }\n", + " finally\n", + " {\n", + " //清理业务\n", + " }\n", + " \n", + " Console.WriteLine(\"异常处理管道: ExceptionDelegatingHandler -> Send -> After\");\n", + "\n", + " return response;\n", + " }\n", + "\n", + " protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n", + " {\n", + " Console.WriteLine(\"异常处理管道: ExceptionDelegatingHandler -> SendAsync -> Before\");\n", + "\n", + " HttpResponseMessage response;\n", + " try \n", + " {\n", + " response = response = await base.SendAsync(request, cancellationToken);\n", + " response.EnsureSuccessStatusCode();\n", + " }\n", + " catch(Exception ex)\n", + " {\n", + " Console.WriteLine($\"异常管道中间件,监控到异常:{ex.Message}\");\n", + "\n", + " response = new HttpResponseMessage(HttpStatusCode.ExpectationFailed);\n", + " }\n", + " finally\n", + " {\n", + " //清理业务\n", + " }\n", + "\n", + " Console.WriteLine(\"异常处理管道: ExceptionDelegatingHandler -> SendAsync -> After\");\n", + "\n", + " return response;\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2、组合多个 HttpMessageHandler 形成请求/响应管道(处理链)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "组合管道,就是利用 DelegatingHandler 类的 InnerHandler 属性,指定本管道的下一个管道(中间件),将多个管道链接起来,形成一个`请求/响应`处理链。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "+ 默认管道" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "polyglot_notebook": { + "kernelName": "csharp" + }, + "vscode": { + "languageId": "polyglot-notebook" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "响应内容:Pong\r\n" + ] + } + ], + "source": [ + "{ //默认配置\n", + " var httpClient = new HttpClient()\n", + " {\n", + " BaseAddress = webApiBaseUri\n", + " };\n", + "\n", + " //发送请求,从日志看管道执行顺序\n", + " var response = await httpClient.GetAsync(\"/api/Hello/Ping\");\n", + "\n", + " var content = await response.Content.ReadAsStringAsync();\n", + "\n", + " Console.WriteLine($\"响应内容:{content}\");\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "+ 手动组合" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "polyglot_notebook": { + "kernelName": "csharp" + }, + "vscode": { + "languageId": "polyglot-notebook" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "令牌验证管道: TokenDelegatingHandler -> SendAsync -> Before\n", + "异常处理管道: ExceptionDelegatingHandler -> SendAsync -> Before\n", + "日志管道: LoggingHandler -> Send -> Before Request: GET http://127.0.0.1:5189/api/Config/GetApiConfig\n", + "日志管道: LoggingHandler -> Send -> After Response: OK\n", + "异常处理管道: ExceptionDelegatingHandler -> SendAsync -> After\n", + "令牌验证管道: TokenDelegatingHandler -> SendAsync -> After\n", + "响应内容:{\"data\":{\"host\":\"localhost\",\"port\":5189,\"scheme\":\"http\",\"pathBase\":\"\",\"baseUrl\":\"http://localhost:5189\",\"webAppMutexName\":\"HttpClientStudy.WebApp\"},\"code\":1,\"message\":\"成功\"}\n" + ] + } + ], + "source": [ + "{ //组装管道\n", + "\n", + " /*\n", + " 1. 最后一个管道必须是SocketsHttpHandler 或者 保证最后的管道的InnerHandler有默认值\n", + " 2. 倒序组装,正序执行:HttpClient构造函数,传入的HttpMessageHandler是第一个执行的,其InnerHandler是下一个执行的,依次类推\n", + " */\n", + "\n", + " var lastHandler = new SocketsHttpHandler()\n", + " {\n", + " UseCookies = false,\n", + " UseProxy = false,\n", + " };\n", + "\n", + " var logPipeline = new LoggingHandler()\n", + " {\n", + " InnerHandler = lastHandler\n", + " };\n", + " \n", + " var exceptionHandler = new ExceptionDelegatingHandler()\n", + " {\n", + " InnerHandler = logPipeline\n", + " };\n", + "\n", + " var tokenHandler = new TokenDelegatingHandler()\n", + " {\n", + " InnerHandler = exceptionHandler\n", + " };\n", + "\n", + " var httpClient = new HttpClient(tokenHandler)\n", + " {\n", + " BaseAddress = webApiBaseUri\n", + " };\n", + "\n", + " //发送请求,从日志看管道执行顺序\n", + " var response = await httpClient.GetAsync(\"/api/Config/GetApiConfig\");\n", + "\n", + " var content = await response.Content.ReadAsStringAsync();\n", + "\n", + " Console.WriteLine($\"响应内容:{content}\");\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "+ 动态组合" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "整个管道链的组装,是非常灵活的,动态组装也不是难事!" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "polyglot_notebook": { + "kernelName": "csharp" + }, + "vscode": { + "languageId": "polyglot-notebook" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "http://127.0.0.1:5189" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "令牌验证管道: TokenDelegatingHandler -> SendAsync -> Before\n", + "日志管道: LoggingHandler -> Send -> Before Request: GET http://127.0.0.1:5189/api/Config/GetApiConfig\n", + "异常处理管道: ExceptionDelegatingHandler -> SendAsync -> Before\n", + "异常处理管道: ExceptionDelegatingHandler -> SendAsync -> After\n", + "日志管道: LoggingHandler -> Send -> After Response: OK\n", + "令牌验证管道: TokenDelegatingHandler -> SendAsync -> After\n", + "响应内容:{\"data\":{\"host\":\"localhost\",\"port\":5189,\"scheme\":\"http\",\"pathBase\":\"\",\"baseUrl\":\"http://localhost:5189\",\"webAppMutexName\":\"HttpClientStudy.WebApp\"},\"code\":1,\"message\":\"成功\"}\n" + ] + } + ], + "source": [ + "/// \n", + "/// 管道构建器\n", + "/// \n", + "public class HttpMessageHandlerBuilder\n", + "{\n", + " public List Handlers { get; set; }\n", + "\n", + " public HttpMessageHandlerBuilder()\n", + " {\n", + " this.Handlers = new List();\n", + " }\n", + "\n", + " public HttpMessageHandlerBuilder(List handlers):base()\n", + " {\n", + " if(handlers != null)\n", + " {\n", + " this.Handlers = handlers;\n", + " }\n", + " }\n", + "\n", + " public void AddHttpMessageHandler(DelegatingHandler handler)\n", + " {\n", + " Handlers.Add(handler);\n", + " }\n", + "\n", + " public void RemoveHttpMessageHandler(DelegatingHandler handler)\n", + " {\n", + " Handlers.Remove(handler);\n", + " }\n", + "\n", + " public List FindHttpMessageHandler(DelegatingHandler handler)\n", + " {\n", + " return Handlers.Where(h => h.GetType() == handler.GetType()).ToList();\n", + " }\n", + "\n", + " /// \n", + " /// 根据管道配置:创建一个新HttpClient实例\n", + " /// \n", + " public HttpClient Build()\n", + " {\n", + " if(this.Handlers ==null || this.Handlers.Count == 0)\n", + " {\n", + " return new HttpClient();\n", + " }\n", + "\n", + " //默认最终处理器\n", + " HttpMessageHandler defaultLastHandler = new SocketsHttpHandler()\n", + " {\n", + " //自定义配置\n", + " PooledConnectionLifetime = TimeSpan.FromSeconds(120),\n", + " };\n", + "\n", + " //倒序组装:完成后执行顺序与注册顺序一致\n", + " //拷贝份,省得多次调用有问题\n", + "\n", + " HttpMessageHandler currentHandler = new SocketsHttpHandler()\n", + " {\n", + " //自定义配置\n", + " PooledConnectionLifetime = TimeSpan.FromSeconds(120),\n", + " };\n", + "\n", + " var builderHandlers = this.Handlers.ToList();\n", + " builderHandlers.Reverse();\n", + "\n", + " if(builderHandlers[0].InnerHandler != null)\n", + " {\n", + " currentHandler = builderHandlers[0].InnerHandler;\n", + " }\n", + "\n", + " for (int i = 0; i < builderHandlers.Count; i++)\n", + " {\n", + " builderHandlers[i].InnerHandler = currentHandler;\n", + " currentHandler = builderHandlers[i];\n", + " }\n", + "\n", + " // 利用管道创建HttpClient\n", + " var httpClient = new HttpClient(currentHandler);\n", + "\n", + " return httpClient;\n", + " }\n", + "}\n", + "\n", + "//构建管道\n", + "List handlers = new List()\n", + "{\n", + " new TokenDelegatingHandler(),\n", + "};\n", + "\n", + "//添加\n", + "handlers.Add(new LoggingHandler());\n", + "handlers.Add(new ExceptionDelegatingHandler());\n", + "\n", + "var handlerBuilder = new HttpMessageHandlerBuilder(handlers);\n", + "\n", + "//第一次:发送请求,从日志看管道执行顺序\n", + "var httpClient = handlerBuilder.Build();\n", + "httpClient.BaseAddress = webApiBaseUri;\n", + "webApiBaseUrl.Display();\n", + "var response = await httpClient.GetAsync(\"/api/Config/GetApiConfig\");\n", + "var content = await response.Content.ReadAsStringAsync();\n", + "\n", + "Console.WriteLine($\"响应内容:{content}\");\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3、发送请求" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "和一般的请求,没什么区别。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4、管道执行顺序" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "+ 请求从最外层的 DelegatingHandler 开始,逐层进入内部处理器。\n", + "+ 响应则从最内层返回,经过每一层的 SendAsync 方法后返回给调用者。\n", + "\n", + "执行下面示例,查看日志输出:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "polyglot_notebook": { + "kernelName": "csharp" + }, + "vscode": { + "languageId": "polyglot-notebook" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "令牌验证管道: TokenDelegatingHandler -> SendAsync -> Before\n", + "异常处理管道: ExceptionDelegatingHandler -> SendAsync -> Before\n", + "日志管道: LoggingHandler -> Send -> Before Request: GET http://127.0.0.1:5189/api/Hello/Ping\n", + "日志管道: LoggingHandler -> Send -> After Response: OK\n", + "异常处理管道: ExceptionDelegatingHandler -> SendAsync -> After\n", + "令牌验证管道: TokenDelegatingHandler -> SendAsync -> After\n", + "响应内容:Pong\n" + ] + } + ], + "source": [ + "{ \n", + " //组装管道\n", + " //最后一个管道,必须是SocketsHttpHandler,或者默认的是SocketsHttpHandler\n", + " var lastHandler = new SocketsHttpHandler()\n", + " {\n", + " UseCookies = false,\n", + " UseProxy = false,\n", + " };\n", + "\n", + " var logPipeline = new LoggingHandler()\n", + " {\n", + " InnerHandler = lastHandler\n", + " };\n", + " \n", + " var exceptionHandler = new ExceptionDelegatingHandler()\n", + " {\n", + " InnerHandler = logPipeline\n", + " };\n", + "\n", + " var tokenHandler = new TokenDelegatingHandler()\n", + " {\n", + " InnerHandler = exceptionHandler\n", + " };\n", + "\n", + " var httpClient = new HttpClient(tokenHandler)\n", + " {\n", + " BaseAddress = webApiBaseUri\n", + " };\n", + "\n", + " //发送请求,从日志看管道执行顺序\n", + " var response = await httpClient.GetAsync(\"/api/Hello/Ping\");\n", + "\n", + " var content = await response.Content.ReadAsStringAsync();\n", + "\n", + " Console.WriteLine($\"响应内容:{content}\");\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5、使用 IHttpClientFactory 构建更复杂的管道(推荐)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 小结" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "| 特性 | 描述 |\n", + "|---------------------|----------------------------------------------------------------------|\n", + "| HttpMessageHandler | 是 HttpClient 的核心处理单元:ml-citation{ref=\"1\" data=\"citationList\"} |\n", + "| DelegatingHandler | 可用于构建自定义中间件逻辑:ml-citation{ref=\"1,5\" data=\"citationList\"} |\n", + "| 管道顺序 | 请求按顺序进入,响应逆序返回:ml-citation{ref=\"5,8\" data=\"citationList\"} |\n", + "| IHttpClientFactory | 推荐用于生产环境,支持灵活配置:ml-citation{ref=\"1,7\" data=\"citationList\"} |\n", + "\n", + "通过添加更多 DelegatingHandler 实现缓存、重试、认证等!" + ] + } + ], + "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 +} diff --git a/Docs/1.4.0.高级使用.概述.ipynb b/Docs/1.4.3.高级使用.类型化客户端.ipynb similarity index 100% rename from Docs/1.4.0.高级使用.概述.ipynb rename to Docs/1.4.3.高级使用.类型化客户端.ipynb diff --git a/Docs/1.4.4.高级使用.使用管道.ipynb b/Docs/1.4.4.高级使用.使用管道.ipynb deleted file mode 100644 index 7656438..0000000 --- a/Docs/1.4.4.高级使用.使用管道.ipynb +++ /dev/null @@ -1,58 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "dotnet_interactive": { - "language": "csharp" - }, - "polyglot_notebook": { - "kernelName": "csharp" - } - }, - "source": [ - "# HttpClient 使用原则" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "dotnet_interactive": { - "language": "csharp" - }, - "polyglot_notebook": { - "kernelName": "csharp" - }, - "vscode": { - "languageId": "polyglot-notebook" - } - }, - "source": [ - "## 1、复用" - ] - } - ], - "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 -} diff --git a/Docs/1.4.1.高级使用.初始化.ipynb b/Docs/1.4.4.高级使用.工厂模式.ipynb similarity index 100% rename from Docs/1.4.1.高级使用.初始化.ipynb rename to Docs/1.4.4.高级使用.工厂模式.ipynb diff --git a/Docs/1.4.2.高级使用.连接池.ipynb b/Docs/1.4.5.高级使用.Polly.ipynb similarity index 100% rename from Docs/1.4.2.高级使用.连接池.ipynb rename to Docs/1.4.5.高级使用.Polly.ipynb diff --git a/Docs/1.4.5.高级使用.类型化客户端.ipynb b/Docs/1.4.5.高级使用.类型化客户端.ipynb deleted file mode 100644 index 7656438..0000000 --- a/Docs/1.4.5.高级使用.类型化客户端.ipynb +++ /dev/null @@ -1,58 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "dotnet_interactive": { - "language": "csharp" - }, - "polyglot_notebook": { - "kernelName": "csharp" - } - }, - "source": [ - "# HttpClient 使用原则" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "dotnet_interactive": { - "language": "csharp" - }, - "polyglot_notebook": { - "kernelName": "csharp" - }, - "vscode": { - "languageId": "polyglot-notebook" - } - }, - "source": [ - "## 1、复用" - ] - } - ], - "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 -} diff --git a/Docs/1.4.6.高级使用.工厂模式.ipynb b/Docs/1.4.6.高级使用.工厂模式.ipynb deleted file mode 100644 index 7656438..0000000 --- a/Docs/1.4.6.高级使用.工厂模式.ipynb +++ /dev/null @@ -1,58 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "dotnet_interactive": { - "language": "csharp" - }, - "polyglot_notebook": { - "kernelName": "csharp" - } - }, - "source": [ - "# HttpClient 使用原则" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "dotnet_interactive": { - "language": "csharp" - }, - "polyglot_notebook": { - "kernelName": "csharp" - }, - "vscode": { - "languageId": "polyglot-notebook" - } - }, - "source": [ - "## 1、复用" - ] - } - ], - "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 -} diff --git a/Docs/1.4.7.高级使用.Polly.ipynb b/Docs/1.4.7.高级使用.Polly.ipynb deleted file mode 100644 index 7656438..0000000 --- a/Docs/1.4.7.高级使用.Polly.ipynb +++ /dev/null @@ -1,58 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "dotnet_interactive": { - "language": "csharp" - }, - "polyglot_notebook": { - "kernelName": "csharp" - } - }, - "source": [ - "# HttpClient 使用原则" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "dotnet_interactive": { - "language": "csharp" - }, - "polyglot_notebook": { - "kernelName": "csharp" - }, - "vscode": { - "languageId": "polyglot-notebook" - } - }, - "source": [ - "## 1、复用" - ] - } - ], - "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 -} diff --git a/Docs/Assets/1.jpg b/Docs/Assets/1.jpg new file mode 100644 index 0000000..9504eb3 Binary files /dev/null and b/Docs/Assets/1.jpg differ diff --git a/Docs/Assets/HttpClient-使用管道.jpg b/Docs/Assets/HttpClient-使用管道.jpg new file mode 100644 index 0000000..feb34e5 Binary files /dev/null and b/Docs/Assets/HttpClient-使用管道.jpg differ diff --git a/HttpClientStudy.UnitTest/HttpClients/HttpMessageHandlerBuilder.cs b/HttpClientStudy.UnitTest/HttpClients/HttpMessageHandlerBuilder.cs new file mode 100644 index 0000000..295a892 --- /dev/null +++ b/HttpClientStudy.UnitTest/HttpClients/HttpMessageHandlerBuilder.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HttpClientStudy.UnitTest.HttpClients +{ + /// + /// 管道构建器 + /// + public class HttpMessageHandlerBuilder + { + public List Handlers { get; set; } + + public HttpMessageHandlerBuilder() + { + this.Handlers = new List(); + } + + public HttpMessageHandlerBuilder(List handlers) : base() + { + if (handlers != null) + { + this.Handlers = handlers; + } + } + + public void AddHttpMessageHandler(DelegatingHandler handler) + { + Handlers.Add(handler); + } + + public void RemoveHttpMessageHandler(DelegatingHandler handler) + { + Handlers.Remove(handler); + } + + public List FindHttpMessageHandler(DelegatingHandler handler) + { + return Handlers.Where(h => h.GetType() == handler.GetType()).ToList(); + } + + /// + /// 根据管道配置:创建一个新HttpClient实例 + /// + public HttpClient Build() + { + if (this.Handlers == null || this.Handlers.Count == 0) + { + return new HttpClient(); + } + + //默认最终处理器 + HttpMessageHandler defaultLastHandler = new SocketsHttpHandler() + { + //自定义配置 + PooledConnectionLifetime = TimeSpan.FromSeconds(120), + }; + + //倒序组装:完成后执行顺序与注册顺序一致 + //拷贝份,省得多次调用有问题 + + HttpMessageHandler currentHandler = new SocketsHttpHandler() + { + //自定义配置 + PooledConnectionLifetime = TimeSpan.FromSeconds(120), + }; + + var builderHandlers = this.Handlers.ToList(); + builderHandlers.Reverse(); + + if (builderHandlers[0].InnerHandler != null) + { + currentHandler = builderHandlers[0].InnerHandler; + } + + for (int i = 0; i < builderHandlers.Count; i++) + { + builderHandlers[i].InnerHandler = currentHandler; + currentHandler = builderHandlers[i]; + } + + // 利用管道创建HttpClient + var httpClient = new HttpClient(currentHandler); + + return httpClient; + } + } +} diff --git a/HttpClientStudy.UnitTest/HttpClients/PipelineClientTest.cs b/HttpClientStudy.UnitTest/HttpClients/PipelineClientTest.cs index ef40fc3..e7d8cdd 100644 --- a/HttpClientStudy.UnitTest/HttpClients/PipelineClientTest.cs +++ b/HttpClientStudy.UnitTest/HttpClients/PipelineClientTest.cs @@ -48,5 +48,33 @@ namespace HttpClientStudy.UnitTest.HttpClients var r = await client.GetAsync(WebApiConfigManager.GetWebApiConfig().BaseUrl + "/api/health"); r.EnsureSuccessStatusCode(); } + + [Fact] + public async Task BuilderHttpClient_Test() + { + //构建管道 + List handlers = new List() + { + new TokenDelegatingHandler(), + }; + + //添加 + handlers.Add(new LoggingHandler()); + handlers.Add(new ExceptionDelegatingHandler()); + + var handlerBuilder = new HttpMessageHandlerBuilder(handlers); + + //第一次:发送请求,从日志看管道执行顺序 + var httpClient = handlerBuilder.Build(); + httpClient.BaseAddress = webApiBaseUri; + var response = await httpClient.GetAsync("/api/Config/GetApiConfig"); + var content = await response.Content.ReadAsStringAsync(); + + Console.WriteLine($"响应内容:{content}"); + + // 改变管科 + handlers.Remove(new ExceptionDelegatingHandler()); + var httpClient2 = handlerBuilder.Build(); + } } }