上篇文章.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。html
相信你們都已經清楚使用HttpClientFactory從services.AddHttpClient()注入相關類開始,咱們就從這裏開始入手。先貼上源碼地址HttpClientFactoryServiceCollectionExtensions源碼而後咱們大概的看一下咱們關注的實現方法,大體以下,代碼有刪減git
/// <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源碼地址找到了咱們熟悉的名字😄😄😄github
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就能夠了,說時遲那時快。動手寫了一個以下實現負載均衡
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實例,大體修改註冊的地方以下。async
public void ConfigureServices(IServiceCollection services) { services.AddTransient<ConsulDiscoveryHttpClientHandler>(); services.AddHttpClient().ConfigurePrimaryHttpMessageHandler<ConsulDiscoveryHttpClientHandler>();; }
接下來咱們來看看ConfigurePrimaryHttpMessageHandler這個擴展方法到底作了什麼,該方法來自HttpClientBuilderExtensions擴展類具體實現以下ide
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的方式。本文可能重在講思路,具體的實現方式可能不夠精細 。其中還涉及到了部分框架源碼,不熟悉源碼的話可能某些地方不是很好理解,再加上本人文筆不足,若是帶來閱讀不便敬請諒解。主要仍是想把本身的理解和思路轉達給你們,望批評指導,以便後期改正。