天天看點

.NET Core HttpClientFactory+Consul實作服務發現

前言

  上篇文章.NET Core HttpClient+Consul實作服務發現提到過,HttpClient存在套接字延遲釋放的問題,高并發情況導緻端口号被耗盡引起伺服器拒絕服務的問題。好在微軟意識到了這個問題,從.NET Core 2.1版本開始推出了HttpClientFactory來彌補這個問題。關于更詳細的HttpClientFactory介紹可以檢視微軟官方文檔 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-3.1#httpclient-and-lifetime-management 我們了解到想把自定義的HttpMessageHandler注入到HttpClient内部,必須要通過構造函數。接下來我們就慢慢發覺如何給HttpClientFactory使用我們自定義的Handler。

HttpClient的建立

  相信大家都已經清楚使用HttpClientFactory從services.AddHttpClient()注入相關類開始,我們就從這裡開始入手。先貼上源碼位址HttpClientFactoryServiceCollectionExtensions源碼然後我們大概的看一下我們關注的實作方法,大緻如下,代碼有删減

/// <summary>     /// Adds the <see cref="IHttpClientFactory"/> and related services to the <see cref="IServiceCollection"/>.     /// </summary>     /// <param name="services">The <see cref="IServiceCollection"/>.</param>     /// <returns>The <see cref="IServiceCollection"/>.</returns>     public static IServiceCollection AddHttpClient(this IServiceCollection services)     {         if (services == null)         {             throw new ArgumentNullException(nameof(services));         }         .....         //         // Core abstractions         //         services.TryAddTransient<HttpMessageHandlerBuilder, DefaultHttpMessageHandlerBuilder>();         services.TryAddSingleton<DefaultHttpClientFactory>();         services.TryAddSingleton<IHttpClientFactory>(serviceProvider => serviceProvider.GetRequiredService<DefaultHttpClientFactory>());         services.TryAddSingleton<IHttpMessageHandlerFactory>(serviceProvider => serviceProvider.GetRequiredService<DefaultHttpClientFactory>());         .....         return services;     }           

通過源碼我們可以看到IHttpClientFactory的實作類注入其實是DefaultHttpClientFactory,拿我們繼續順着源碼繼續查找DefaultHttpClientFactory源碼位址找到了我們熟悉的名字😄😄😄

public HttpClient CreateClient(string name)     {         if (name == null)         {             throw new ArgumentNullException(nameof(name));         }         var handler = CreateHandler(name);         var client = new HttpClient(handler, disposeHandler: false);         var options = _optionsMonitor.Get(name);         for (var i = 0; i < options.HttpClientActions.Count; i++)         {             options.HttpClientActions[i](client);         }         return client;     }           

在這裡我們發現了CreateHandler方法由它建立了handler傳入了HttpClient,繼續向下看,發現這段代碼

public HttpMessageHandler CreateHandler(string name)     {         if (name == null)         {             throw new ArgumentNullException(nameof(name));         }         var entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;         StartHandlerEntryTimer(entry);         return entry.Handler;     }           

然後我們_entryFactory這個委托,然後一直找啊找

internal ActiveHandlerTrackingEntry CreateHandlerEntry(string name)     {         .....         try         {             var builder = services.GetRequiredService<HttpMessageHandlerBuilder>();             builder.Name = name;             Action<HttpMessageHandlerBuilder> configure = Configure;             for (var i = _filters.Length - 1; i >= 0; i--)             {                 configure = _filters[i].Configure(configure);             }             configure(builder);             var handler = new LifetimeTrackingHttpMessageHandler(builder.Build());             return new ActiveHandlerTrackingEntry(name, handler, scope, options.HandlerLifetime);             .....         }         catch         {             .....         }     }           

發現了HttpMessageHandlerBuilder的身影,由它建構了HttpMessageHandler,咦!好像在哪見過,恍然大悟原來是在AddHttpClient擴充方法裡

services.TryAddTransient<HttpMessageHandlerBuilder, DefaultHttpMessageHandlerBuilder>();           

然後找到了DefaultHttpMessageHandlerBuilder在這裡我看到了熟悉的身影

.NET Core HttpClientFactory+Consul實作服務發現

找到這裡内心一陣澎湃,也就是說隻要把我實作的HttpClientHandler替換掉預設的就好了,可是感覺無地方下手啊。這時突然想到DefaultHttpMessageHandlerBuilder這類是注冊進來的,那我自己實作一個ConsulHttpMessageHandlerBuilder替換掉預設注冊的DefaultHttpMessageHandlerBuilder就可以了,說時遲那時快。動手寫了一個如下實作

自定義HttpMessageHandlerBuilder

public class ConsulHttpMessageHandlerBuilder: HttpMessageHandlerBuilder     {         public ConsulHttpMessageHandlerBuilder(ConsulDiscoveryHttpClientHandler consulDiscoveryHttpClientHandler)         {             PrimaryHandler = consulDiscoveryHttpClientHandler;         }         private string _name;         public override IList<DelegatingHandler> AdditionalHandlers => new List<DelegatingHandler>();         public override string Name {             get => _name;             set             {                 if (value == null)                 {                     throw new ArgumentNullException(nameof(value));                 }                 _name = value;             }         }         public override HttpMessageHandler PrimaryHandler { get; set; }         public override HttpMessageHandler Build()         {             if (PrimaryHandler == null)             {                 throw new InvalidOperationException(nameof(PrimaryHandler));             }             return CreateHandlerPipeline(PrimaryHandler, AdditionalHandlers);         }     }           

