注:本文隶属于《理解ASP.NET Core》系列文章,请查看置顶博客或点击此处查看全文目录

前言

在.NET中,我们有很多发送Http请求的手段,如HttpWebRequestWebClient以及HttpClient

在进入正文之前,先简单了解一下前2个:

HttpWebRequest

  1. namespace System.Net
  2. {
  3. public class HttpWebRequest : WebRequest, ISerializable { }
  4. }

HttpWebRequest位于System.Net命名空间下,继承自抽象类WebRequest,是.NET中最早、最原始地用于操作Http请求的类。相对来说,该类提供的方法更接近于底层,所以它的使用较为繁琐,对于开发者的水平要求是比较高的。

WebClient

  1. namespace System.Net
  2. {
  3. public class WebClient : Component { }
  4. }

同样的,WebClient也位于System.Net命名空间下,它主要是对WebRequest进行了一层封装,简化了常用任务场景的使用,如文件上传、文件下载、数据上传、数据下载等,并提供了一系列事件。

不过,虽然HttpWebRequestWebClient仍然可用,但官方建议,若没有特殊要求,不要使用他俩,而应该使用HttpClient。那HttpClient是什么呢?

HttpClient

  1. namespace System.Net.Http
  2. {
  3. public class HttpClient : HttpMessageInvoker { }
  4. }

HttpClient位于System.Net.Http命名空间下,它提供了GetAsyncPostAsyncPutAsyncDeleteAsyncPatchAsync等方法,更适合操作当下流行的Rest风格的Http Api。而且,它提供的方法几乎都是异步的,非常适合当下的异步编程模型。

而且,HttpClient旨在实例化一次,并在应用程序的整个生命周期内重复使用,也就是说,可以使用一个HttpClient实例可以发送多次以及多个不同的请求。

不过需要注意的是,如果每次请求反而都实例化一个HttpClient,由于Dispose并不会立即释放套接字,那么当短时间内有大量请求时,就会导致服务器的套接字数被耗尽,从而引发SocketException异常。

我们一起来看一个错误的示例:

  1. public class ValuesController : ControllerBase
  2. {
  3. [HttpGet("WrongUsage")]
  4. public async Task<string> WrongUsage()
  5. {
  6. try
  7. {
  8. // 模拟10次请求,每次请求都创建一个新的 HttpClient
  9. var i = 0;
  10. while (i++ < 10)
  11. {
  12. using var client = new HttpClient();
  13. await client.GetAsync("https://jsonplaceholder.typicode.com/posts/1");
  14. }
  15. return "Success";
  16. }
  17. catch (Exception ex)
  18. {
  19. return ex.ToString();
  20. }
  21. }
  22. }

jsonplaceholder.typicode.com 是一个免费提供虚假API的网站,我们可以使用它来方便测试。

在Windows中,当你请求WrongUsage接口之后,可以通过 netstat 命令查看套接字连接(jsonplaceholder的IP为172.67.131.170:443),你会发现程序虽然已经退出了,但是连接并没有像我们所预期的那样立即关闭:

  1. > netstat -n | find "172.67.131.170"
  2. TCP 172.16.161.10:1057 172.67.131.170:443 TIME_WAIT
  3. TCP 172.16.161.10:1058 172.67.131.170:443 TIME_WAIT
  4. TCP 172.16.161.10:1061 172.67.131.170:443 TIME_WAIT
  5. TCP 172.16.161.10:1065 172.67.131.170:443 TIME_WAIT
  6. TCP 172.16.161.10:1070 172.67.131.170:443 TIME_WAIT
  7. TCP 172.16.161.10:1073 172.67.131.170:443 TIME_WAIT
  8. TCP 172.16.161.10:10005 172.67.131.170:443 TIME_WAIT

下面是一个较为合理的示例:

  1. public class ValuesController : ControllerBase
  2. {
  3. private static readonly HttpClient _httpClient;
  4. static ValuesController()
  5. {
  6. // 复用同一个实例
  7. _httpClient = new HttpClient();
  8. }
  9. }

可以看出,HttpClient很容易被错误使用,并且,即使是上面的正确示例,仍然有很多待优化的地方。因此,为了解决这个问题,IHttpClientFactory诞生了。

IHttpClientFactory

