diff --git a/FastGithub.Http/HttpClient.cs b/FastGithub.Http/HttpClient.cs index 910fb19..07074bd 100644 --- a/FastGithub.Http/HttpClient.cs +++ b/FastGithub.Http/HttpClient.cs @@ -32,7 +32,7 @@ namespace FastGithub.Http /// /// /// - internal HttpClient(HttpClientHandler handler, bool disposeHandler) + public HttpClient(HttpMessageHandler handler, bool disposeHandler) : base(handler, disposeHandler) { } diff --git a/FastGithub.Http/HttpClientFactory.cs b/FastGithub.Http/HttpClientFactory.cs index 796bcbf..01ed721 100644 --- a/FastGithub.Http/HttpClientFactory.cs +++ b/FastGithub.Http/HttpClientFactory.cs @@ -1,6 +1,6 @@ using FastGithub.Configuration; using FastGithub.DomainResolve; -using Microsoft.Extensions.Options; +using System; using System.Collections.Concurrent; namespace FastGithub.Http @@ -11,19 +11,29 @@ namespace FastGithub.Http sealed class HttpClientFactory : IHttpClientFactory { private readonly IDomainResolver domainResolver; - private ConcurrentDictionary domainHandlers = new(); + + /// + /// httpHandler的生命周期 + /// + private readonly TimeSpan lifeTime = TimeSpan.FromMinutes(2d); + + /// + /// HttpHandler清理器 + /// + private readonly LifetimeHttpHandlerCleaner httpHandlerCleaner = new(); + + /// + /// LazyOf(LifetimeHttpHandler)缓存 + /// + private readonly ConcurrentDictionary> httpHandlerLazyCache = new(); /// /// HttpClient工厂 /// /// - /// - public HttpClientFactory( - IDomainResolver domainResolver, - IOptionsMonitor options) + public HttpClientFactory(IDomainResolver domainResolver) { this.domainResolver = domainResolver; - options.OnChange(opt => this.domainHandlers = new()); } /// @@ -33,8 +43,41 @@ namespace FastGithub.Http /// public HttpClient CreateHttpClient(DomainConfig domainConfig) { - var httpClientHandler = this.domainHandlers.GetOrAdd(domainConfig, config => new HttpClientHandler(config, this.domainResolver)); - return new HttpClient(httpClientHandler, disposeHandler: false); + var lifetimeHttpHandlerLazy = this.httpHandlerLazyCache.GetOrAdd(domainConfig, this.CreateLifetimeHttpHandlerLazy); + var lifetimeHttpHandler = lifetimeHttpHandlerLazy.Value; + return new HttpClient(lifetimeHttpHandler, disposeHandler: false); + } + + /// + /// 创建LazyOf(LifetimeHttpHandler) + /// + /// + /// + private Lazy CreateLifetimeHttpHandlerLazy(DomainConfig domainConfig) + { + return new Lazy(() => this.CreateLifetimeHttpHandler(domainConfig), true); + } + + /// + /// 创建LifetimeHttpHandler + /// + /// + private LifetimeHttpHandler CreateLifetimeHttpHandler(DomainConfig domainConfig) + { + var httpClientHandler = new HttpClientHandler(domainConfig, this.domainResolver); + return new LifetimeHttpHandler(httpClientHandler, this.lifeTime, this.OnLifetimeHttpHandlerDeactivate); + } + + /// + /// 当有httpHandler失效时 + /// + /// httpHandler + private void OnLifetimeHttpHandlerDeactivate(LifetimeHttpHandler lifetimeHttpHandler) + { + // 切换激活状态的记录的实例 + var domainConfig = ((HttpClientHandler)lifetimeHttpHandler.InnerHandler!).DomainConfig; + this.httpHandlerLazyCache[domainConfig] = this.CreateLifetimeHttpHandlerLazy(domainConfig); + this.httpHandlerCleaner.Add(lifetimeHttpHandler); } } } \ No newline at end of file diff --git a/FastGithub.Http/HttpClientHandler.cs b/FastGithub.Http/HttpClientHandler.cs index 97ef818..2b23dab 100644 --- a/FastGithub.Http/HttpClientHandler.cs +++ b/FastGithub.Http/HttpClientHandler.cs @@ -21,10 +21,14 @@ namespace FastGithub.Http /// class HttpClientHandler : DelegatingHandler { - private readonly DomainConfig domainConfig; private readonly IDomainResolver domainResolver; private readonly TimeSpan connectTimeout = TimeSpan.FromSeconds(10d); + /// + /// 获取域名配置 + /// + public DomainConfig DomainConfig { get; } + /// /// HttpClientHandler /// @@ -33,7 +37,7 @@ namespace FastGithub.Http public HttpClientHandler(DomainConfig domainConfig, IDomainResolver domainResolver) { this.domainResolver = domainResolver; - this.domainConfig = domainConfig; + this.DomainConfig = domainConfig; this.InnerHandler = this.CreateSocketsHttpHandler(); } @@ -53,23 +57,16 @@ namespace FastGithub.Http // 请求上下文信息 var isHttps = uri.Scheme == Uri.UriSchemeHttps; - var tlsSniValue = this.domainConfig.GetTlsSniPattern().WithDomain(uri.Host).WithRandom(); + var tlsSniValue = this.DomainConfig.GetTlsSniPattern().WithDomain(uri.Host).WithRandom(); request.SetRequestContext(new RequestContext(isHttps, tlsSniValue)); - // 设置请求头host,修改协议为http,使用ip取代域名 - var address = await this.domainResolver.ResolveAnyAsync(uri.Host, cancellationToken); - var uriBuilder = new UriBuilder(uri) - { - Scheme = Uri.UriSchemeHttp, - Host = address.ToString() - }; - + // 设置请求头host,修改协议为http request.Headers.Host = uri.Host; - request.RequestUri = uriBuilder.Uri; + request.RequestUri = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp }.Uri; - if (this.domainConfig.Timeout != null) + if (this.DomainConfig.Timeout != null) { - using var timeoutTokenSource = new CancellationTokenSource(this.domainConfig.Timeout.Value); + using var timeoutTokenSource = new CancellationTokenSource(this.DomainConfig.Timeout.Value); using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token); return await base.SendAsync(request, linkedTokenSource.Token); } @@ -102,8 +99,7 @@ namespace FastGithub.Http private async ValueTask ConnectCallback(SocketsHttpConnectionContext context, CancellationToken cancellationToken) { var innerExceptions = new List(); - var dnsEndPoint = new DnsEndPoint(context.InitialRequestMessage.Headers.Host!, context.DnsEndPoint.Port); - var ipEndPoints = this.GetIPEndPointsAsync(dnsEndPoint, cancellationToken); + var ipEndPoints = this.GetIPEndPointsAsync(context.DnsEndPoint, cancellationToken); await foreach (var ipEndPoint in ipEndPoints) { @@ -161,12 +157,12 @@ namespace FastGithub.Http { if (errors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch)) { - if (this.domainConfig.TlsIgnoreNameMismatch == true) + if (this.DomainConfig.TlsIgnoreNameMismatch == true) { return true; } - var domain = context.InitialRequestMessage.Headers.Host!; + var domain = context.DnsEndPoint.Host; var dnsNames = ReadDnsNames(cert); return dnsNames.Any(dns => IsMatch(dns, domain)); } @@ -183,7 +179,7 @@ namespace FastGithub.Http /// private async IAsyncEnumerable GetIPEndPointsAsync(DnsEndPoint dnsEndPoint, [EnumeratorCancellation] CancellationToken cancellationToken) { - if (IPAddress.TryParse(this.domainConfig.IPAddress, out var address) || + if (IPAddress.TryParse(this.DomainConfig.IPAddress, out var address) || IPAddress.TryParse(dnsEndPoint.Host, out address)) { yield return new IPEndPoint(address, dnsEndPoint.Port); diff --git a/FastGithub.Http/LifetimeHttpHandler.cs b/FastGithub.Http/LifetimeHttpHandler.cs new file mode 100644 index 0000000..3bd6699 --- /dev/null +++ b/FastGithub.Http/LifetimeHttpHandler.cs @@ -0,0 +1,44 @@ +using System; +using System.Net.Http; +using System.Threading; + +namespace FastGithub.Http +{ + /// + /// 表示自主管理生命周期的的HttpMessageHandler + /// + sealed class LifetimeHttpHandler : DelegatingHandler + { + private readonly Timer timer; + + /// + /// 具有生命周期的HttpHandler + /// + /// HttpHandler + /// 拦截器的生命周期 + /// 失效回调 + public LifetimeHttpHandler(HttpMessageHandler handler, TimeSpan lifeTime, Action deactivateAction) + : base(handler) + { + this.timer = new Timer(this.OnTimerCallback, deactivateAction, lifeTime, Timeout.InfiniteTimeSpan); + } + + /// + /// timer触发时 + /// + /// + private void OnTimerCallback(object? state) + { + this.timer.Dispose(); + ((Action)(state!))(this); + } + + /// + /// 这里不释放资源 + /// + /// + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/FastGithub.Http/LifetimeHttpHandlerCleaner.cs b/FastGithub.Http/LifetimeHttpHandlerCleaner.cs new file mode 100644 index 0000000..b77fe0d --- /dev/null +++ b/FastGithub.Http/LifetimeHttpHandlerCleaner.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.Http +{ + /// + /// 表示LifetimeHttpHandler清理器 + /// + sealed class LifetimeHttpHandlerCleaner + { + /// + /// 当前监视生命周期的记录的数量 + /// + private int trackingEntryCount = 0; + + /// + /// 监视生命周期的记录队列 + /// + private readonly ConcurrentQueue trackingEntries = new(); + + /// + /// 获取或设置清理的时间间隔 + /// 默认10s + /// + public TimeSpan CleanupInterval { get; set; } = TimeSpan.FromSeconds(10d); + + /// + /// 添加要清除的httpHandler + /// + /// httpHandler + public void Add(LifetimeHttpHandler handler) + { + var entry = new TrackingEntry(handler); + this.trackingEntries.Enqueue(entry); + + // 从0变为1,要启动清理作业 + if (Interlocked.Increment(ref this.trackingEntryCount) == 1) + { + this.StartCleanup(); + } + } + + /// + /// 启动清理作业 + /// + private async void StartCleanup() + { + while (this.Cleanup() == false) + { + await Task.Delay(this.CleanupInterval); + } + } + + /// + /// 清理失效的拦截器 + /// 返回是否完全清理 + /// + /// + private bool Cleanup() + { + var cleanCount = this.trackingEntries.Count; + for (var i = 0; i < cleanCount; i++) + { + this.trackingEntries.TryDequeue(out var entry); + Debug.Assert(entry != null); + + if (entry.CanDispose == false) + { + this.trackingEntries.Enqueue(entry); + continue; + } + + entry.Dispose(); + if (Interlocked.Decrement(ref this.trackingEntryCount) == 0) + { + return true; + } + } + return false; + } + + + /// + /// 表示监视生命周期的记录 + /// + private class TrackingEntry : IDisposable + { + /// + /// 用于释放资源的对象 + /// + private readonly IDisposable disposable; + + /// + /// 监视对象的弱引用 + /// + private readonly WeakReference weakReference; + + /// + /// 获取是否可以释放资源 + /// + /// + public bool CanDispose => this.weakReference.IsAlive == false; + + /// + /// 监视生命周期的记录 + /// + /// 激活状态的httpHandler + public TrackingEntry(LifetimeHttpHandler handler) + { + this.disposable = handler.InnerHandler!; + this.weakReference = new WeakReference(handler); + } + + /// + /// 释放资源 + /// + public void Dispose() + { + try + { + this.disposable.Dispose(); + } + catch (Exception) + { + } + } + } + } +}