相對于原來的代碼其實就變動了一點,就是用自己的ConsulDiscoveryHttpClientHandler替換了預設的HttpClientHandler,具體ConsulDiscoveryHttpClientHandler的實作可以參考上篇文章的實作。然後在注冊的地方,替換掉預設的DefaultHttpMessageHandlerBuilder。

public void ConfigureServices(IServiceCollection services)     {           services.AddHttpClient();           services.AddTransient<ConsulDiscoveryHttpClientHandler>();           services.Replace(new ServiceDescriptor(typeof(HttpMessageHandlerBuilder),typeof(ConsulHttpMessageHandlerBuilder),ServiceLifetime.Transient));     }           

試了下,沒毛病,心中暗喜了幾秒。但是冷靜下來想一想,感覺不是很合理還要自己寫Builder替換預設的方式。不符合開放封閉原則,對原有代碼本身的入侵比較大,似乎不是很合理。要不就說,學習一定要仔細,特别是剛開始的時候,能少踩好多坑。在微軟的幫助文檔裡已經提到了能通過IHttpClientBuilder的擴充方法可以用自定義的實作替換掉預設的PrimaryHandler執行個體,大緻修改注冊的地方如下。

public void ConfigureServices(IServiceCollection services)     {           services.AddTransient<ConsulDiscoveryHttpClientHandler>();           services.AddHttpClient().ConfigurePrimaryHttpMessageHandler<ConsulDiscoveryHttpClientHandler>();;     }           

HttpClientBuilderExtensions擴充類實作

接下來我們來看看ConfigurePrimaryHttpMessageHandler這個擴充方法到底做了什麼,該方法來自HttpClientBuilderExtensions擴充類具體實作如下

public static IHttpClientBuilder ConfigurePrimaryHttpMessageHandler<THandler>(this IHttpClientBuilder builder)                 where THandler : HttpMessageHandler     {         if (builder == null)         {             throw new ArgumentNullException(nameof(builder));         }         builder.Services.Configure<HttpClientFactoryOptions>(builder.Name, options =>         {             options.HttpMessageHandlerBuilderActions.Add(b => b.PrimaryHandler = b.Services.GetRequiredService<THandler>());         });         return builder;     }           

然後通過DefaultHttpClientFactory類的CreateHandlerEntry方法裡可以看到HttpClientFactoryOptions類的HttpMessageHandlerBuilderActions調用的的地方其實傳入的就死目前注冊到HttpMessageHandlerBuilder的DefaultHttpMessageHandlerBuilder,大緻調用代碼如下

internal ActiveHandlerTrackingEntry CreateHandlerEntry(string name)     {         .....         try         {             var builder = services.GetRequiredService<HttpMessageHandlerBuilder>();             builder.Name = name;             Action<HttpMessageHandlerBuilder> configure = Configure;             for (var i = _filters.Length - 1; i >= 0; i--)             {                 configure = _filters[i].Configure(configure);             }             configure(builder);             var handler = new LifetimeTrackingHttpMessageHandler(builder.Build());             return new ActiveHandlerTrackingEntry(name, handler, scope, options.HandlerLifetime);             void Configure(HttpMessageHandlerBuilder b)             {                 for (var i = 0; i < options.HttpMessageHandlerBuilderActions.Count; i++)                 {                     options.HttpMessageHandlerBuilderActions[i](b);                 }             }         }         catch         {             .....         }     }           

回頭來看HttpClientBuilderExtensions擴充類還有一個ConfigureHttpMessageHandlerBuilder擴充方法

public static IHttpClientBuilder AddHttpMessageHandler<THandler>(this IHttpClientBuilder builder)                 where THandler : DelegatingHandler     {         if (builder == null)         {             throw new ArgumentNullException(nameof(builder));         }         builder.Services.Configure<HttpClientFactoryOptions>(builder.Name, options =>         {             options.HttpMessageHandlerBuilderActions.Add(b => b.AdditionalHandlers.Add(b.Services.GetRequiredService<THandler>()));         });         return builder;     }           

這個是對DefaultHttpMessageHandlerBuilder附加的Handler做添加操作,那麼PrimaryHandler和AdditionalHandlers之間到底有什麼關系呢?我們回過頭來看一下DefaultHttpMessageHandlerBuilder類相關方法的具體實作,大緻代碼如下