看名字就知道了,IHttpClientFactory可以帮我们创建所需要的HttpClient实例,我们无须关心实例的创建过程。与HttpClient一样,位于System.Net.Http命名空间下。

下面先了解一下它的一些用法。

基础用法

首先,注册HttpClientFactory相关的服务

  1. var builder = WebApplication.CreateBuilder(args);
  2. builder.Services.AddHttpClient();

然后,构造函数注入IHttpClientFactory,通过CreateClient()创建Client实例。

  1. public class ValuesController : ControllerBase
  2. {
  3. private readonly IHttpClientFactory _httpClientFactory;
  4. public ValuesController(IHttpClientFactory httpClientFactory)
  5. {
  6. _httpClientFactory = httpClientFactory;
  7. }
  8. [HttpGet]
  9. public async Task<string> Get()
  10. {
  11. // 通过 _httpClientFactory 创建 Client 实例
  12. var client = _httpClientFactory.CreateClient();
  13. var response = await client.GetAsync("https://jsonplaceholder.typicode.com/posts/1");
  14. if (response.IsSuccessStatusCode)
  15. {
  16. return await response.Content.ReadAsStringAsync();
  17. }
  18. return $"{response.StatusCode}: {response.ReasonPhrase}";
  19. }
  20. }

输出:

  1. {
  2. "userId": 1,
  3. "id": 1,
  4. "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  5. "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
  6. }

命名客户端

类似于命名选项,我们也可以添加命名的HttpClient,并添加一些全局默认配置。下面我们添加一个名为jsonplaceholder的客户端:

  1. // jsonplaceholder client
  2. builder.Services.AddHttpClient("jsonplaceholder", (sp, client) =>
  3. {
  4. // 基址
  5. client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
  6. // 请求头
  7. client.DefaultRequestHeaders.Add(HeaderNames.Accept, "application/json");
  8. client.DefaultRequestHeaders.Add(HeaderNames.UserAgent, "HttpClientFactory-Sample-Named");
  9. });
  10. [HttpGet("named")]
  11. public async Task<dynamic> GetNamed()
  12. {
  13. // 获取指定名称的 Client
  14. var client = _httpClientFactory.CreateClient("jsonplaceholder");
  15. var response = await client.GetAsync("posts/1");
  16. if (response.IsSuccessStatusCode)
  17. {
  18. return new
  19. {
  20. Content = await response.Content.ReadAsStringAsync(),
  21. AcceptHeader = response.RequestMessage!.Headers.Accept.ToString(),
  22. UserAgentHeader = response.RequestMessage.Headers.UserAgent.ToString()
  23. };
  24. }
  25. return $"{response.StatusCode}: {response.ReasonPhrase}";
  26. }

输出:

  1. {
  2. "content": "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"sunt aut facere repellat provident occaecati excepturi optio reprehenderit\",\n \"body\": \"quia et suscipit\\nsuscipit recusandae consequuntur expedita et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem sunt rem eveniet architecto\"\n}",
  3. "acceptHeader": "application/json",
  4. "userAgentHeader": "HttpClientFactory-Sample-Named"
  5. }

实际上,在创建HttpClient实例时,也可以指定未在服务中注册的HttpClient名字。读完文章后面,你就知道为什么了。

类型化客户端

客户端也可以被类型化,这样做的好处有:

  • 无需像命名客户端那样通过传递字符串获取客户端实例
  • 可以将同一类别的调用接口进行归类、封装
  • 有智能提示

下面看个简单地例子,首先,创建一个类型化客户端JsonPlaceholderClient,用于封装对jsonplaceholder接口的调用:

  1. public class JsonPlaceholderClient
  2. {
  3. private readonly HttpClient _httpClient;
  4. // 直接注入 HttpClient
  5. public JsonPlaceholderClient(HttpClient httpClient)
  6. {
  7. _httpClient = httpClient;
  8. }
  9. public async Task<dynamic> GetPost(int id) =>
  10. await _httpClient.GetFromJsonAsync<dynamic>($"/posts/{id}");
  11. }

为了让DI容器知道要将哪个HttpClient实例注入到JsonPlaceholderClient的构造函数,我们需要配置一下服务:

  1. builder.Services.AddHttpClient<JsonPlaceholderClient>(client =>
  2. {
  3. client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
  4. client.DefaultRequestHeaders.Add(HeaderNames.Accept, "application/json");
  5. client.DefaultRequestHeaders.Add(HeaderNames.UserAgent, "HttpClientFactory-Sample-Typed");
  6. });

