• 理解ASP.NET Core - 发送Http请求(HttpClient)


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

    前言

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

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

    HttpWebRequest

    csharp
    namespace System.Net
    {
        public class HttpWebRequest : WebRequest, ISerializable { }
    }
    

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

    WebClient

    csharp
    namespace System.Net
    {
        public class WebClient : Component { }
    }
    

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

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

    HttpClient

    csharp
    namespace System.Net.Http
    {
        public class HttpClient : HttpMessageInvoker { }
    }
    

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

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

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

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

    csharp
    public class ValuesController : ControllerBase
    {
        [HttpGet("WrongUsage")]
        public async Task<string> WrongUsage()
        {
            try
            {
                // 模拟10次请求,每次请求都创建一个新的 HttpClient
                var i = 0;
                while (i++ < 10)
                {
                    using var client = new HttpClient();
                    await client.GetAsync("https://jsonplaceholder.typicode.com/posts/1");                   
                }
    
                return "Success";
            }
            catch (Exception ex)
            {
                return ex.ToString();
            }
        }
    }
    

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

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

    csharp
    > netstat -n | find "172.67.131.170"
      TCP    172.16.161.10:1057     172.67.131.170:443     TIME_WAIT
      TCP    172.16.161.10:1058     172.67.131.170:443     TIME_WAIT
      TCP    172.16.161.10:1061     172.67.131.170:443     TIME_WAIT
      TCP    172.16.161.10:1065     172.67.131.170:443     TIME_WAIT
      TCP    172.16.161.10:1070     172.67.131.170:443     TIME_WAIT
      TCP    172.16.161.10:1073     172.67.131.170:443     TIME_WAIT
      TCP    172.16.161.10:10005    172.67.131.170:443     TIME_WAIT
    

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

    csharp
    public class ValuesController : ControllerBase
    {
        private static readonly HttpClient _httpClient;
    
        static ValuesController()
        {
            // 复用同一个实例
            _httpClient = new HttpClient();
        }
    }
    

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

    IHttpClientFactory

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

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

    基础用法

    首先,注册HttpClientFactory相关的服务

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

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

    csharp
    public class ValuesController : ControllerBase
    {
        private readonly IHttpClientFactory _httpClientFactory;
    
        public ValuesController(IHttpClientFactory httpClientFactory)
        {
            _httpClientFactory = httpClientFactory;
        }
    
        [HttpGet]
        public async Task<string> Get()
        {
            // 通过 _httpClientFactory 创建 Client 实例
            var client = _httpClientFactory.CreateClient();
            var response = await client.GetAsync("https://jsonplaceholder.typicode.com/posts/1");
            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            
            return $"{response.StatusCode}: {response.ReasonPhrase}";
        }
    }
    

    输出:

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

    命名客户端

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

    csharp
    // jsonplaceholder client
    builder.Services.AddHttpClient("jsonplaceholder", (sp, client) =>
    {
        // 基址
        client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
        // 请求头
        client.DefaultRequestHeaders.Add(HeaderNames.Accept, "application/json");
        client.DefaultRequestHeaders.Add(HeaderNames.UserAgent, "HttpClientFactory-Sample-Named");
    });
    
    [HttpGet("named")]
    public async Task<dynamic> GetNamed()
    {
        // 获取指定名称的 Client
        var client = _httpClientFactory.CreateClient("jsonplaceholder");
        var response = await client.GetAsync("posts/1");
        if (response.IsSuccessStatusCode)
        {
            return new
            {
                Content = await response.Content.ReadAsStringAsync(),
                AcceptHeader = response.RequestMessage!.Headers.Accept.ToString(),
                UserAgentHeader = response.RequestMessage.Headers.UserAgent.ToString()
            };
        }
    
        return $"{response.StatusCode}: {response.ReasonPhrase}";
    }
    

    输出:

    json
    {
      "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}",
      "acceptHeader": "application/json",
      "userAgentHeader": "HttpClientFactory-Sample-Named"
    }
    

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

    类型化客户端

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

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

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

    csharp
    public class JsonPlaceholderClient
    {
        private readonly HttpClient _httpClient;
    
        // 直接注入 HttpClient
        public JsonPlaceholderClient(HttpClient httpClient)
        {
            _httpClient = httpClient;
        }
    
        public async Task<dynamic> GetPost(int id) =>
            await _httpClient.GetFromJsonAsync<dynamic>($"/posts/{id}");
    }
    

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

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

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

    csharp
    public class ValuesController : ControllerBase
    {
        private readonly JsonPlaceholderClient _jsonPlaceholderClient;
    
        public ValuesController(JsonPlaceholderClient jsonPlaceholderClient)
        {
            _jsonPlaceholderClient = jsonPlaceholderClient;
        }
        
        [HttpGet("typed")]
        public async Task<dynamic> GetTyped()
        {
            var post = await _jsonPlaceholderClient.GetPost(1);
            
            return post;
        }
    }
    

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

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

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

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

    首先,安装Nuget包:

    highlighter- ada
    Install-Package WebApiClientCore
    

    接着,创建一个接口IJsonPlaceholderApi

    csharp
    [Header("User-Agent", "HttpClientFactory-Sample-Api")]
    [Header("Custom-Header", "Custom-Value")]
    public interface IJsonPlaceholderApi
    {
        [HttpGet("/posts/{id}")]
        Task<dynamic> GetPost(int id);
    }
    

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

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

    csharp
    builder.Services.AddHttpApi<IJsonPlaceholderApi>(
        o =>
        {
            o.HttpHost = new Uri("https://jsonplaceholder.typicode.com/");
            o.UseDefaultUserAgent = false;
        });
    

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

    csharp
    public class ValuesController : ControllerBase
    {
        private readonly IJsonPlaceholderApi _jsonPlaceholderApi;
    
        public ValuesController(IJsonPlaceholderApi jsonPlaceholderApi)
        {
            _jsonPlaceholderApi = jsonPlaceholderApi;
        }
        
        [HttpGet("api")]
        public async Task<dynamic> GetApi()
        {
            var post = await _jsonPlaceholderApi.GetPost(1);
    
            return post;
        }
    }
    

    HttpClient设计原理

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

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

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

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

    csharp
    public class HttpMessageInvoker : IDisposable
    {
        private volatile bool _disposed;
        private readonly bool _disposeHandler;
        private readonly HttpMessageHandler _handler;
    
        public HttpMessageInvoker(HttpMessageHandler handler) : this(handler, true) { }
    
        public HttpMessageInvoker(HttpMessageHandler handler, bool disposeHandler)
        {
            _handler = handler;
            _disposeHandler = disposeHandler;
        }
    
        [UnsupportedOSPlatformAttribute("browser")]
        public virtual HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) =>
            _handler.Send(request, cancellationToken);
    
        public virtual Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
            _handler.SendAsync(request, cancellationToken);
    
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    
        protected virtual void Dispose(bool disposing)
        {
            if (disposing && !_disposed)
            {
                _disposed = true;
    
                if (_disposeHandler)
                {
                    _handler.Dispose();
                }
            }
        }
    }
    
    public class HttpClient : HttpMessageInvoker
    {
        private const HttpCompletionOption DefaultCompletionOption = HttpCompletionOption.ResponseContentRead;
    
        private volatile bool _disposed;
        private int _maxResponseContentBufferSize;
    
        public HttpClient() : this(new HttpClientHandler()) { }
    
        public HttpClient(HttpMessageHandler handler) : this(handler, true) { }
    
        public HttpClient(HttpMessageHandler handler, bool disposeHandler) : base(handler, disposeHandler)  =>
            _maxResponseContentBufferSize = HttpContent.MaxBufferSize;
    
        // 中间的Rest方法就略过了,因为它们的内部都是通过调用 SendAsync 实现的
    
        // 同步的 Send 方法与异步的 SendAsync 实现类似
        public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request) =>
            SendAsync(request, DefaultCompletionOption, CancellationToken.None);
    
        public override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
            SendAsync(request, DefaultCompletionOption, cancellationToken);
    
        public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption) =>
            SendAsync(request, completionOption, CancellationToken.None);
    
        public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
        {
            var response = await base.SendAsync(request, cts.Token).ConfigureAwait(false);
            ThrowForNullResponse(response);
    
            if (ShouldBufferResponse(completionOption, request))
            {
                await response.Content.LoadIntoBufferAsync(_maxResponseContentBufferSize, cts.Token).ConfigureAwait(false);
            }
    
            return response;
        }
    
        private static void ThrowForNullResponse(HttpResponseMessage? response)
        {
            if (response is null) throw new InvalidOperationException(...);
        }
    
        private static bool ShouldBufferResponse(HttpCompletionOption completionOption, HttpRequestMessage request) =>
            completionOption == HttpCompletionOption.ResponseContentRead 
            && !string.Equals(request.Method.Method, "HEAD", StringComparison.OrdinalIgnoreCase);
    
        protected override void Dispose(bool disposing)
        {
            if (disposing && !_disposed)
            {
                _disposed = true;
    
                // ...
            }
    
            base.Dispose(disposing);
        }
    }
    

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

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

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

    下面看一下HttpMessageHandler及其子类HttpClientHandler

    csharp
    public abstract class HttpMessageHandler : IDisposable
    {
        protected HttpMessageHandler() { }
    
        // 这个方法是后加的,为了不影响它的已存在的子类,所以将其设置为了virtual(而不是abstract),并默认抛NSE
        protected internal virtual HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            throw new NotSupportedException(...);
        }
    
        protected internal abstract Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
    
        protected virtual void Dispose(bool disposing)
        {
            // 基类中啥都没干
        }
    
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
    
    // 这里我们不讨论作为WASM运行在浏览器中的情况
    public class HttpClientHandler : HttpMessageHandler
    {
        // Socket
        private readonly SocketsHttpHandler _underlyingHandler;
        
        private volatile bool _disposed;
    
        public HttpClientHandler()
        {
            _underlyingHandler = new HttpHandlerType();
            ClientCertificateOptions = ClientCertificateOption.Manual;
        }
        
        private HttpMessageHandler Handler => _underlyingHandler;
        
        // Send 与 SendAsync 类似
        protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
            Handler.SendAsync(request, cancellationToken);
        
        protected override void Dispose(bool disposing)
        {
            if (disposing && !_disposed)
            {
                _disposed = true;
                _underlyingHandler.Dispose();
            }
    
            base.Dispose(disposing);
        }
    }
    

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

    • 更高的性能
    • 消除了平台依赖,简化了部署和服务
    • 在所有的.NET平台上行为一致
    csharp
    [UnsupportedOSPlatform("browser")]
    public sealed class SocketsHttpHandler : HttpMessageHandler
    {
        private readonly HttpConnectionSettings _settings = new HttpConnectionSettings();
        private HttpMessageHandlerStage? _handler;
        private bool _disposed;
        
        // Send 与 SendAsync 类似
        protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            HttpMessageHandler handler = _handler ?? SetupHandlerChain();
            return handler.SendAsync(request, cancellationToken);
        }
        
        private HttpMessageHandlerStage SetupHandlerChain()
        {
            HttpConnectionSettings settings = _settings.CloneAndNormalize();
            HttpConnectionPoolManager poolManager = new HttpConnectionPoolManager(settings);
            HttpMessageHandlerStage handler;
    
            if (settings._credentials == null)
            {
                handler = new HttpConnectionHandler(poolManager);
            }
            else
            {
                handler = new HttpAuthenticatedConnectionHandler(poolManager);
            }
    
            // 省略了一些Handlers管道的组装,与中间件管道类似
    
            // 释放旧的 _handler
            if (Interlocked.CompareExchange(ref _handler, handler, null) != null)
            {
                handler.Dispose();
            }
    
            return _handler;
        }
        
        protected override void Dispose(bool disposing)
        {
            if (disposing && !_disposed)
            {
                _disposed = true;
                _handler?.Dispose();
            }
    
            base.Dispose(disposing);
        }
    }
    
    // HttpAuthenticatedConnectionHandler 结构类似
    internal sealed class HttpConnectionHandler : HttpMessageHandlerStage
    {
        // Http连接池管理器
        private readonly HttpConnectionPoolManager _poolManager;
    
        public HttpConnectionHandler(HttpConnectionPoolManager poolManager) =>
            _poolManager = poolManager;
    
        internal override ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) =>
            _poolManager.SendAsync(request, async, doRequestAuth: false, cancellationToken);
    
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                _poolManager.Dispose();
            }
    
            base.Dispose(disposing);
        }
    }
    

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

    IHttpClientFactory设计方式

    我们先从服务注册看起:

    csharp
    public static class HttpClientFactoryServiceCollectionExtensions
    {
        public static IServiceCollection AddHttpClient(this IServiceCollection services)
        {
            services.AddLogging();
            services.AddOptions();
    
            // 核心服务
            services.TryAddTransient<HttpMessageHandlerBuilder, DefaultHttpMessageHandlerBuilder>();
            services.TryAddSingleton<DefaultHttpClientFactory>();
            services.TryAddSingleton<IHttpClientFactory>(serviceProvider => serviceProvider.GetRequiredService<DefaultHttpClientFactory>());
            services.TryAddSingleton<IHttpMessageHandlerFactory>(serviceProvider => serviceProvider.GetRequiredService<DefaultHttpClientFactory>());
            
            // 类型化客户端服务
            services.TryAdd(ServiceDescriptor.Transient(typeof(ITypedHttpClientFactory<>), typeof(DefaultTypedHttpClientFactory<>)));
            services.TryAdd(ServiceDescriptor.Singleton(typeof(DefaultTypedHttpClientFactory<>.Cache), typeof(DefaultTypedHttpClientFactory<>.Cache)));
    
            services.TryAddEnumerable(ServiceDescriptor.Singleton<IHttpMessageHandlerBuilderFilter, LoggingHttpMessageHandlerBuilderFilter>());
    
            services.TryAddSingleton(new HttpClientMappingRegistry());
    
            // 默认注册一个名字为空字符串的 HttpClient 实例
            services.TryAddTransient(s => s.GetRequiredService<IHttpClientFactory>().CreateClient(string.Empty));
    
            return services;
        }
    
        public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, string name)
        {
            AddHttpClient(services);
    
            // 返回一个Builder,以允许继续针对HttpClient进行配置
            return new DefaultHttpClientBuilder(services, name);
        }
        
        public static IHttpClientBuilder AddHttpClient<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TClient>(
            this IServiceCollection services)
            where TClient : class
        {
            AddHttpClient(services);
    
            // 获取类型名作为客户端名
            string name = TypeNameHelper.GetTypeDisplayName(typeof(TClient), fullName: false);
            var builder = new DefaultHttpClientBuilder(services, name);
            // 目的是通过 ActivatorUtilities 动态创建 TClient 实例,并通过构造函数注入 HttpClient
            builder.AddTypedClientCore<TClient>(validateSingleType: true);
            return builder;
        }
    }
    

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

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

    IHttpClientFactory接口的默认实现是DefaultHttpClientFactory

    csharp
    internal class DefaultHttpClientFactory : IHttpClientFactory, IHttpMessageHandlerFactory
    {
        private readonly IServiceProvider _services;
        private readonly Func<string, Lazy<ActiveHandlerTrackingEntry>> _entryFactory;
    
        // 有效的Handler对象池,使用Lazy来保证每个命名客户端具有唯一的 HttpMessageHandler 实例
        internal readonly ConcurrentDictionary<string, Lazy<ActiveHandlerTrackingEntry>> _activeHandlers;
        // 过期的Handler集合
        internal readonly ConcurrentQueue<ExpiredHandlerTrackingEntry> _expiredHandlers;
    
        public DefaultHttpClientFactory(
            IServiceProvider services,
            IServiceScopeFactory scopeFactory,
            ILoggerFactory loggerFactory,
            IOptionsMonitor<HttpClientFactoryOptions> optionsMonitor,
            IEnumerable<IHttpMessageHandlerBuilderFilter> filters)
        {
            _services = services;
            _activeHandlers = new ConcurrentDictionary<string, Lazy<ActiveHandlerTrackingEntry>>(StringComparer.Ordinal);
            _entryFactory = (name) =>
            {
                return new Lazy<ActiveHandlerTrackingEntry>(() =>
                {
                    return CreateHandlerEntry(name);
                }, LazyThreadSafetyMode.ExecutionAndPublication);
            };
        }
    
        public HttpClient CreateClient(string name)
        {
            HttpMessageHandler handler = CreateHandler(name);
            return new HttpClient(handler, disposeHandler: false);
        }
    
        public HttpMessageHandler CreateHandler(string name)
        {
            // 若存在指定的命名客户端的活跃的Handler,则直接使用,若不存在,则新建一个
            ActiveHandlerTrackingEntry entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;
            return entry.Handler;
        }
        
        internal ActiveHandlerTrackingEntry CreateHandlerEntry(string name)
        {
            HttpMessageHandlerBuilder builder = _services.GetRequiredService<HttpMessageHandlerBuilder>();
            builder.Name = name;
    
            var handler = new LifetimeTrackingHttpMessageHandler(builder.Build());
    
            // options.HandlerLifetime 默认2分钟
            return new ActiveHandlerTrackingEntry(name, handler, scope, options.HandlerLifetime);
        }
    }
    
    public static class HttpClientFactoryExtensions
    {
        public static HttpClient CreateClient(this IHttpClientFactory factory) =>
            factory.CreateClient(Options.DefaultName);  // 名字为 string.Empty
    }
    

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

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

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

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

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

    csharp
    public abstract class DelegatingHandler : HttpMessageHandler
    {
        private HttpMessageHandler? _innerHandler;
        private volatile bool _disposed;
    
        [DisallowNull]
        public HttpMessageHandler? InnerHandler
        {
            get => _innerHandler;
            set => _innerHandler = value;
        }
    
        protected DelegatingHandler() { }
    
        // 这里接收的innerHandler就是负责发送和处理Http请求的
        protected DelegatingHandler(HttpMessageHandler innerHandler) =>
            InnerHandler = innerHandler;
    
        protected internal override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) =>
            _innerHandler!.Send(request, cancellationToken);
    
        protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
            _innerHandler!.SendAsync(request, cancellationToken);
    
        protected override void Dispose(bool disposing)
        {
            if (disposing && !_disposed)
            {
                _disposed = true;
                if (_innerHandler != null)
                {
                    _innerHandler.Dispose();
                }
            }
    
            base.Dispose(disposing);
        }
    }
    

    这里我们看到的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的创建

    __EOF__

  • 本文作者: xiaoxiaotank
  • 本文链接: https://www.cnblogs.com/xiaoxiaotank/p/16273773.html
  • 关于博主: 使用微信扫描一下左侧的二维码关注我的订阅号,或加入我的QQ交流群:705680556
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    LeetCode 73. 矩阵置零(java实现)
    强化学习论文分析3---蜂窝网络联合频谱和功率分配的深度强化学习--《Deep Reinforcement Learning for ......》
    聊聊HttpClient的DnsResolver
    centos统计子目录和文件的大小并排序
    LDR6328Q,快充界的黑马
    基于ssm的智慧农贸信息化管理平台-计算机毕业设计源码
    第三章:最新版零基础学习 PYTHON 教程(第十五节 - Python 运算符—Python 成员身份和身份运算符)
    ChatGPT王炸升级
    大学物理---质点运动学
    vue3 项目搭建教程整理
  • 原文地址:https://www.cnblogs.com/xiaoxiaotank/p/16273773.html