diff --git a/FastGithub.DomainResolve/DnsClient.cs b/FastGithub.DomainResolve/DnsClient.cs index 2d0807d..02527bc 100644 --- a/FastGithub.DomainResolve/DnsClient.cs +++ b/FastGithub.DomainResolve/DnsClient.cs @@ -30,7 +30,6 @@ namespace FastGithub.DomainResolve private readonly FastGithubConfig fastGithubConfig; private readonly ILogger logger; - private readonly ConcurrentDictionary domainIPAddressCollection = new(); private readonly ConcurrentDictionary semaphoreSlims = new(); private readonly IMemoryCache dnsCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly TimeSpan defaultEmptyTtl = TimeSpan.FromSeconds(30d); @@ -54,15 +53,6 @@ namespace FastGithub.DomainResolve this.logger = logger; } - /// - /// 预加载 - /// - /// 域名 - public void Prefetch(string domain) - { - this.domainIPAddressCollection.TryAdd(domain, new IPAddressCollection()); - } - /// /// 解析域名 /// @@ -70,51 +60,6 @@ namespace FastGithub.DomainResolve /// /// public async IAsyncEnumerable ResolveAsync(string domain, [EnumeratorCancellation] CancellationToken cancellationToken) - { - if (this.domainIPAddressCollection.TryGetValue(domain, out var collection) && collection.Count > 0) - { - foreach (var address in collection.ToArray()) - { - yield return address; - } - } - else - { - this.domainIPAddressCollection.TryAdd(domain, new IPAddressCollection()); - await foreach (var adddress in this.ResolveCoreAsync(domain, cancellationToken)) - { - yield return adddress; - } - } - } - - /// - /// 对所有域名所有IP进行ping测试 - /// - /// - /// - public async Task PingAllDomainsAsync(CancellationToken cancellationToken) - { - foreach (var keyValue in this.domainIPAddressCollection) - { - var domain = keyValue.Key; - var collection = keyValue.Value; - - await foreach (var address in this.ResolveCoreAsync(domain, cancellationToken)) - { - collection.Add(address); - } - await collection.PingAllAsync(); - } - } - - /// - /// 解析域名 - /// - /// 域名 - /// - /// - private async IAsyncEnumerable ResolveCoreAsync(string domain, [EnumeratorCancellation] CancellationToken cancellationToken) { var hashSet = new HashSet(); foreach (var dns in this.GetDnsServers()) diff --git a/FastGithub.DomainResolve/DomainResolveHostedService.cs b/FastGithub.DomainResolve/DomainResolveHostedService.cs index dfab162..e4b2db0 100644 --- a/FastGithub.DomainResolve/DomainResolveHostedService.cs +++ b/FastGithub.DomainResolve/DomainResolveHostedService.cs @@ -11,20 +11,20 @@ namespace FastGithub.DomainResolve sealed class DomainResolveHostedService : BackgroundService { private readonly DnscryptProxy dnscryptProxy; - private readonly DnsClient dnsClient; - private readonly TimeSpan pingPeriodTimeSpan = TimeSpan.FromSeconds(10d); + private readonly IDomainResolver domainResolver; + private readonly TimeSpan testPeriodTimeSpan = TimeSpan.FromSeconds (1d); /// /// 域名解析后台服务 /// /// - /// + /// public DomainResolveHostedService( DnscryptProxy dnscryptProxy, - DnsClient dnsClient) + IDomainResolver domainResolver) { this.dnscryptProxy = dnscryptProxy; - this.dnsClient = dnsClient; + this.domainResolver = domainResolver; } /// @@ -37,8 +37,8 @@ namespace FastGithub.DomainResolve await this.dnscryptProxy.StartAsync(stoppingToken); while (stoppingToken.IsCancellationRequested == false) { - await this.dnsClient.PingAllDomainsAsync(stoppingToken); - await Task.Delay(this.pingPeriodTimeSpan, stoppingToken); + await this.domainResolver.TestAllEndPointsAsync(stoppingToken); + await Task.Delay(this.testPeriodTimeSpan, stoppingToken); } } diff --git a/FastGithub.DomainResolve/DomainResolver.cs b/FastGithub.DomainResolve/DomainResolver.cs index e65fb14..b376939 100644 --- a/FastGithub.DomainResolve/DomainResolver.cs +++ b/FastGithub.DomainResolve/DomainResolver.cs @@ -1,6 +1,12 @@ using FastGithub.Configuration; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using System.Net; +using System.Net.Sockets; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -12,6 +18,7 @@ namespace FastGithub.DomainResolve sealed class DomainResolver : IDomainResolver { private readonly DnsClient dnsClient; + private readonly ConcurrentDictionary dnsEndPointAddressTestResult = new(); /// /// 域名解析器 @@ -23,38 +30,117 @@ namespace FastGithub.DomainResolve } /// - /// 载加载 + /// 预加载 /// /// 域名 public void Prefetch(string domain) { - this.dnsClient.Prefetch(domain); + var endPoint = new DnsEndPoint(domain, 443); + this.dnsEndPointAddressTestResult.TryAdd(endPoint, IPAddressTestResult.Empty); + } + + /// + /// 对所有节点进行测速 + /// + /// + /// + public async Task TestAllEndPointsAsync(CancellationToken cancellationToken) + { + foreach (var keyValue in this.dnsEndPointAddressTestResult) + { + if (keyValue.Value.IsEmpty || keyValue.Value.IsExpired) + { + var dnsEndPoint = keyValue.Key; + var addresses = new List(); + await foreach (var adddress in this.dnsClient.ResolveAsync(dnsEndPoint.Host, cancellationToken)) + { + addresses.Add(adddress); + } + + var addressTestResult = IPAddressTestResult.Empty; + if (addresses.Count == 1) + { + var addressElapseds = new[] { new IPAddressElapsed(addresses[0], TimeSpan.Zero) }; + addressTestResult = new IPAddressTestResult(addressElapseds); + } + else if (addresses.Count > 1) + { + var tasks = addresses.Select(item => GetIPAddressElapsedAsync(item, dnsEndPoint.Port, cancellationToken)); + var addressElapseds = await Task.WhenAll(tasks); + addressTestResult = new IPAddressTestResult(addressElapseds); + } + this.dnsEndPointAddressTestResult[dnsEndPoint] = addressTestResult; + } + } + } + + /// + /// 获取连接耗时 + /// + /// + /// + /// + /// + private static async Task GetIPAddressElapsedAsync(IPAddress address, int port, CancellationToken cancellationToken) + { + var stopWatch = Stopwatch.StartNew(); + try + { + using var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(10d)); + using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token); + using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); + await socket.ConnectAsync(address, port, linkedTokenSource.Token); + return new IPAddressElapsed(address, stopWatch.Elapsed); + } + catch (Exception) + { + cancellationToken.ThrowIfCancellationRequested(); + return new IPAddressElapsed(address, TimeSpan.MaxValue); + } + finally + { + stopWatch.Stop(); + } } /// /// 解析ip /// - /// 域名 + /// 节点 /// /// - public async Task ResolveAnyAsync(string domain, CancellationToken cancellationToken = default) + public async Task ResolveAnyAsync(DnsEndPoint endPoint, CancellationToken cancellationToken = default) { - await foreach (var address in this.ResolveAllAsync(domain, cancellationToken)) + await foreach (var address in this.ResolveAllAsync(endPoint, cancellationToken)) { return address; } - throw new FastGithubException($"解析不到{domain}的IP"); + throw new FastGithubException($"解析不到{endPoint.Host}的IP"); } /// /// 解析域名 /// - /// 域名 + /// 节点 /// /// - public IAsyncEnumerable ResolveAllAsync(string domain, CancellationToken cancellationToken) + public async IAsyncEnumerable ResolveAllAsync(DnsEndPoint endPoint, [EnumeratorCancellation] CancellationToken cancellationToken) { - return this.dnsClient.ResolveAsync(domain, cancellationToken); + if (this.dnsEndPointAddressTestResult.TryGetValue(endPoint, out var speedTestResult) && speedTestResult.IsEmpty == false) + { + foreach (var addressElapsed in speedTestResult.AddressElapseds) + { + yield return addressElapsed.Adddress; + } + } + else + { + this.dnsEndPointAddressTestResult.TryAdd(endPoint, IPAddressTestResult.Empty); + await foreach (var adddress in this.dnsClient.ResolveAsync(endPoint.Host, cancellationToken)) + { + yield return adddress; + } + } } } } diff --git a/FastGithub.DomainResolve/IDomainResolver.cs b/FastGithub.DomainResolve/IDomainResolver.cs index 4a1f4a7..45037db 100644 --- a/FastGithub.DomainResolve/IDomainResolver.cs +++ b/FastGithub.DomainResolve/IDomainResolver.cs @@ -11,25 +11,32 @@ namespace FastGithub.DomainResolve public interface IDomainResolver { /// - /// 载加载 + /// 预加载 /// /// 域名 void Prefetch(string domain); /// - /// 解析ip + /// 对所有节点进行测速 /// - /// 域名 /// /// - Task ResolveAnyAsync(string domain, CancellationToken cancellationToken = default); + Task TestAllEndPointsAsync(CancellationToken cancellationToken); + + /// + /// 解析ip + /// + /// 节点 + /// + /// + Task ResolveAnyAsync(DnsEndPoint endPoint, CancellationToken cancellationToken = default); /// /// 解析所有ip /// - /// 域名 + /// 节点 /// /// - IAsyncEnumerable ResolveAllAsync(string domain, CancellationToken cancellationToken = default); + IAsyncEnumerable ResolveAllAsync(DnsEndPoint endPoint, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/FastGithub.DomainResolve/IPAddressCollection.cs b/FastGithub.DomainResolve/IPAddressCollection.cs deleted file mode 100644 index c6fbac3..0000000 --- a/FastGithub.DomainResolve/IPAddressCollection.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Net; -using System.Net.NetworkInformation; -using System.Threading.Tasks; - -namespace FastGithub.DomainResolve -{ - /// - /// IPAddress集合 - /// - [DebuggerDisplay("Count = {Count}")] - sealed class IPAddressCollection - { - private readonly object syncRoot = new(); - private readonly HashSet hashSet = new(); - - /// - /// 获取元素数量 - /// - public int Count => this.hashSet.Count; - - /// - /// 添加元素 - /// - /// - /// - public bool Add(IPAddress address) - { - lock (this.syncRoot) - { - return this.hashSet.Add(new IPAddressItem(address)); - } - } - - /// - /// 转后为数组 - /// - /// - public IPAddress[] ToArray() - { - lock (this.syncRoot) - { - return this.hashSet.OrderBy(item => item.PingElapsed).Select(item => item.Address).ToArray(); - } - } - - /// - /// Ping所有IP - /// - /// - public async Task PingAllAsync() - { - foreach (var item in this.ToItemArray()) - { - await item.PingAsync(); - } - } - - /// - /// 转换为数组 - /// - /// - private IPAddressItem[] ToItemArray() - { - lock (this.syncRoot) - { - return this.hashSet.ToArray(); - } - } - - /// - /// IP地址项 - /// - [DebuggerDisplay("Address = {Address}, PingElapsed = {PingElapsed}")] - private class IPAddressItem : IEquatable - { - /// - /// Ping的时间点 - /// - private int? pingTicks; - - /// - /// 地址 - /// - public IPAddress Address { get; } - - /// - /// Ping耗时 - /// - public TimeSpan PingElapsed { get; private set; } = TimeSpan.MaxValue; - - /// - /// IP地址项 - /// - /// - public IPAddressItem(IPAddress address) - { - this.Address = address; - } - /// - /// 发起ping请求 - /// - /// - public async Task PingAsync() - { - if (this.NeedToPing() == false) - { - return; - } - - try - { - using var ping = new Ping(); - var reply = await ping.SendPingAsync(this.Address); - this.PingElapsed = reply.Status == IPStatus.Success - ? TimeSpan.FromMilliseconds(reply.RoundtripTime) - : TimeSpan.MaxValue; - } - catch (Exception) - { - this.PingElapsed = TimeSpan.MaxValue; - } - finally - { - this.pingTicks = Environment.TickCount; - } - } - - /// - /// 是否需要ping - /// 5分钟内只ping一次 - /// - /// - private bool NeedToPing() - { - var ticks = this.pingTicks; - if (ticks == null) - { - return true; - } - - var pingTimeSpan = TimeSpan.FromMilliseconds(Environment.TickCount - ticks.Value); - return pingTimeSpan > TimeSpan.FromMinutes(5d); - } - - public bool Equals(IPAddressItem? other) - { - return other != null && other.Address.Equals(this.Address); - } - - public override bool Equals(object? obj) - { - return obj is IPAddressItem other && this.Equals(other); - } - - public override int GetHashCode() - { - return this.Address.GetHashCode(); - } - } - } -} diff --git a/FastGithub.DomainResolve/IPAddressElapsed.cs b/FastGithub.DomainResolve/IPAddressElapsed.cs new file mode 100644 index 0000000..e5d21ba --- /dev/null +++ b/FastGithub.DomainResolve/IPAddressElapsed.cs @@ -0,0 +1,34 @@ +using System; +using System.Diagnostics; +using System.Net; + +namespace FastGithub.DomainResolve +{ + /// + /// IP连接耗时 + /// + [DebuggerDisplay("Adddress={Adddress} Elapsed={Elapsed}")] + struct IPAddressElapsed + { + /// + /// 获取IP地址 + /// + public IPAddress Adddress { get; } + + /// + /// 获取连接耗时 + /// + public TimeSpan Elapsed { get; } + + /// + /// IP连接耗时 + /// + /// + /// + public IPAddressElapsed(IPAddress adddress, TimeSpan elapsed) + { + this.Adddress = adddress; + this.Elapsed = elapsed; + } + } +} diff --git a/FastGithub.DomainResolve/IPAddressTestResult.cs b/FastGithub.DomainResolve/IPAddressTestResult.cs new file mode 100644 index 0000000..38f0c86 --- /dev/null +++ b/FastGithub.DomainResolve/IPAddressTestResult.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FastGithub.DomainResolve +{ + /// + /// IP测速结果 + /// + sealed class IPAddressTestResult + { + private static readonly TimeSpan lifeTime = TimeSpan.FromMinutes(2d); + private readonly int creationTickCount = Environment.TickCount; + + /// + /// 获取空的 + /// + public static IPAddressTestResult Empty = new(Array.Empty()); + + /// + /// 获取是否为空 + /// + public bool IsEmpty => this.AddressElapseds.Length == 0; + + /// + /// 获取是否已过期 + /// + public bool IsExpired => lifeTime < TimeSpan.FromMilliseconds(Environment.TickCount - this.creationTickCount); + + /// + /// 获取测速结果 + /// + public IPAddressElapsed[] AddressElapseds { get; } + + /// + /// 测速结果 + /// + /// + public IPAddressTestResult(IEnumerable addressElapseds) + { + this.AddressElapseds = addressElapseds.OrderBy(item => item.Elapsed).ToArray(); + } + } +} diff --git a/FastGithub.Http/HttpClientHandler.cs b/FastGithub.Http/HttpClientHandler.cs index 2ae92a8..2e8fc98 100644 --- a/FastGithub.Http/HttpClientHandler.cs +++ b/FastGithub.Http/HttpClientHandler.cs @@ -186,7 +186,7 @@ namespace FastGithub.Http } else { - await foreach (var item in this.domainResolver.ResolveAllAsync(dnsEndPoint.Host, cancellationToken)) + await foreach (var item in this.domainResolver.ResolveAllAsync(dnsEndPoint, cancellationToken)) { yield return new IPEndPoint(item, dnsEndPoint.Port); } diff --git a/FastGithub.HttpServer/HttpProxyMiddleware.cs b/FastGithub.HttpServer/HttpProxyMiddleware.cs index 722ed9d..a4f11f4 100644 --- a/FastGithub.HttpServer/HttpProxyMiddleware.cs +++ b/FastGithub.HttpServer/HttpProxyMiddleware.cs @@ -158,7 +158,7 @@ namespace FastGithub.HttpServer } // 不使用系统dns - address = await this.domainResolver.ResolveAnyAsync(targetHost); + address = await this.domainResolver.ResolveAnyAsync(new DnsEndPoint(targetHost, targetPort)); return new IPEndPoint(address, targetPort); } diff --git a/FastGithub.HttpServer/SshReverseProxyHandler.cs b/FastGithub.HttpServer/SshReverseProxyHandler.cs index 37ed22e..79a8738 100644 --- a/FastGithub.HttpServer/SshReverseProxyHandler.cs +++ b/FastGithub.HttpServer/SshReverseProxyHandler.cs @@ -1,6 +1,7 @@ using FastGithub.DomainResolve; using Microsoft.AspNetCore.Connections; using System.IO.Pipelines; +using System.Net; using System.Net.Sockets; using System.Threading.Tasks; @@ -12,8 +13,7 @@ namespace FastGithub.HttpServer sealed class SshReverseProxyHandler : ConnectionHandler { private readonly IDomainResolver domainResolver; - private const string SSH_GITHUB_COM = "ssh.github.com"; - private const int SSH_OVER_HTTPS_PORT = 443; + private readonly DnsEndPoint sshOverHttpsEndPoint = new("ssh.github.com", 443); /// /// github的ssh代理处理者 @@ -31,9 +31,9 @@ namespace FastGithub.HttpServer /// public override async Task OnConnectedAsync(ConnectionContext context) { - var address = await this.domainResolver.ResolveAnyAsync(SSH_GITHUB_COM); + var address = await this.domainResolver.ResolveAnyAsync(this.sshOverHttpsEndPoint); using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp); - await socket.ConnectAsync(address, SSH_OVER_HTTPS_PORT); + await socket.ConnectAsync(address, this.sshOverHttpsEndPoint.Port); var targetStream = new NetworkStream(socket, ownsSocket: false); var task1 = targetStream.CopyToAsync(context.Transport.Output);