前言
上篇文章.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的方式。本文可能重在講思路,具體的實現方式可能不夠精細 。其中還涉及到了部分框架原始碼,不熟悉原始碼的話可能某些地方不是很好理解,再加上本人文筆不足,如果帶來閱讀不便敬請諒解。主要還是想把自己的理解和思路轉達給大家,望批評指導,以便後期改正。