前言
上篇文章.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在這裡我看到了熟悉的身影

找到這裡内心一陣澎湃,也就是說隻要把我實作的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的方式。本文可能重在講思路,具體的實作方式可能不夠精細 。其中還涉及到了部分架構源碼,不熟悉源碼的話可能某些地方不是很好了解,再加上本人文筆不足,如果帶來閱讀不便敬請諒解。主要還是想把自己的了解和思路轉達給大家,望批評指導,以便後期改正。
👇歡迎掃碼關注👇