public override HttpMessageHandler Build()     {         if (PrimaryHandler == null)         {             var message = Resources.FormatHttpMessageHandlerBuilder_PrimaryHandlerIsNull(nameof(PrimaryHandler));             throw new InvalidOperationException(message);         }             return CreateHandlerPipeline(PrimaryHandler, AdditionalHandlers);     }     protected internal static HttpMessageHandler CreateHandlerPipeline(HttpMessageHandler primaryHandler, IEnumerable<DelegatingHandler> additionalHandlers)     {         if (primaryHandler == null)         {             throw new ArgumentNullException(nameof(primaryHandler));         }         if (additionalHandlers == null)         {             throw new ArgumentNullException(nameof(additionalHandlers));         }         var additionalHandlersList = additionalHandlers as IReadOnlyList<DelegatingHandler> ?? additionalHandlers.ToArray();         var next = primaryHandler;         for (var i = additionalHandlersList.Count - 1; i >= 0; i--)         {             var handler = additionalHandlersList[i];             if (handler == null)             {                 var message = Resources.FormatHttpMessageHandlerBuilder_AdditionalHandlerIsNull(nameof(additionalHandlers));                 throw new InvalidOperationException(message);             }             if (handler.InnerHandler != null)             {                 var message = Resources.FormatHttpMessageHandlerBuilder_AdditionHandlerIsInvalid(                     nameof(DelegatingHandler.InnerHandler),                     nameof(DelegatingHandler),                     nameof(HttpMessageHandlerBuilder),                     Environment.NewLine,                     handler);                 throw new InvalidOperationException(message);             }             handler.InnerHandler = next;             next = handler;         }         return next;     }           

通過這段代碼可以看出原來是用PrimaryHandler和AdditionalHandlers集合建構了一個Handler執行管道,PrimaryHandler作為管道的最後執行點,附加管道按照代碼注入的順序執行。看到這裡相信你基本上對HttpClientFactory大緻的工作方式就有一定的認知。其實從編碼的角度上來講,除非有特殊要求,否則我們不會替換掉PrimaryHandler,隻需要将我們的Handler添加到AdditionalHandlers集合即可。

最終實作方式

通過上面的分析我們基本上可以動手實作一個最合理的實作方式了

public void ConfigureServices(IServiceCollection services)     {         //consul位址         services.AddConsul("http://localhost:8500/");         //HttpPClient查找名稱(建議使用服務注冊名稱)         services.AddHttpClient("PersonService", c =>         {             //服務注冊的名稱(建議和HttpPClient查找名稱一緻)             c.BaseAddress = new Uri("http://PersonService/");         }).AddHttpMessageHandler<ConsulDiscoveryDelegatingHandler>();     }           

AddConsul擴充方法

public static IServiceCollection AddConsul(this IServiceCollection services, string consulAddress)     {         services.AddTransient(provider => {             return new ConsulClient(x =>             {                 // consul 服務位址                 x.Address = new Uri(consulAddress);             });         });         //注冊自定義的DelegatingHandler         services.AddTransient<ConsulDiscoveryDelegatingHandler>();         return services;     }           

自定義的ConsulDiscoveryDelegatingHandler

public class ConsulDiscoveryDelegatingHandler : DelegatingHandler     {          private readonly ConsulClient _consulClient;          private readonly ILogger<ConsulDiscoveryDelegatingHandler> _logger;          public ConsulDiscoveryDelegatingHandler(ConsulClient consulClient,                 ILogger<ConsulDiscoveryDelegatingHandler> logger)          {             _consulClient = consulClient;              _logger = logger;          }          protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)          {               var current = request.RequestUri;               try               {                                 //調用的服務位址裡的域名(主機名)傳入發現的服務名稱即可                   request.RequestUri = new Uri($"{current.Scheme}://{LookupService(current.Host)}/{current.PathAndQuery}");                   return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);               }               catch (Exception e)               {                   _logger?.LogDebug(e, "Exception during SendAsync()");                   throw;               }               finally               {                   request.RequestUri = current;               }           }           private string LookupService(string serviceName)           {               var services = _consulClient.Catalog.Service(serviceName).Result.Response;               if (services != null && services.Any())               {                     //模拟負載均衡算法(随機擷取一個位址)                     int index = r.Next(services.Count());                     var service = services.ElementAt(index);                     return $"{service.ServiceAddress}:{service.ServicePort}");                }                return null;           }     }           

編寫PersonTestController測試代碼

public class PersonTestController : Controller     {         private readonly IHttpClientFactory _clientFactory;         public PersonTestController(IHttpClientFactory clientFactory)         {             _clientFactory = clientFactory;         }         public async Task<ActionResult<string>> GetPersonInfo(int personId)         {             var client = _clientFactory.CreateClient("PersonService");             var response = await client.GetAsync($"/Person/Get/{personId}");             var result = await response.Content.ReadAsStringAsync();             return result;         }     }           

總結

通過這兩篇文章,主要講解了HttpClientFactory和HttpClient結合Consul完成服務發現,個人更推薦在後續的開發和實踐中采用HttpClientFactory的方式。本文可能重在講思路,具體的實作方式可能不夠精細 。其中還涉及到了部分架構源碼,不熟悉源碼的話可能某些地方不是很好了解,再加上本人文筆不足,如果帶來閱讀不便敬請諒解。主要還是想把自己的了解和思路轉達給大家,望批評指導,以便後期改正。

👇歡迎掃碼關注👇

.NET Core HttpClientFactory+Consul實作服務發現

繼續閱讀