ip快速排序

This commit is contained in:
陈国伟 2021-09-29 11:37:02 +08:00
parent e88312d177
commit 92ff4a54b4
13 changed files with 107 additions and 422 deletions

View File

@ -11,6 +11,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
@ -31,9 +32,11 @@ namespace FastGithub.DomainResolve
private readonly ConcurrentDictionary<string, SemaphoreSlim> semaphoreSlims = new();
private readonly IMemoryCache dnsCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly TimeSpan dnsExpiration = TimeSpan.FromMinutes(1d);
private readonly TimeSpan defaultEmptyTtl = TimeSpan.FromSeconds(30d);
private readonly int resolveTimeout = (int)TimeSpan.FromSeconds(2d).TotalMilliseconds;
private record LookupResult(IPAddress[] Addresses, TimeSpan TimeToLive);
/// <summary>
/// DNS客户端
/// </summary>
@ -56,21 +59,12 @@ namespace FastGithub.DomainResolve
/// <param name="domain">域名</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async IAsyncEnumerable<IPAddress[]> ResolveAsync(string domain, [EnumeratorCancellation] CancellationToken cancellationToken)
public async IAsyncEnumerable<IPAddress> ResolveAsync(string domain, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var hashSet = new HashSet<IPAddress>();
foreach (var dns in this.GetDnsServers())
{
var addresses = await this.LookupAsync(dns, domain, cancellationToken);
var value = Filter(hashSet, addresses).ToArray();
if (value.Length > 0)
{
yield return value;
}
}
static IEnumerable<IPAddress> Filter(HashSet<IPAddress> hashSet, IPAddress[] addresses)
{
foreach (var address in addresses)
{
if (hashSet.Add(address) == true)
@ -116,16 +110,19 @@ namespace FastGithub.DomainResolve
try
{
if (this.dnsCache.TryGetValue<IPAddress[]>(key, out var value) == false)
if (this.dnsCache.TryGetValue<IPAddress[]>(key, out var value))
{
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;
}
var result = await this.LookupCoreAsync(dns, domain, cancellationToken);
this.dnsCache.Set(key, result.Addresses, result.TimeToLive);
var items = string.Join(", ", result.Addresses.Select(item => item.ToString()));
this.logger.LogInformation($"dns://{dns}{domain}->[{items}]");
return result.Addresses;
}
catch (Exception ex)
{
this.logger.LogWarning($"dns://{dns}无法解析{domain}{ex.Message}");
@ -144,11 +141,11 @@ namespace FastGithub.DomainResolve
/// <param name="domain"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task<IPAddress[]> LookupCoreAsync(IPEndPoint dns, string domain, CancellationToken cancellationToken = default)
private async Task<LookupResult> LookupCoreAsync(IPEndPoint dns, string domain, CancellationToken cancellationToken = default)
{
if (domain == LOCALHOST)
{
return new[] { IPAddress.Loopback };
return new LookupResult(new[] { IPAddress.Loopback }, TimeSpan.MaxValue);
}
var resolver = dns.Port == DNS_PORT
@ -165,11 +162,70 @@ namespace FastGithub.DomainResolve
var clientRequest = new ClientRequest(resolver, request);
var response = await clientRequest.Resolve(cancellationToken);
return response.AnswerRecords
var addresses = response.AnswerRecords
.OfType<IPAddressResourceRecord>()
.Where(item => IPAddress.IsLoopback(item.IPAddress) == false)
.Select(item => item.IPAddress)
.ToArray();
if (addresses.Length == 0)
{
return new LookupResult(addresses, this.defaultEmptyTtl);
}
if (addresses.Length > 1)
{
addresses = await OrderByPingAnyAsync(addresses);
}
var timeToLive = response.AnswerRecords.First().TimeToLive;
if (timeToLive <= TimeSpan.Zero)
{
timeToLive = this.defaultEmptyTtl;
}
this.logger.LogWarning($"{domain} [{timeToLive}]");
return new LookupResult(addresses, timeToLive);
}
/// <summary>
/// ping排序
/// </summary>
/// <param name="addresses"></param>
/// <returns></returns>
private static async Task<IPAddress[]> OrderByPingAnyAsync(IPAddress[] addresses)
{
var fastedAddress = await await Task.WhenAny(addresses.Select(address => PingAsync(address)));
if (fastedAddress == null)
{
return addresses;
}
var hashSet = new HashSet<IPAddress> { fastedAddress };
foreach (var address in addresses)
{
hashSet.Add(address);
}
return hashSet.ToArray();
}
/// <summary>
/// ping请求
/// </summary>
/// <param name="address"></param>
/// <returns></returns>
private static async Task<IPAddress?> PingAsync(IPAddress address)
{
try
{
using var ping = new Ping();
var reply = await ping.SendPingAsync(address);
return reply.Status == IPStatus.Success ? address : default;
}
catch (Exception)
{
return default;
}
}
}
}

View File

@ -1,5 +1,4 @@
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;
@ -8,25 +7,17 @@ namespace FastGithub.DomainResolve
/// <summary>
/// 域名解析后台服务
/// </summary>
sealed class DomainResolveHostedService : BackgroundService
sealed class DnscryptProxyHostedService : BackgroundService
{
private readonly DnscryptProxy dnscryptProxy;
private readonly DomainSpeedTester speedTester;
private readonly TimeSpan speedTestDueTime = TimeSpan.FromSeconds(10d);
private readonly TimeSpan speedTestPeriod = TimeSpan.FromMinutes(2d);
/// <summary>
/// 域名解析后台服务
/// </summary>
/// <param name="dnscryptProxy"></param>
/// <param name="speedTester"></param>
public DomainResolveHostedService(
DnscryptProxy dnscryptProxy,
DomainSpeedTester speedTester)
public DnscryptProxyHostedService(DnscryptProxy dnscryptProxy)
{
this.dnscryptProxy = dnscryptProxy;
this.speedTester = speedTester;
}
/// <summary>
@ -37,13 +28,6 @@ namespace FastGithub.DomainResolve
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await this.dnscryptProxy.StartAsync(stoppingToken);
await Task.Delay(this.speedTestDueTime, stoppingToken);
while (stoppingToken.IsCancellationRequested == false)
{
await this.speedTester.TestSpeedAsync(stoppingToken);
await Task.Delay(this.speedTestPeriod, stoppingToken);
}
}
/// <summary>

View File

@ -1,7 +1,6 @@
using FastGithub.Configuration;
using System.Collections.Generic;
using System.Net;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
@ -12,15 +11,15 @@ namespace FastGithub.DomainResolve
/// </summary>
sealed class DomainResolver : IDomainResolver
{
private readonly DomainSpeedTester speedTester;
private readonly DnsClient dnsClient;
/// <summary>
/// 域名解析器
/// </summary>
/// <param name="speedTester"></param>
public DomainResolver(DomainSpeedTester speedTester)
/// <param name="dnsClient"></param>
public DomainResolver(DnsClient dnsClient)
{
this.speedTester = speedTester;
this.dnsClient = dnsClient;
}
/// <summary>
@ -29,7 +28,7 @@ namespace FastGithub.DomainResolve
/// <param name="domain">域名</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<IPAddress> ResolveAsync(string domain, CancellationToken cancellationToken = default)
public async Task<IPAddress> ResolveAnyAsync(string domain, CancellationToken cancellationToken = default)
{
await foreach (var address in this.ResolveAllAsync(domain, cancellationToken))
{
@ -44,23 +43,9 @@ namespace FastGithub.DomainResolve
/// <param name="domain">域名</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async IAsyncEnumerable<IPAddress> ResolveAllAsync(string domain, [EnumeratorCancellation] CancellationToken cancellationToken)
public IAsyncEnumerable<IPAddress> ResolveAllAsync(string domain, CancellationToken cancellationToken)
{
if (this.speedTester.TryGetOrderAllIPAddresses(domain, out var addresses))
{
foreach (var address in addresses)
{
yield return address;
}
}
else
{
this.speedTester.Add(domain);
await foreach (var address in this.speedTester.GetOrderAnyIPAddressAsync(domain, cancellationToken))
{
yield return address;
}
}
return this.dnsClient.ResolveAsync(domain, cancellationToken);
}
}
}

View File

@ -1,178 +0,0 @@
using FastGithub.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
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>
/// <param name="addresses"></param>
/// <returns></returns>
public bool TryGetOrderAllIPAddresses(string domain, [MaybeNullWhen(false)] out IPAddress[] addresses)
{
lock (this.syncRoot)
{
if (this.domainIPAddressHashSet.TryGetValue(domain, out var hashSet) && hashSet.Count > 0)
{
addresses = hashSet.ToArray().OrderBy(item => item.PingElapsed).Select(item => item.Address).ToArray();
return true;
}
}
addresses = default;
return false;
}
/// <summary>
/// 获取只排序头个元素的IP地址
/// </summary>
/// <param name="domain">域名</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async IAsyncEnumerable<IPAddress> GetOrderAnyIPAddressAsync(string domain, [EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var addresses in this.dnsClient.ResolveAsync(domain, cancellationToken))
{
foreach (var address in addresses)
{
yield return address;
}
}
}
/// <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 addresses in this.dnsClient.ResolveAsync(domain, cancellationToken))
{
foreach (var address in addresses)
{
hashSet.Add(new IPAddressItem(address));
}
}
await hashSet.PingAllAsync();
}
}
}
}