最后,我们直接注入JsonPlaceholderClient,而不再是IHttpClientFactory,使用起来就好像在调用本地服务似的:

  1. public class ValuesController : ControllerBase
  2. {
  3. private readonly JsonPlaceholderClient _jsonPlaceholderClient;
  4. public ValuesController(JsonPlaceholderClient jsonPlaceholderClient)
  5. {
  6. _jsonPlaceholderClient = jsonPlaceholderClient;
  7. }
  8. [HttpGet("typed")]
  9. public async Task<dynamic> GetTyped()
  10. {
  11. var post = await _jsonPlaceholderClient.GetPost(1);
  12. return post;
  13. }
  14. }

借助第三方库生成的客户端

一般来说,类型化的客户端已经大大简化了我们使用HttpClient的步骤和难度,不过,我们还可以借助第三方库再次简化我们的代码:我们只需要定义要调用的服务接口,第三方库会生成代理类。

常用的第三方库有以下两个:

这两个第三方库的使用方式非常类似,由于我比较熟悉WebApiClientCore,所以后面的示例均使用它进行演示。

首先,安装Nuget包:

  1. Install-Package WebApiClientCore

接着,创建一个接口IJsonPlaceholderApi

  1. [Header("User-Agent", "HttpClientFactory-Sample-Api")]
  2. [Header("Custom-Header", "Custom-Value")]
  3. public interface IJsonPlaceholderApi
  4. {
  5. [HttpGet("/posts/{id}")]
  6. Task<dynamic> GetPost(int id);
  7. }

怎么样,看起来是不是很像在写Web Api?

对了,别忘了进行服务注册:

  1. builder.Services.AddHttpApi<IJsonPlaceholderApi>(
  2. o =>
  3. {
  4. o.HttpHost = new Uri("https://jsonplaceholder.typicode.com/");
  5. o.UseDefaultUserAgent = false;
  6. });

最后,我们就可以更方便地用它了:

  1. public class ValuesController : ControllerBase
  2. {
  3. private readonly IJsonPlaceholderApi _jsonPlaceholderApi;
  4. public ValuesController(IJsonPlaceholderApi jsonPlaceholderApi)
  5. {
  6. _jsonPlaceholderApi = jsonPlaceholderApi;
  7. }
  8. [HttpGet("api")]
  9. public async Task<dynamic> GetApi()
  10. {
  11. var post = await _jsonPlaceholderApi.GetPost(1);
  12. return post;
  13. }
  14. }

HttpClient设计原理

上面我们提到过:HttpClient旨在实例化一次,并在应用程序的整个生命周期内重复使用。如果每次请求都实例化一个HttpClient,由于Dispose并不会立即释放套接字,那么当短时间内有大量请求时,服务器的套接字数就会被耗尽,从而引发SocketException异常。

为了能够真正理解这句话,我们一起看一下HttpClient的是如何发送请求并处理响应结果的。

下面,我们先看下HttpClient的基本结构:

按照惯例,为了方便理解,后续列出的源码中我已经删除了一些不是那么重要的代码。

  1. public class HttpMessageInvoker : IDisposable
  2. {
  3. private volatile bool _disposed;
  4. private readonly bool _disposeHandler;
  5. private readonly HttpMessageHandler _handler;
  6. public HttpMessageInvoker(HttpMessageHandler handler) : this(handler, true) { }
  7. public HttpMessageInvoker(HttpMessageHandler handler, bool disposeHandler)
  8. {
  9. _handler = handler;
  10. _disposeHandler = disposeHandler;
  11. }
  12. [UnsupportedOSPlatformAttribute("browser")]
  13. public virtual HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) =>
  14. _handler.Send(request, cancellationToken);
  15. public virtual Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
  16. _handler.SendAsync(request, cancellationToken);
  17. public void Dispose()
  18. {
  19. Dispose(true);
  20. GC.SuppressFinalize(this);
  21. }
  22. protected virtual void Dispose(bool disposing)
  23. {
  24. if (disposing && !_disposed)
  25. {
  26. _disposed = true;
  27. if (_disposeHandler)
  28. {
  29. _handler.Dispose();
  30. }
  31. }
  32. }
  33. }
  34. public class HttpClient : HttpMessageInvoker
  35. {
  36. private const HttpCompletionOption DefaultCompletionOption = HttpCompletionOption.ResponseContentRead;
  37. private volatile bool _disposed;
  38. private int _maxResponseContentBufferSize;
  39. public HttpClient() : this(new HttpClientHandler()) { }
  40. public HttpClient(HttpMessageHandler handler) : this(handler, true) { }
  41. public HttpClient(HttpMessageHandler handler, bool disposeHandler) : base(handler, disposeHandler) =>
  42. _maxResponseContentBufferSize = HttpContent.MaxBufferSize;
  43. // 中间的Rest方法就略过了,因为它们的内部都是通过调用 SendAsync 实现的
  44. // 同步的 Send 方法与异步的 SendAsync 实现类似
  45. public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request) =>
  46. SendAsync(request, DefaultCompletionOption, CancellationToken.None);
  47. public override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
  48. SendAsync(request, DefaultCompletionOption, cancellationToken);
  49. public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption) =>
  50. SendAsync(request, completionOption, CancellationToken.None);
  51. public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
  52. {
  53. var response = await base.SendAsync(request, cts.Token).ConfigureAwait(false);
  54. ThrowForNullResponse(response);
  55. if (ShouldBufferResponse(completionOption, request))
  56. {
  57. await response.Content.LoadIntoBufferAsync(_maxResponseContentBufferSize, cts.Token).ConfigureAwait(false);
  58. }
  59. return response;
  60. }
  61. private static void ThrowForNullResponse(HttpResponseMessage? response)
  62. {
  63. if (response is null) throw new InvalidOperationException(...);
  64. }
  65. private static bool ShouldBufferResponse(HttpCompletionOption completionOption, HttpRequestMessage request) =>
  66. completionOption == HttpCompletionOption.ResponseContentRead
  67. && !string.Equals(request.Method.Method, "HEAD", StringComparison.OrdinalIgnoreCase);
  68. protected override void Dispose(bool disposing)
  69. {
  70. if (disposing && !_disposed)
  71. {
  72. _disposed = true;
  73. // ...
  74. }
  75. base.Dispose(disposing);
  76. }
  77. }

看过之后,我们对HttpClient的基本结构可以有一个清晰的认识:

  • HttpClient继承自HttpMessageInvoker,“调用者”,很形象的一个名字。
  • Send/SendAsync方法是整个类的核心方法,所有的请求都是通过调用它们来实现的
  • HttpClient只是对HttpMessageHandler的包装,实际上,所有的请求都是通过这个Handler来发送的。

如果你足够细心,你会发现其中的一个构造函数接收了一个名为disposeHandler的参数,用于指示是否要释放HttpMessageHandler。为什么要这么设计呢?我们知道,HttpClient旨在实例化一次,并在应用程序的整个生命周期内重复使用,实际上指的是HttpMessageHandler,为了在多个地方复用它,该参数允许我们创建多个HttpClient实例,但使用的都是同一个HttpMessageHandler实例(参见下方的IHttpClientFactory设计方式)。

下面看一下HttpMessageHandler及其子类HttpClientHandler

  1. public abstract class HttpMessageHandler : IDisposable
  2. {
  3. protected HttpMessageHandler() { }
  4. // 这个方法是后加的,为了不影响它的已存在的子类,所以将其设置为了virtual(而不是abstract),并默认抛NSE
  5. protected internal virtual HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
  6. {
  7. throw new NotSupportedException(...);
  8. }
  9. protected internal abstract Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
  10. protected virtual void Dispose(bool disposing)
  11. {
  12. // 基类中啥都没干
  13. }
  14. public void Dispose()
  15. {
  16. Dispose(true);
  17. GC.SuppressFinalize(this);
  18. }
  19. }
  20. // 这里我们不讨论作为WASM运行在浏览器中的情况
  21. public class HttpClientHandler : HttpMessageHandler
  22. {
  23. // Socket
  24. private readonly SocketsHttpHandler _underlyingHandler;
  25. private volatile bool _disposed;
  26. public HttpClientHandler()
  27. {
  28. _underlyingHandler = new HttpHandlerType();
  29. ClientCertificateOptions = ClientCertificateOption.Manual;
  30. }
  31. private HttpMessageHandler Handler => _underlyingHandler;
  32. // Send 与 SendAsync 类似
  33. protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
  34. Handler.SendAsync(request, cancellationToken);
  35. protected override void Dispose(bool disposing)
  36. {
  37. if (disposing && !_disposed)
  38. {
  39. _disposed = true;
  40. _underlyingHandler.Dispose();
  41. }
  42. base.Dispose(disposing);
  43. }
  44. }

