Merge pull request #63 from dotnetcore/ip-speed-test

Ip speed test
This commit is contained in:
老九 2021-09-28 17:58:20 +08:00 committed by GitHub
commit e8c5fd5216
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 479 additions and 86 deletions

View File

@ -7,7 +7,7 @@ namespace FastGithub.Configuration
/// 表示域名表达式 /// 表示域名表达式
/// *表示除.之外任意0到多个字符 /// *表示除.之外任意0到多个字符
/// </summary> /// </summary>
sealed class DomainPattern : IComparable<DomainPattern> public class DomainPattern : IComparable<DomainPattern>
{ {
private readonly Regex regex; private readonly Regex regex;
private readonly string domainPattern; private readonly string domainPattern;

View File

@ -117,9 +117,9 @@ namespace FastGithub.Configuration
/// 获取所有域名表达式 /// 获取所有域名表达式
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public string[] GetDomainPatterns() public DomainPattern[] GetDomainPatterns()
{ {
return this.domainConfigs.Keys.Select(item => item.ToString()).ToArray(); return this.domainConfigs.Keys.ToArray();
} }
} }
} }

View File

@ -2,13 +2,16 @@
using DNS.Client.RequestResolver; using DNS.Client.RequestResolver;
using DNS.Protocol; using DNS.Protocol;
using DNS.Protocol.ResourceRecords; using DNS.Protocol.ResourceRecords;
using FastGithub.Configuration;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -21,22 +24,72 @@ namespace FastGithub.DomainResolve
{ {
private const int DNS_PORT = 53; private const int DNS_PORT = 53;
private const string LOCALHOST = "localhost"; private const string LOCALHOST = "localhost";
private readonly DnscryptProxy dnscryptProxy;
private readonly FastGithubConfig fastGithubConfig;
private readonly ILogger<DnsClient> logger; private readonly ILogger<DnsClient> logger;
private readonly ConcurrentDictionary<string, SemaphoreSlim> semaphoreSlims = new(); private readonly ConcurrentDictionary<string, SemaphoreSlim> semaphoreSlims = new();
private readonly IMemoryCache dnsCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly IMemoryCache dnsCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly TimeSpan dnsExpiration = TimeSpan.FromMinutes(2d); private readonly TimeSpan dnsExpiration = TimeSpan.FromMinutes(1d);
private readonly int resolveTimeout = (int)TimeSpan.FromSeconds(2d).TotalMilliseconds; private readonly int resolveTimeout = (int)TimeSpan.FromSeconds(2d).TotalMilliseconds;
/// <summary> /// <summary>
/// DNS客户端 /// DNS客户端
/// </summary> /// </summary>
/// <param name="dnscryptProxy"></param>
/// <param name="fastGithubConfig"></param>
/// <param name="logger"></param> /// <param name="logger"></param>
public DnsClient(ILogger<DnsClient> logger) public DnsClient(
DnscryptProxy dnscryptProxy,
FastGithubConfig fastGithubConfig,
ILogger<DnsClient> logger)
{ {
this.dnscryptProxy = dnscryptProxy;
this.fastGithubConfig = fastGithubConfig;
this.logger = logger; this.logger = logger;
} }
/// <summary>
/// 解析域名
/// </summary>
/// <param name="domain">域名</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async IAsyncEnumerable<IPAddress> ResolveAsync(string domain, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var hashSet = new HashSet<IPAddress>();
foreach (var dns in this.GetDnsServers())
{
foreach (var address in await this.LookupAsync(dns, domain, cancellationToken))
{
if (hashSet.Add(address) == true)
{
yield return address;
}
}
}
}
/// <summary>
/// 获取dns服务
/// </summary>
/// <returns></returns>
private IEnumerable<IPEndPoint> GetDnsServers()
{
var cryptDns = this.dnscryptProxy.LocalEndPoint;
if (cryptDns != null)
{
yield return cryptDns;
yield return cryptDns;
}
foreach (var fallbackDns in this.fastGithubConfig.FallbackDns)
{
yield return fallbackDns;
}
}
/// <summary> /// <summary>
/// 解析域名 /// 解析域名
/// </summary> /// </summary>
@ -44,7 +97,7 @@ namespace FastGithub.DomainResolve
/// <param name="domain"></param> /// <param name="domain"></param>
/// <param name="cancellationToken"></param> /// <param name="cancellationToken"></param>
/// <returns></returns> /// <returns></returns>
public async Task<IPAddress[]> LookupAsync(IPEndPoint dns, string domain, CancellationToken cancellationToken = default) private async Task<IPAddress[]> LookupAsync(IPEndPoint dns, string domain, CancellationToken cancellationToken = default)
{ {
var key = $"{dns}:{domain}"; var key = $"{dns}:{domain}";
var semaphore = this.semaphoreSlims.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); var semaphore = this.semaphoreSlims.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
@ -96,10 +149,16 @@ namespace FastGithub.DomainResolve
RecursionDesired = true, RecursionDesired = true,
OperationCode = OperationCode.Query OperationCode = OperationCode.Query
}; };
request.Questions.Add(new Question(new Domain(domain), RecordType.A)); request.Questions.Add(new Question(new Domain(domain), RecordType.A));
var clientRequest = new ClientRequest(resolver, request); var clientRequest = new ClientRequest(resolver, request);
var response = await clientRequest.Resolve(cancellationToken); var response = await clientRequest.Resolve(cancellationToken);
return response.AnswerRecords.OfType<IPAddressResourceRecord>().Select(item => item.IPAddress).ToArray();
return response.AnswerRecords
.OfType<IPAddressResourceRecord>()
.Where(item => IPAddress.IsLoopback(item.IPAddress) == false)
.Select(item => item.IPAddress)
.ToArray();
} }
} }
} }

