初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.Caching;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Ae.Dns.Client;
|
||||
using Ae.Dns.Protocol;
|
||||
using Ae.Dns.Protocol.Enums;
|
||||
using Ae.Dns.Protocol.Records;
|
||||
using PCL.Core.IO.Net.Http;
|
||||
using PCL.Core.Logging;
|
||||
|
||||
namespace PCL.Core.IO.Net.Dns;
|
||||
|
||||
public class DnsQuery : IDisposable
|
||||
{
|
||||
private const string ModuleName = "DoH query";
|
||||
public static DnsQuery Instance { get; } = new();
|
||||
|
||||
private readonly DnsCachingClient _resolver;
|
||||
private readonly HttpClient[] _httpClients;
|
||||
|
||||
private DnsQuery()
|
||||
{
|
||||
var proxyHandler = new HttpClientHandler()
|
||||
{
|
||||
Proxy = HttpProxyManager.Instance
|
||||
};
|
||||
// 使用Ae.Dns创建DoH客户端,支持多个DoH服务器
|
||||
_httpClients =
|
||||
[
|
||||
new HttpClient(proxyHandler)
|
||||
{
|
||||
BaseAddress = new Uri("https://doh.pub/")
|
||||
},
|
||||
new HttpClient(proxyHandler)
|
||||
{
|
||||
BaseAddress = new Uri("https://doh.pysio.online/")
|
||||
},
|
||||
new HttpClient(proxyHandler)
|
||||
{
|
||||
BaseAddress = new Uri("https://cloudflare-dns.com/")
|
||||
}
|
||||
];
|
||||
_resolver = new DnsCachingClient(
|
||||
new WeightedDnsRacerClient(2, _httpClients.Select(static x => new DnsHttpClient(x)).ToArray<IDnsClient>()),
|
||||
new MemoryCache("DoH Query Cache"));
|
||||
}
|
||||
|
||||
public async Task<DnsMessage?> QueryAsync(string host, DnsQueryType qType, CancellationToken cts = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _resolver.Query(DnsQueryFactory.CreateQuery(host, qType), cts);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ModuleName, $"Failed to resolve DNS for {host}: {ex.Message}, use system default DNS");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<IPAddress[]?> QueryForIpAsync(string host, CancellationToken cts = default)
|
||||
{
|
||||
var queryResponse = await Task.WhenAll(
|
||||
[
|
||||
QueryAsync(host, DnsQueryType.A, cts),
|
||||
QueryAsync(host, DnsQueryType.AAAA, cts)
|
||||
]
|
||||
);
|
||||
|
||||
if (queryResponse.All(static x => x is null))
|
||||
{
|
||||
LogWrapper.Warn(ModuleName, $"Failed to query IP for host {host} using DoH, use system default DNS");
|
||||
return await System.Net.Dns.GetHostAddressesAsync(host, cts);
|
||||
}
|
||||
|
||||
return queryResponse.Where(static x => x is not null)
|
||||
.SelectMany(static x => x!.Answers)
|
||||
.Where(static x => x.Type is DnsQueryType.A or DnsQueryType.AAAA)
|
||||
.Select(static x => x.Resource as DnsIpAddressResource)
|
||||
.Where(static x => x is not null)
|
||||
.Select(static x => x!.IPAddress)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
foreach (var client in _httpClients)
|
||||
{
|
||||
client.Dispose();
|
||||
}
|
||||
_resolver.Dispose(); // 好像这个包的 Dispose 并没有做什么 lol
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Ae.Dns.Protocol.Records;
|
||||
|
||||
namespace PCL.Core.IO.Net.Dns;
|
||||
|
||||
public class DnsSrvResource : IDnsResource
|
||||
{
|
||||
public string Target { get; set; } = "";
|
||||
public int Weight { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public int Port { get; set; }
|
||||
|
||||
public void WriteBytes(Memory<byte> bytes, ref int offset)
|
||||
{
|
||||
// 6 Bytes for priority, weight, port and 2 bytes for length
|
||||
var buf = bytes.Span[offset..];
|
||||
BinaryPrimitives.WriteUInt16BigEndian(buf[offset..(offset + 2)], (ushort)Priority);
|
||||
offset += 2;
|
||||
BinaryPrimitives.WriteUInt16BigEndian(buf[offset..(offset + 2)], (ushort)Weight);
|
||||
offset += 2;
|
||||
BinaryPrimitives.WriteUInt16BigEndian(buf[offset..(offset + 2)], (ushort)Port);
|
||||
offset += 2;
|
||||
// target string
|
||||
foreach (var seg in Target.Split('.'))
|
||||
{
|
||||
var segBuf = Encoding.UTF8.GetBytes(seg);
|
||||
var segLength = (byte)segBuf.Length;
|
||||
buf[offset] = segLength;
|
||||
offset++;
|
||||
segBuf.CopyTo(buf[offset..]);
|
||||
offset += segBuf.Length;
|
||||
}
|
||||
}
|
||||
|
||||
public void ReadBytes(ReadOnlyMemory<byte> bytes, ref int offset, int length)
|
||||
{
|
||||
var buf = bytes.Slice(offset, length).Span;
|
||||
offset += length;
|
||||
var priorityBuf = buf[..2];
|
||||
Priority = BinaryPrimitives.ReadUInt16BigEndian(priorityBuf);
|
||||
var weightBuf = buf[2..4];
|
||||
Weight = BinaryPrimitives.ReadUInt16BigEndian(weightBuf);
|
||||
var portBuf = buf[4..6];
|
||||
Port = BinaryPrimitives.ReadUInt16BigEndian(portBuf);
|
||||
// left target buf. 1 Byte for length, then the string. Until we got end of string.
|
||||
var segments = new List<string>();
|
||||
var i = 6;
|
||||
while (i < length)
|
||||
{
|
||||
var segLength = buf[i];
|
||||
if (segLength == 0) break;
|
||||
i++;
|
||||
if (i + segLength > length)
|
||||
{
|
||||
throw new ArgumentException("Invalid DNS SRV resource record: segment length exceeds buffer size");
|
||||
}
|
||||
|
||||
segments.Add(Encoding.UTF8.GetString(buf.Slice(i, segLength)));
|
||||
i += segLength;
|
||||
}
|
||||
|
||||
Target = string.Join('.', segments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Ae.Dns.Protocol;
|
||||
using PCL.Core.Utils.Exts;
|
||||
|
||||
namespace PCL.Core.IO.Net.Dns;
|
||||
|
||||
public sealed class WeightedDnsRacerClient : IDnsClient
|
||||
{
|
||||
private readonly (IDnsClient Client, byte Weight)[] _clients;
|
||||
private readonly int _concurrentCount;
|
||||
private bool _updateWeigh = true;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public WeightedDnsRacerClient(int concurrentCount, params IDnsClient[] clients)
|
||||
{
|
||||
if (clients is null || clients.Length < concurrentCount)
|
||||
throw new ArgumentException("Not enough clients.");
|
||||
|
||||
_concurrentCount = concurrentCount;
|
||||
_clients = clients.Select(static c => (Client: c, Weight: (byte)0)).ToArray();
|
||||
}
|
||||
|
||||
public async Task<DnsMessage> Query(DnsMessage query, CancellationToken ct = default)
|
||||
{
|
||||
(IDnsClient Client, byte Weight)[] candidates;
|
||||
lock (_lock)
|
||||
{
|
||||
candidates = _clients
|
||||
.OrderByDescending(static x => x.Weight)
|
||||
.Take(_concurrentCount)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
var tasks = new Task<DnsMessage>[candidates.Length];
|
||||
|
||||
for (var i = 0; i < candidates.Length; i++)
|
||||
{
|
||||
var task = candidates[i].Client.Query(query, cts.Token);
|
||||
task.Forget();
|
||||
tasks[i] = task;
|
||||
}
|
||||
|
||||
var winnerTask = await tasks.WhenAnySuccessAsync().ConfigureAwait(false);
|
||||
if (winnerTask is null) throw new InvalidOperationException("All queries failed.");
|
||||
|
||||
var winner = candidates[Array.IndexOf(tasks, winnerTask)].Client;
|
||||
|
||||
if (_updateWeigh)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
for (var i = 0; i < _clients.Length; i++)
|
||||
{
|
||||
if (_clients[i].Client == winner)
|
||||
{
|
||||
if (_clients[i].Weight < 255)
|
||||
_clients[i] = (_clients[i].Client, (byte)(_clients[i].Weight + 1));
|
||||
else
|
||||
_updateWeigh = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cts.Cancel();
|
||||
return await winnerTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var (client, _) in _clients)
|
||||
client?.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user