实际上,在.NET Core 2.1(不包含)之前,HttpClient默认使用的HttpMessageHandler在各个平台上的实现各不相同,直到.NET Core 2.1开始,HttpClient才统一默认使用SocketsHttpHandler,这带来了很多好处:

  • 更高的性能
  • 消除了平台依赖,简化了部署和服务
  • 在所有的.NET平台上行为一致
  1. [UnsupportedOSPlatform("browser")]
  2. public sealed class SocketsHttpHandler : HttpMessageHandler
  3. {
  4. private readonly HttpConnectionSettings _settings = new HttpConnectionSettings();
  5. private HttpMessageHandlerStage? _handler;
  6. private bool _disposed;
  7. // Send 与 SendAsync 类似
  8. protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
  9. {
  10. HttpMessageHandler handler = _handler ?? SetupHandlerChain();
  11. return handler.SendAsync(request, cancellationToken);
  12. }
  13. private HttpMessageHandlerStage SetupHandlerChain()
  14. {
  15. HttpConnectionSettings settings = _settings.CloneAndNormalize();
  16. HttpConnectionPoolManager poolManager = new HttpConnectionPoolManager(settings);
  17. HttpMessageHandlerStage handler;
  18. if (settings._credentials == null)
  19. {
  20. handler = new HttpConnectionHandler(poolManager);
  21. }
  22. else
  23. {
  24. handler = new HttpAuthenticatedConnectionHandler(poolManager);
  25. }
  26. // 省略了一些Handlers管道的组装,与中间件管道类似
  27. // 释放旧的 _handler
  28. if (Interlocked.CompareExchange(ref _handler, handler, null) != null)
  29. {
  30. handler.Dispose();
  31. }
  32. return _handler;
  33. }
  34. protected override void Dispose(bool disposing)
  35. {
  36. if (disposing && !_disposed)
  37. {
  38. _disposed = true;
  39. _handler?.Dispose();
  40. }
  41. base.Dispose(disposing);
  42. }
  43. }
  44. // HttpAuthenticatedConnectionHandler 结构类似
  45. internal sealed class HttpConnectionHandler : HttpMessageHandlerStage
  46. {
  47. // Http连接池管理器
  48. private readonly HttpConnectionPoolManager _poolManager;
  49. public HttpConnectionHandler(HttpConnectionPoolManager poolManager) =>
  50. _poolManager = poolManager;
  51. internal override ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) =>
  52. _poolManager.SendAsync(request, async, doRequestAuth: false, cancellationToken);
  53. protected override void Dispose(bool disposing)
  54. {
  55. if (disposing)
  56. {
  57. _poolManager.Dispose();
  58. }
  59. base.Dispose(disposing);
  60. }
  61. }

后面的就比较底层了,今天咱们就看到这里吧。下面我们看一下IHttpClientFactory

IHttpClientFactory设计方式