View File

@ -1,4 +1,5 @@
using FastGithub.Configuration; using FastGithub.Configuration;
using Microsoft.Extensions.Logging;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
@ -20,6 +21,8 @@ namespace FastGithub.DomainResolve
private const string PATH = "dnscrypt-proxy"; private const string PATH = "dnscrypt-proxy";
private const string NAME = "dnscrypt-proxy"; private const string NAME = "dnscrypt-proxy";
private readonly ILogger<DnscryptProxy> logger;
/// <summary> /// <summary>
/// 相关进程 /// 相关进程
/// </summary> /// </summary>
@ -30,12 +33,38 @@ namespace FastGithub.DomainResolve
/// </summary> /// </summary>
public IPEndPoint? LocalEndPoint { get; private set; } public IPEndPoint? LocalEndPoint { get; private set; }
/// <summary>
/// DnscryptProxy服务
/// </summary>
/// <param name="logger"></param>
public DnscryptProxy(ILogger<DnscryptProxy> logger)
{
this.logger = logger;
}
/// <summary> /// <summary>
/// 启动dnscrypt-proxy /// 启动dnscrypt-proxy
/// </summary> /// </summary>
/// <param name="cancellationToken"></param> /// <param name="cancellationToken"></param>
/// <returns></returns> /// <returns></returns>
public async Task StartAsync(CancellationToken cancellationToken) public async Task StartAsync(CancellationToken cancellationToken)
{
try
{
await this.StartCoreAsync(cancellationToken);
}
catch (Exception ex)
{
this.logger.LogWarning($"{NAME}启动失败:{ex.Message}");
}
}
/// <summary>
/// 启动dnscrypt-proxy
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task StartCoreAsync(CancellationToken cancellationToken)
{ {
var tomlPath = Path.Combine(PATH, $"{NAME}.toml"); var tomlPath = Path.Combine(PATH, $"{NAME}.toml");
var port = GetAvailablePort(IPAddress.Loopback.AddressFamily); var port = GetAvailablePort(IPAddress.Loopback.AddressFamily);
@ -70,7 +99,32 @@ namespace FastGithub.DomainResolve
} }
} }
/// <summary>
/// 停止服务
/// </summary>
public void Stop()
{
try
{
if (OperatingSystem.IsWindows())
{
StartDnscryptProxy("-service stop")?.WaitForExit();
StartDnscryptProxy("-service uninstall")?.WaitForExit();
}
if (this.process != null && this.process.HasExited == false)
{
this.process.Kill();
}
}
catch (Exception ex)
{
this.logger.LogWarning($"{NAME}停止失败:{ex.Message }");
}
finally
{
this.LocalEndPoint = null;
}
}
/// <summary> /// <summary>
/// 获取可用的随机端口 /// 获取可用的随机端口
@ -113,24 +167,6 @@ namespace FastGithub.DomainResolve
this.LocalEndPoint = null; this.LocalEndPoint = null;
} }
/// <summary>
/// 停止dnscrypt-proxy
/// </summary>
public void Stop()
{
if (OperatingSystem.IsWindows())
{
StartDnscryptProxy("-service stop")?.WaitForExit();
StartDnscryptProxy("-service uninstall")?.WaitForExit();
}
if (this.process != null && this.process.HasExited == false)
{
this.process.Kill();
}
this.LocalEndPoint = null;
}
/// <summary> /// <summary>
/// 启动DnscryptProxy进程 /// 启动DnscryptProxy进程
/// </summary> /// </summary>

