.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在这里我看到了熟悉的身影
找到这里内心一阵澎湃,也就是说只要把我实现的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的方式。本文可能重在讲思路,具体的实现方式可能不够精细 。其中还涉及到了部分框架源码,不熟悉源码的话可能某些地方不是很好理解,再加上本人文笔不足,如果带来阅读不便敬请谅解。主要还是想把自己的理解和思路转达给大家,望批评指导,以便后期改正。