我们先从服务注册看起:

  1. public static class HttpClientFactoryServiceCollectionExtensions
  2. {
  3. public static IServiceCollection AddHttpClient(this IServiceCollection services)
  4. {
  5. services.AddLogging();
  6. services.AddOptions();
  7. // 核心服务
  8. services.TryAddTransient<HttpMessageHandlerBuilder, DefaultHttpMessageHandlerBuilder>();
  9. services.TryAddSingleton<DefaultHttpClientFactory>();
  10. services.TryAddSingleton<IHttpClientFactory>(serviceProvider => serviceProvider.GetRequiredService<DefaultHttpClientFactory>());
  11. services.TryAddSingleton<IHttpMessageHandlerFactory>(serviceProvider => serviceProvider.GetRequiredService<DefaultHttpClientFactory>());
  12. // 类型化客户端服务
  13. services.TryAdd(ServiceDescriptor.Transient(typeof(ITypedHttpClientFactory<>), typeof(DefaultTypedHttpClientFactory<>)));
  14. services.TryAdd(ServiceDescriptor.Singleton(typeof(DefaultTypedHttpClientFactory<>.Cache), typeof(DefaultTypedHttpClientFactory<>.Cache)));
  15. services.TryAddEnumerable(ServiceDescriptor.Singleton<IHttpMessageHandlerBuilderFilter, LoggingHttpMessageHandlerBuilderFilter>());
  16. services.TryAddSingleton(new HttpClientMappingRegistry());
  17. // 默认注册一个名字为空字符串的 HttpClient 实例
  18. services.TryAddTransient(s => s.GetRequiredService<IHttpClientFactory>().CreateClient(string.Empty));
  19. return services;
  20. }
  21. public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, string name)
  22. {
  23. AddHttpClient(services);
  24. // 返回一个Builder,以允许继续针对HttpClient进行配置
  25. return new DefaultHttpClientBuilder(services, name);
  26. }
  27. public static IHttpClientBuilder AddHttpClient<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TClient>(
  28. this IServiceCollection services)
  29. where TClient : class
  30. {
  31. AddHttpClient(services);
  32. // 获取类型名作为客户端名
  33. string name = TypeNameHelper.GetTypeDisplayName(typeof(TClient), fullName: false);
  34. var builder = new DefaultHttpClientBuilder(services, name);
  35. // 目的是通过 ActivatorUtilities 动态创建 TClient 实例,并通过构造函数注入 HttpClient
  36. builder.AddTypedClientCore<TClient>(validateSingleType: true);
  37. return builder;
  38. }
  39. }

很显然,HttpMessageHandlerBuilder的作用就是创建HttpMessageHandler实例,默认实现为DefaultHttpMessageHandlerBuilder

IHttpMessageHandlerBuilderFilter会在DefaultHttpClientFactory中用到,它可以在HttpMessageHandlerBuilder.Build调用之前对HttpMessageHandlerBuilder进行一些初始化操作。

IHttpClientFactory接口的默认实现是DefaultHttpClientFactory

  1. internal class DefaultHttpClientFactory : IHttpClientFactory, IHttpMessageHandlerFactory
  2. {
  3. private readonly IServiceProvider _services;
  4. private readonly Func<string, Lazy<ActiveHandlerTrackingEntry>> _entryFactory;
  5. // 有效的Handler对象池,使用Lazy来保证每个命名客户端具有唯一的 HttpMessageHandler 实例
  6. internal readonly ConcurrentDictionary<string, Lazy<ActiveHandlerTrackingEntry>> _activeHandlers;
  7. // 过期的Handler集合
  8. internal readonly ConcurrentQueue<ExpiredHandlerTrackingEntry> _expiredHandlers;
  9. public DefaultHttpClientFactory(
  10. IServiceProvider services,
  11. IServiceScopeFactory scopeFactory,
  12. ILoggerFactory loggerFactory,
  13. IOptionsMonitor<HttpClientFactoryOptions> optionsMonitor,
  14. IEnumerable<IHttpMessageHandlerBuilderFilter> filters)
  15. {
  16. _services = services;
  17. _activeHandlers = new ConcurrentDictionary<string, Lazy<ActiveHandlerTrackingEntry>>(StringComparer.Ordinal);
  18. _entryFactory = (name) =>
  19. {
  20. return new Lazy<ActiveHandlerTrackingEntry>(() =>
  21. {
  22. return CreateHandlerEntry(name);
  23. }, LazyThreadSafetyMode.ExecutionAndPublication);
  24. };
  25. }
  26. public HttpClient CreateClient(string name)
  27. {
  28. HttpMessageHandler handler = CreateHandler(name);
  29. return new HttpClient(handler, disposeHandler: false);
  30. }
  31. public HttpMessageHandler CreateHandler(string name)
  32. {
  33. // 若存在指定的命名客户端的活跃的Handler,则直接使用,若不存在,则新建一个
  34. ActiveHandlerTrackingEntry entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;
  35. return entry.Handler;
  36. }
  37. internal ActiveHandlerTrackingEntry CreateHandlerEntry(string name)
  38. {
  39. HttpMessageHandlerBuilder builder = _services.GetRequiredService<HttpMessageHandlerBuilder>();
  40. builder.Name = name;
  41. var handler = new LifetimeTrackingHttpMessageHandler(builder.Build());
  42. // options.HandlerLifetime 默认2分钟
  43. return new ActiveHandlerTrackingEntry(name, handler, scope, options.HandlerLifetime);
  44. }
  45. }
  46. public static class HttpClientFactoryExtensions
  47. {
  48. public static HttpClient CreateClient(this IHttpClientFactory factory) =>
  49. factory.CreateClient(Options.DefaultName); // 名字为 string.Empty
  50. }