View File

@ -1,5 +1,4 @@
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -7,58 +6,54 @@ using System.Threading.Tasks;
namespace FastGithub.DomainResolve namespace FastGithub.DomainResolve
{ {
/// <summary> /// <summary>
/// DnscryptProxy后台服务 /// 域名解析后台服务
/// </summary> /// </summary>
sealed class DnscryptProxyHostedService : BackgroundService sealed class DomainResolveHostedService : BackgroundService
{ {
private readonly ILogger<DnscryptProxyHostedService> logger;
private readonly DnscryptProxy dnscryptProxy; private readonly DnscryptProxy dnscryptProxy;
private readonly DomainSpeedTester speedTester;
private readonly TimeSpan speedTestDueTime = TimeSpan.FromSeconds(10d);
private readonly TimeSpan speedTestPeriod = TimeSpan.FromMinutes(2d);
/// <summary> /// <summary>
/// DnscryptProxy后台服务 /// 域名解析后台服务
/// </summary> /// </summary>
/// <param name="dnscryptProxy"></param> /// <param name="dnscryptProxy"></param>
/// <param name="logger"></param> /// <param name="speedTester"></param>
public DnscryptProxyHostedService( public DomainResolveHostedService(
DnscryptProxy dnscryptProxy, DnscryptProxy dnscryptProxy,
ILogger<DnscryptProxyHostedService> logger) DomainSpeedTester speedTester)
{ {
this.dnscryptProxy = dnscryptProxy; this.dnscryptProxy = dnscryptProxy;
this.logger = logger; this.speedTester = speedTester;
} }
/// <summary> /// <summary>
/// 启动dnscrypt-proxy /// 后台任务
/// </summary> /// </summary>
/// <param name="stoppingToken"></param> /// <param name="stoppingToken"></param>
/// <returns></returns> /// <returns></returns>
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
try await this.dnscryptProxy.StartAsync(stoppingToken);
await Task.Delay(this.speedTestDueTime, stoppingToken);
while (stoppingToken.IsCancellationRequested == false)
{ {
await this.dnscryptProxy.StartAsync(stoppingToken); await this.speedTester.TestSpeedAsync(stoppingToken);
} await Task.Delay(this.speedTestPeriod, stoppingToken);
catch (Exception ex)
{
this.logger.LogWarning($"{this.dnscryptProxy}启动失败:{ex.Message}");
} }
} }
/// <summary> /// <summary>
/// 停止dnscrypt-proxy /// 停止服务
/// </summary> /// </summary>
/// <param name="cancellationToken"></param> /// <param name="cancellationToken"></param>
/// <returns></returns> /// <returns></returns>
public override Task StopAsync(CancellationToken cancellationToken) public override Task StopAsync(CancellationToken cancellationToken)
{ {
try this.dnscryptProxy.Stop();
{
this.dnscryptProxy.Stop();
}
catch (Exception ex)
{
this.logger.LogWarning($"{this.dnscryptProxy}停止失败:{ex.Message}");
}
return base.StopAsync(cancellationToken); return base.StopAsync(cancellationToken);
} }
} }

View File

