diff --git a/FastGithub.DomainResolve/DnsClient.cs b/FastGithub.DomainResolve/DnsClient.cs index 4d1f03f..8a29636 100644 --- a/FastGithub.DomainResolve/DnsClient.cs +++ b/FastGithub.DomainResolve/DnsClient.cs @@ -81,7 +81,6 @@ namespace FastGithub.DomainResolve if (cryptDns != null) { yield return cryptDns; - yield return cryptDns; } foreach (var fallbackDns in this.fastGithubConfig.FallbackDns) @@ -109,9 +108,6 @@ namespace FastGithub.DomainResolve { value = await this.LookupCoreAsync(dns, domain, cancellationToken); this.dnsCache.Set(key, value, this.dnsExpiration); - - var items = string.Join(", ", value.Select(item => item.ToString())); - this.logger.LogInformation($"dns://{dns}:{domain}->[{items}]"); } return value; } diff --git a/FastGithub.DomainResolve/DomainResolver.cs b/FastGithub.DomainResolve/DomainResolver.cs index e44ef41..236e1a4 100644 --- a/FastGithub.DomainResolve/DomainResolver.cs +++ b/FastGithub.DomainResolve/DomainResolver.cs @@ -1,6 +1,7 @@ using FastGithub.Configuration; using System.Collections.Generic; using System.Net; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -12,14 +13,19 @@ namespace FastGithub.DomainResolve sealed class DomainResolver : IDomainResolver { private readonly DnsClient dnsClient; + private readonly DomainSpeedTestService speedTestService; /// /// 域名解析器 - /// + /// /// - public DomainResolver(DnsClient dnsClient) + /// + public DomainResolver( + DnsClient dnsClient, + DomainSpeedTestService speedTestService) { this.dnsClient = dnsClient; + this.speedTestService = speedTestService; } /// @@ -43,9 +49,24 @@ namespace FastGithub.DomainResolve /// 域名 /// /// - public IAsyncEnumerable ResolveAllAsync(string domain, CancellationToken cancellationToken) + public async IAsyncEnumerable ResolveAllAsync(string domain, [EnumeratorCancellation] CancellationToken cancellationToken) { - return this.dnsClient.ResolveAsync(domain, cancellationToken); + var addresses = this.speedTestService.GetIPAddresses(domain); + if (addresses.Length > 0) + { + foreach (var address in addresses) + { + yield return address; + } + } + else + { + this.speedTestService.Add(domain); + await foreach (var address in this.dnsClient.ResolveAsync(domain, cancellationToken)) + { + yield return address; + } + } } } } diff --git a/FastGithub.DomainResolve/DomainSpeedTestHostedService.cs b/FastGithub.DomainResolve/DomainSpeedTestHostedService.cs new file mode 100644 index 0000000..6699073 --- /dev/null +++ b/FastGithub.DomainResolve/DomainSpeedTestHostedService.cs @@ -0,0 +1,61 @@ +using Microsoft.Extensions.Hosting; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.DomainResolve +{ + /// + /// 域名的IP测速后台服务 + /// + sealed class DomainSpeedTestHostedService : BackgroundService + { + private readonly DomainSpeedTestService speedTestService; + private readonly TimeSpan testDueTime = TimeSpan.FromMinutes(1d); + + /// + /// 域名的IP测速后台服务 + /// + /// + public DomainSpeedTestHostedService(DomainSpeedTestService speedTestService) + { + this.speedTestService = speedTestService; + } + + /// + /// 启动时 + /// + /// + /// + public override async Task StartAsync(CancellationToken cancellationToken) + { + await this.speedTestService.LoadDataAsync(cancellationToken); + await base.StartAsync(cancellationToken); + } + + /// + /// 停止时 + /// + /// + /// + public override async Task StopAsync(CancellationToken cancellationToken) + { + await this.speedTestService.SaveDataAsync(); + await base.StopAsync(cancellationToken); + } + + /// + /// 后台测速 + /// + /// + /// + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (stoppingToken.IsCancellationRequested == false) + { + await this.speedTestService.TestSpeedAsync(stoppingToken); + await Task.Delay(this.testDueTime, stoppingToken); + } + } + } +} diff --git a/FastGithub.DomainResolve/DomainSpeedTestService.cs b/FastGithub.DomainResolve/DomainSpeedTestService.cs new file mode 100644 index 0000000..890305c --- /dev/null +++ b/FastGithub.DomainResolve/DomainSpeedTestService.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace FastGithub.DomainResolve +{ + /// + /// 域名的IP测速服务 + /// + sealed class DomainSpeedTestService + { + private const string DATA_FILE = "domains.json"; + private readonly DnsClient dnsClient; + + private readonly object syncRoot = new(); + private readonly Dictionary domainIPAddressHashSet = new(); + + /// + /// 域名的IP测速服务 + /// + /// + public DomainSpeedTestService(DnsClient dnsClient) + { + this.dnsClient = dnsClient; + } + + /// + /// 添加要测速的域名 + /// + /// + /// + public bool Add(string domain) + { + lock (this.syncRoot) + { + return this.domainIPAddressHashSet.TryAdd(domain, new IPAddressItemHashSet()); + } + } + + /// + /// 获取测试后排序的IP + /// + /// + /// + public IPAddress[] GetIPAddresses(string domain) + { + lock (this.syncRoot) + { + if (this.domainIPAddressHashSet.TryGetValue(domain, out var hashSet) && hashSet.Count > 0) + { + return hashSet.ToArray().OrderBy(item => item.PingElapsed).Select(item => item.Address).ToArray(); + } + return Array.Empty(); + } + } + + /// + /// 加载数据 + /// + /// + /// + public async Task LoadDataAsync(CancellationToken cancellationToken) + { + if (File.Exists(DATA_FILE) == false) + { + return; + } + + var fileStream = File.OpenRead(DATA_FILE); + var domains = await JsonSerializer.DeserializeAsync(fileStream, cancellationToken: cancellationToken); + if (domains == null) + { + return; + } + + lock (this.syncRoot) + { + foreach (var domain in domains) + { + this.domainIPAddressHashSet.TryAdd(domain, new IPAddressItemHashSet()); + } + } + } + + /// + /// 保存数据 + /// + /// + public async Task SaveDataAsync() + { + var domains = this.domainIPAddressHashSet.Keys.ToArray(); + using var fileStream = File.OpenWrite(DATA_FILE); + await JsonSerializer.SerializeAsync(fileStream, domains); + } + + /// + /// 进行一轮IP测速 + /// + /// + /// + public async Task TestSpeedAsync(CancellationToken cancellationToken) + { + KeyValuePair[] keyValues; + lock (this.syncRoot) + { + keyValues = this.domainIPAddressHashSet.ToArray(); + } + + foreach (var keyValue in keyValues) + { + var domain = keyValue.Key; + var hashSet = keyValue.Value; + await foreach (var address in this.dnsClient.ResolveAsync(domain, cancellationToken)) + { + hashSet.Add(new IPAddressItem(address)); + } + await hashSet.PingAllAsync(); + } + } + } +} diff --git a/FastGithub.DomainResolve/IPAddressItem.cs b/FastGithub.DomainResolve/IPAddressItem.cs index 189bf1b..0e058ea 100644 --- a/FastGithub.DomainResolve/IPAddressItem.cs +++ b/FastGithub.DomainResolve/IPAddressItem.cs @@ -1,34 +1,54 @@ using System; +using System.Diagnostics; using System.Net; using System.Net.NetworkInformation; using System.Threading.Tasks; namespace FastGithub.DomainResolve { + /// + /// IP地址项 + /// + [DebuggerDisplay("Address = {Address}, PingElapsed = {PingElapsed}")] sealed class IPAddressItem : IEquatable { + private readonly Ping ping = new(); + + /// + /// 地址 + /// public IPAddress Address { get; } - public TimeSpan Elapsed { get; private set; } = TimeSpan.MaxValue; + /// + /// Ping耗时 + /// + public TimeSpan PingElapsed { get; private set; } = TimeSpan.MaxValue; + /// + /// IP地址项 + /// + /// public IPAddressItem(IPAddress address) { this.Address = address; } - public async Task TestSpeedAsync() + /// + /// 发起ping请求 + /// + /// + public async Task PingAsync() { try { - using var ping = new Ping(); - var reply = await ping.SendPingAsync(this.Address); - this.Elapsed = reply.Status == IPStatus.Success + var reply = await this.ping.SendPingAsync(this.Address); + this.PingElapsed = reply.Status == IPStatus.Success ? TimeSpan.FromMilliseconds(reply.RoundtripTime) : TimeSpan.MaxValue; } catch (Exception) { - this.Elapsed = TimeSpan.MaxValue; + this.PingElapsed = TimeSpan.MaxValue; } } diff --git a/FastGithub.DomainResolve/IPAddressItemHashSet.cs b/FastGithub.DomainResolve/IPAddressItemHashSet.cs index c98b2da..d8de684 100644 --- a/FastGithub.DomainResolve/IPAddressItemHashSet.cs +++ b/FastGithub.DomainResolve/IPAddressItemHashSet.cs @@ -4,14 +4,24 @@ using System.Threading.Tasks; namespace FastGithub.DomainResolve { + /// + /// IPAddressItem集合 + /// sealed class IPAddressItemHashSet { private readonly object syncRoot = new(); - private readonly HashSet hashSet = new(); + /// + /// 获取元素数量 + /// public int Count => this.hashSet.Count; + /// + /// 添加元素 + /// + /// + /// public bool Add(IPAddressItem item) { lock (this.syncRoot) @@ -20,17 +30,10 @@ namespace FastGithub.DomainResolve } } - public void AddRange(IEnumerable items) - { - lock (this.syncRoot) - { - foreach (var item in items) - { - this.hashSet.Add(item); - } - } - } - + /// + /// 转换为数组 + /// + /// public IPAddressItem[] ToArray() { lock (this.syncRoot) @@ -39,9 +42,22 @@ namespace FastGithub.DomainResolve } } - public Task TestSpeedAsync() + /// + /// Ping所有IP + /// + /// + public Task PingAllAsync() { - var tasks = this.ToArray().Select(item => item.TestSpeedAsync()); + var items = this.ToArray(); + if (items.Length == 0) + { + return Task.CompletedTask; + } + if (items.Length == 1) + { + return items[0].PingAsync(); + } + var tasks = items.Select(item => item.PingAsync()); return Task.WhenAll(tasks); } } diff --git a/FastGithub.DomainResolve/ServiceCollectionExtensions.cs b/FastGithub.DomainResolve/ServiceCollectionExtensions.cs index c35acc7..cf646a5 100644 --- a/FastGithub.DomainResolve/ServiceCollectionExtensions.cs +++ b/FastGithub.DomainResolve/ServiceCollectionExtensions.cs @@ -18,8 +18,11 @@ namespace FastGithub { services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); - return services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + return services; } } }