可以发现,我们每次调用CreateClient,都是新创建一个HttpClient实例,但是,当这些HttpClient实例同名时,所使用的HttpMessageHandler在一定条件下,其实都是同一个。

另外,你也可以发现,所有通过IHttpClientFactory创建的HttpClient,都是命名客户端:

  • 未指定名字的,则默认使用空字符串作为客户端的名字
  • 类型客户端使用类型名作为客户端的名字

Handler的创建是通过DefaultHttpMessageHandlerBuilder调用Build来实现的,不同的是,Factory并非是简单地创建一个Handler,而是建立了一个Handler管道,这是通过抽象类DelegatingHandler实现的。其中,管道最底层的Handler默认是HttpClientHandler,与我们直接new HttpClient()时所创建的Handler是一样的。

与中间件管道类似,DelegatingHandler的作用就是将Http请求的发送和处理委托给内部的另一个Handler处理,而它可以在这个Handler处理之前和之后加一些自己的特定逻辑。

  1. public abstract class DelegatingHandler : HttpMessageHandler
  2. {
  3. private HttpMessageHandler? _innerHandler;
  4. private volatile bool _disposed;
  5. [DisallowNull]
  6. public HttpMessageHandler? InnerHandler
  7. {
  8. get => _innerHandler;
  9. set => _innerHandler = value;
  10. }
  11. protected DelegatingHandler() { }
  12. // 这里接收的innerHandler就是负责发送和处理Http请求的
  13. protected DelegatingHandler(HttpMessageHandler innerHandler) =>
  14. InnerHandler = innerHandler;
  15. protected internal override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) =>
  16. _innerHandler!.Send(request, cancellationToken);
  17. protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
  18. _innerHandler!.SendAsync(request, cancellationToken);
  19. protected override void Dispose(bool disposing)
  20. {
  21. if (disposing && !_disposed)
  22. {
  23. _disposed = true;
  24. if (_innerHandler != null)
  25. {
  26. _innerHandler.Dispose();
  27. }
  28. }
  29. base.Dispose(disposing);
  30. }
  31. }

这里我们看到的LifetimeTrackingHttpMessageHandler,以及源码中我删除掉的LoggingHttpMessageHandler都是DelegatingHandler的子类。

你有没有想过,为啥最后要包装成LifetimeTrackingHttpMessageHandler呢?其实很简单,它就是一个标识,标志着它内部的Handler在超出生命周期后,需要被释放。

另外,实际上,创建好的HttpMessageHandler并非能够一直重用,默认可重用的生命周期为2分钟,我们会将可重用的放在_activeHandlers中,而过期的放在了_expiredHandlers,并在合适的时候释放销毁。注意,过期不意味着要立即销毁,只是不再重用,即不再分配给新的HttpClient实例了。

那为什么不让创建好的HttpMessageHandler一直重用,干嘛要销毁呢?它的原理与各种池(如数据库连接池、线程池)类似,就是为了保证套接字连接在空闲的时候能够被及时关闭,而不是长时间保持打开的状态,白白占用资源。

总结

现在,我们已经对HttpClientIHttpClientFactory有了一个清晰的认识,我们简单总结一下:

  • HttpClient是当前.NET版本中发送Http请求的首选
  • HttpClient提供了很多异步Rest方法,非常适合当下的异步编程模型
  • HttpClient旨在实例化一次,并在应用程序的整个生命周期内重复使用。
  • 直接创建HttpClient实例,很容易被错误使用,建议通过IHttpClientFactory来创建
  • HttpClient是对HttpMessageHandler的包装,默认使用HttpMessageHandler的子类HttpClientHandler,而HttpClientHandler也只是对SocketsHttpHandler的简单包装(不讨论WASM)
  • 通过IHttpClientFactory,我们可以方便地创建命名客户端、类型化客户端等
  • IHttpClientFactory通过创建多个HttpClient实例,但多个实例重用同一个HttpMessageHandler来优化HttpClient的创建