@ -12,24 +12,20 @@ namespace FastGithub.DomainResolve
/// </summary> /// </summary>
sealed class DomainResolver : IDomainResolver sealed class DomainResolver : IDomainResolver
{ {
private readonly DnscryptProxy dnscryptProxy;
private readonly FastGithubConfig fastGithubConfig;
private readonly DnsClient dnsClient; private readonly DnsClient dnsClient;
private readonly DomainSpeedTester speedTester;
/// <summary> /// <summary>
/// 域名解析器 /// 域名解析器
/// </summary> /// </summary>
/// <param name="dnscryptProxy"></param>
/// <param name="fastGithubConfig"></param>
/// <param name="dnsClient"></param> /// <param name="dnsClient"></param>
/// <param name="speedTester"></param>
public DomainResolver( public DomainResolver(
DnscryptProxy dnscryptProxy, DnsClient dnsClient,
FastGithubConfig fastGithubConfig, DomainSpeedTester speedTester)
DnsClient dnsClient)
{ {
this.dnscryptProxy = dnscryptProxy;
this.fastGithubConfig = fastGithubConfig;
this.dnsClient = dnsClient; this.dnsClient = dnsClient;
this.speedTester = speedTester;
} }
/// <summary> /// <summary>
@ -55,35 +51,21 @@ namespace FastGithub.DomainResolve
/// <returns></returns> /// <returns></returns>
public async IAsyncEnumerable<IPAddress> ResolveAllAsync(string domain, [EnumeratorCancellation] CancellationToken cancellationToken) public async IAsyncEnumerable<IPAddress> ResolveAllAsync(string domain, [EnumeratorCancellation] CancellationToken cancellationToken)
{ {
var hashSet = new HashSet<IPAddress>(); var addresses = this.speedTester.GetIPAddresses(domain);
foreach (var dns in this.GetDnsServers()) if (addresses.Length > 0)
{ {
foreach (var address in await this.dnsClient.LookupAsync(dns, domain, cancellationToken)) foreach (var address in addresses)
{ {
if (hashSet.Add(address) == true) yield return address;
{
yield return address;
}
} }
} }
} else
/// <summary>
/// 获取dns服务
/// </summary>
/// <returns></returns>
private IEnumerable<IPEndPoint> GetDnsServers()
{
var cryptDns = this.dnscryptProxy.LocalEndPoint;
if (cryptDns != null)
{ {
yield return cryptDns; this.speedTester.Add(domain);
yield return cryptDns; await foreach (var address in this.dnsClient.ResolveAsync(domain, cancellationToken))
} {
yield return address;
foreach (var fallbackDns in this.fastGithubConfig.FallbackDns) }
{
yield return fallbackDns;
} }
} }
} }

View File

@ -0,0 +1,151 @@
using FastGithub.Configuration;
using Microsoft.Extensions.Logging;
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
{
/// <summary>
/// 域名的IP测速服务
/// </summary>
sealed class DomainSpeedTester
{
private const string DOMAINS_JSON_FILE = "domains.json";
private readonly DnsClient dnsClient;
private readonly ILogger<DomainSpeedTester> logger;
private readonly object syncRoot = new();
private readonly Dictionary<string, IPAddressItemHashSet> domainIPAddressHashSet = new();
/// <summary>
/// 域名的IP测速服务
/// </summary>
/// <param name="dnsClient"></param>
/// <param name="logger"></param>
public DomainSpeedTester(
DnsClient dnsClient,
ILogger<DomainSpeedTester> logger)
{
this.dnsClient = dnsClient;
this.logger = logger;
try
{
this.LoadDomains();
}
catch (Exception ex)
{
logger.LogWarning($"加载域名数据失败:{ex.Message}");
}
}
/// <summary>
/// 加载域名数据
/// </summary>
private void LoadDomains()
{
if (File.Exists(DOMAINS_JSON_FILE) == false)
{
return;
}
var utf8Json = File.ReadAllBytes(DOMAINS_JSON_FILE);
var domains = JsonSerializer.Deserialize<string[]>(utf8Json);
if (domains == null)
{
return;
}
foreach (var domain in domains)
{
this.domainIPAddressHashSet.TryAdd(domain, new IPAddressItemHashSet());
}
}
/// <summary>
/// 添加要测速的域名
/// </summary>
/// <param name="domain"></param>
public void Add(string domain)
{
lock (this.syncRoot)
{
if (this.domainIPAddressHashSet.TryAdd(domain, new IPAddressItemHashSet()))
{
try
{
this.SaveDomains();
}
catch (Exception ex)
{
logger.LogWarning($"保存域名数据失败:{ex.Message}");
}
}
}
}
/// <summary>
/// 保存域名
/// </summary>
private void SaveDomains()
{
var domains = this.domainIPAddressHashSet.Keys
.Select(item => new DomainPattern(item))
.OrderBy(item => item)
.Select(item => item.ToString())
.ToArray();
var utf8Json = JsonSerializer.SerializeToUtf8Bytes(domains, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllBytes(DOMAINS_JSON_FILE, utf8Json);
}
/// <summary>
/// 获取测试后排序的IP
/// </summary>
/// <param name="domain"></param>
/// <returns></returns>
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<IPAddress>();
}
}
/// <summary>
/// 进行一轮IP测速
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task TestSpeedAsync(CancellationToken cancellationToken)
{
KeyValuePair<string, IPAddressItemHashSet>[] 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();
}
}
}
}