View File

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

View File

@ -16,7 +16,7 @@ namespace FastGithub.DomainResolve
/// <param name="domain">域名</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<IPAddress> ResolveAsync(string domain, CancellationToken cancellationToken = default);
Task<IPAddress> ResolveAnyAsync(string domain, CancellationToken cancellationToken = default);
/// <summary>
/// 解析所有ip

View File

@ -1,70 +0,0 @@
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

@ -1,64 +0,0 @@
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,9 +18,8 @@ namespace FastGithub
{
services.TryAddSingleton<DnsClient>();
services.TryAddSingleton<DnscryptProxy>();
services.TryAddSingleton<DomainSpeedTester>();
services.TryAddSingleton<IDomainResolver, DomainResolver>();
services.AddHostedService<DomainResolveHostedService>();
services.AddHostedService<DnscryptProxyHostedService>();
return services;
}
}

View File

@ -1,22 +0,0 @@
[
"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"
]

View File

@ -43,7 +43,7 @@ namespace FastGithub.Http
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var uri = request.RequestUri;
if (uri == null)
@ -56,18 +56,24 @@ namespace FastGithub.Http
var tlsSniValue = this.domainConfig.GetTlsSniPattern().WithDomain(uri.Host).WithRandom();
request.SetRequestContext(new RequestContext(isHttps, tlsSniValue));
// 设置请求host修改协议为http
// 设置请求头host修改协议为http使用ip取代域名
var address = await this.domainResolver.ResolveAnyAsync(uri.Host, cancellationToken);
var uriBuilder = new UriBuilder(uri)
{
Scheme = Uri.UriSchemeHttp,
Host = address.ToString()
};
request.Headers.Host = uri.Host;
request.RequestUri = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp }.Uri;
request.RequestUri = uriBuilder.Uri;
if (this.domainConfig.Timeout != null)
{
using var timeoutTokenSource = new CancellationTokenSource(this.domainConfig.Timeout.Value);
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token);
return base.SendAsync(request, linkedTokenSource.Token);
return await base.SendAsync(request, linkedTokenSource.Token);
}
return base.SendAsync(request, cancellationToken);
return await base.SendAsync(request, cancellationToken);
}
/// <summary>
@ -96,7 +102,8 @@ namespace FastGithub.Http
private async ValueTask<Stream> ConnectCallback(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
var innerExceptions = new List<Exception>();
var ipEndPoints = this.GetIPEndPointsAsync(context.DnsEndPoint, cancellationToken);
var dnsEndPoint = new DnsEndPoint(context.InitialRequestMessage.Headers.Host!, context.DnsEndPoint.Port);
var ipEndPoints = this.GetIPEndPointsAsync(dnsEndPoint, cancellationToken);
await foreach (var ipEndPoint in ipEndPoints)
{
@ -159,7 +166,7 @@ namespace FastGithub.Http
return true;
}
var domain = context.DnsEndPoint.Host;
var domain = context.InitialRequestMessage.Headers.Host!;
var dnsNames = ReadDnsNames(cert);
return dnsNames.Any(dns => IsMatch(dns, domain));
}

View File

@ -158,7 +158,7 @@ namespace FastGithub.HttpServer
}
// 不使用系统dns
address = await this.domainResolver.ResolveAsync(targetHost);
address = await this.domainResolver.ResolveAnyAsync(targetHost);
return new IPEndPoint(address, targetPort);
}

View File

@ -31,7 +31,7 @@ namespace FastGithub.HttpServer
/// <returns></returns>
public override async Task OnConnectedAsync(ConnectionContext context)
{
var address = await this.domainResolver.ResolveAsync(SSH_GITHUB_COM);
var address = await this.domainResolver.ResolveAnyAsync(SSH_GITHUB_COM);
using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(address, SSH_OVER_HTTPS_PORT);
var targetStream = new NetworkStream(socket, ownsSocket: false);