理解ASP.NET Core - 发送Http请求(HttpClient)的更多相关文章

  1. .Net Core 发送https请求/.net core 调用数字证书 使用X509Certificate2

    .Net Core 发送https请求 .net core 调用数字证书 使用X509Certificate2 .NET下面的 .netfromwork使用和asp.net core下使用方式不一样 ...

  2. 理解ASP.NET Core - [02] Middleware

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 中间件 先借用微软官方文档的一张图: 可以看到,中间件实际上是一种配置在HTTP请求管道中,用 ...

  3. 理解ASP.NET Core - 错误处理(Handle Errors)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或[点击此处查看全文目录](https://www.cnblogs.com/xiaoxiaotank/p/151852 ...

  4. 理解ASP.NET Core - 基于Cookie的身份认证(Authentication)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 概述 通常,身份认证(Authentication)和授权(Authorization)都会放 ...

  5. 理解ASP.NET Core - 基于JwtBearer的身份认证(Authentication)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 在开始之前,如果你还不了解基于Cookie的身份认证,那么建议你先阅读<基于Cookie ...

  6. 理解 ASP.NET Core: 处理管道

    理解 ASP.NET Core 处理管道 在 ASP.NET Core 的管道处理部分,实现思想已经不是传统的面向对象模式,而是切换到了函数式编程模式.这导致代码的逻辑大大简化,但是,对于熟悉面向对象 ...

  7. 理解ASP.NET Core - [01] Startup

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 准备工作:一份ASP.NET Core Web API应用程序 当我们来到一个陌生的环境,第一 ...

  8. 理解ASP.NET Core - [03] Dependency Injection

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 依赖注入 什么是依赖注入 简单说,就是将对象的创建和销毁工作交给DI容器来进行,调用方只需要接 ...

  9. 理解ASP.NET Core - [04] Host

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 本文会涉及部分 Host 相关的源码,并会附上 github 源码地址,不过为了降低篇幅,我会 ...

随机推荐

  1. Python学习--Python的了解与安装

    Python简介: Python 是一种解释型.面向对象.动态数据类型的高级程序设计语言. Python 由荷兰人Guido van Rossum 于 1989 年底发明,第一个公开发行版发行于 19 ...

  2. ctfhub 双写绕过 文件头检查

    双写绕过 进入环境 上传一句话木马 上传路径感觉不对检查源代码 从此处可以看出需要双写绕过 使用bp抓包 此处这样修改即可双写绕过 使用蚁剑连接 即可找到flag 文件头检查 进入环境 上传一句话木马 ...

  3. docker 容器简单使用

    文章目录 docker简介 docker容器简单使用 1.HelloWorld 2.运行交互式的容器 3.启动容器(后台模式) 安装docker容器的博文有很多这里就不做过多赘述了,另外如果不想安装d ...

  4. HTML5 版的flappy bird

    Flappy Bird这款简单的小游戏累计下载量已经超过5000万次,每天收入至少5万美元.然而,2月10日其开发者Dong Nguyen却将Flappy Bird从苹果App Store和Googl ...

  5. html5手机页面的那些meta

    一.普通手机页的设置1.<meta name="viewport" content=""/>说明:屏幕的缩放 content的几个属性: width ...

  6. 一个很好用的 vue-picker组件

    vue-picker a picker componemt for vue2.0 走了一圈 github 都没有找到自己想要的移动端的 vue-picker的组件,于是自己就下手,撸了一个出来,感受下 ...

  7. Muse UI遇到的坑

    写在前面:我只是一个前端小白,文章中的提到可能会有不足之处,仅提供一个参考.若有不完善的地方,欢迎各位大佬指出,希望对你有帮助! 故事背景是这样的,最近做一个Vue项目,使用到 Muse UI 组件库 ...

  8. 【java】密码检查

    [问题描述] 开发一个密码检查软件,密码要求: 长度超过8位 包括大小写字母.数字.其它符号,以上四种至少三种 不能有相同长度超2的子串重复 [输入形式] 一组或多组长度超过2的子符串.每组占一行 [ ...

  9. Mybatis-sql语句的抽取

    1.抽取之前的UserMapper.xml <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE ...

  10. 获取ul中li的value值

    <script> $(function(){ $(".month-list").find("li").click(function(){ var t ...