View File

@ -17,4 +17,16 @@
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="domains.json" />
</ItemGroup>
<ItemGroup>
<Content Include="domains.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,70 @@
using System;
using System.Diagnostics;
using System.Net;
using System.Net.NetworkInformation;
using System.Threading.Tasks;
namespace FastGithub.DomainResolve
{
/// <summary>
/// IP地址项
/// </summary>
[DebuggerDisplay("Address = {Address}, PingElapsed = {PingElapsed}")]
sealed class IPAddressItem : IEquatable<IPAddressItem>
{
private readonly Ping ping = new();
/// <summary>
/// 地址
/// </summary>
public IPAddress Address { get; }
/// <summary>
/// Ping耗时
/// </summary>
public TimeSpan PingElapsed { get; private set; } = TimeSpan.MaxValue;
/// <summary>
/// IP地址项
/// </summary>
/// <param name="address"></param>
public IPAddressItem(IPAddress address)
{
this.Address = address;
}
/// <summary>
/// 发起ping请求
/// </summary>
/// <returns></returns>
public async Task PingAsync()
{
try
{
var reply = await this.ping.SendPingAsync(this.Address);
this.PingElapsed = reply.Status == IPStatus.Success
? TimeSpan.FromMilliseconds(reply.RoundtripTime)
: TimeSpan.MaxValue;
}
catch (Exception)
{
this.PingElapsed = TimeSpan.MaxValue;
}
}
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();
}
}
}

View File

@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace FastGithub.DomainResolve
{
/// <summary>
/// IPAddressItem集合
/// </summary>
sealed class IPAddressItemHashSet
{
private readonly object syncRoot = new();
private readonly HashSet<IPAddressItem> hashSet = new();
/// <summary>
/// 获取元素数量
/// </summary>
public int Count => this.hashSet.Count;
/// <summary>
/// 添加元素
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public bool Add(IPAddressItem item)
{
lock (this.syncRoot)
{
return this.hashSet.Add(item);
}
}
/// <summary>
/// 转换为数组
/// </summary>
/// <returns></returns>
public IPAddressItem[] ToArray()
{
lock (this.syncRoot)
{
return this.hashSet.ToArray();
}
}
/// <summary>
/// Ping所有IP
/// </summary>
/// <returns></returns>
public Task PingAllAsync()
{
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);
}
}
}

View File

@ -18,8 +18,10 @@ namespace FastGithub
{ {
services.TryAddSingleton<DnsClient>(); services.TryAddSingleton<DnsClient>();
services.TryAddSingleton<DnscryptProxy>(); services.TryAddSingleton<DnscryptProxy>();
services.TryAddSingleton<DomainSpeedTester>();
services.TryAddSingleton<IDomainResolver, DomainResolver>(); services.TryAddSingleton<IDomainResolver, DomainResolver>();
return services.AddHostedService<DnscryptProxyHostedService>(); services.AddHostedService<DomainResolveHostedService>();
return services;
} }
} }
} }

View File

@ -0,0 +1,22 @@
[
"github.com",
"v2ex.com",
"alive.github.com",
"api.github.com",
"collector.githubapp.com",
"github.githubassets.com",
"avatars.githubusercontent.com",
"camo.githubusercontent.com",
"github-releases.githubusercontent.com",
"raw.githubusercontent.com",
"www.gravatar.com",
"onedrive.live.com",
"cdn.v2ex.com",
"microsoft.github.io",
"fonts.geekzu.org",
"gapis.geekzu.org",
"i.stack.imgur.com",
"skyapi.onedrive.live.com",
"codeproject.freetls.fastly.net",
"codeproject.global.ssl.fastly.net"
]