refactor!: rename the project from Monitor to MoShiro [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful
ci/woodpecker/push/main-amd64-build Pipeline was successful
Namespaces, assemblies, solution, binaries, images, systemd units, config paths, environment variables, protocol methods and the Linux user are all renamed. Breaking: existing databases, node identities and installed units must be recreated.
This commit is contained in:
1 parent
27ac7fb787
commit
2e146d4da7
309 files changed
+1565
-1572
No files matched your search
@@ -0,0 +1,337 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using MoShiro.Protocol;
|
||||
|
||||
namespace MoShiro.Measurement;
|
||||
|
||||
/// <summary>拨号方的一条数据面 TCP 连接(已经完成 TLS 和首帧认证)。</summary>
|
||||
public sealed class TcpLink : IDisposable
|
||||
{
|
||||
private readonly TcpClient _client;
|
||||
|
||||
private TcpLink(TcpClient client, Stream stream, long tcpUs, long? tlsUs)
|
||||
{
|
||||
_client = client;
|
||||
Socket = client.Client;
|
||||
Stream = stream;
|
||||
TcpUs = tcpUs;
|
||||
TlsUs = tlsUs;
|
||||
}
|
||||
|
||||
public Stream Stream { get; }
|
||||
|
||||
/// <summary>底层套接字(内核采样用)。</summary>
|
||||
public Socket Socket { get; }
|
||||
|
||||
public long TcpUs { get; }
|
||||
|
||||
public long? TlsUs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 连接 → (TLS)→ 认证首帧。<paramref name="sendAuth"/> 为 false 时只测量建立连接的耗时(connect)。
|
||||
/// 对端证书按 <see cref="BlockEnv.PeerSpkiSha256"/> 固定;没有指纹(目标任务,目标是任意地址)就只测握手耗时、不校验证书。
|
||||
/// 失败时抛出 <see cref="SocketException"/>、<see cref="AuthenticationException"/>、<see cref="IOException"/> 或超时的 <see cref="OperationCanceledException"/>。
|
||||
/// </summary>
|
||||
public static async Task<TcpLink> OpenAsync(BlockEnv env, int port, bool tls, uint streamId, bool sendAuth, CancellationToken ct)
|
||||
{
|
||||
var client = new TcpClient { NoDelay = true };
|
||||
try
|
||||
{
|
||||
long start = env.Clock.NowUs();
|
||||
await client.ConnectAsync(env.PeerAddress!, port, ct);
|
||||
long tcpUs = env.Clock.NowUs() - start;
|
||||
Stream stream = client.GetStream();
|
||||
long? tlsUs = null;
|
||||
if (tls)
|
||||
{
|
||||
long tlsStart = env.Clock.NowUs();
|
||||
byte[]? pin = env.PeerSpkiSha256;
|
||||
#pragma warning disable CA5359 // 没有指纹时只测量握手耗时(目标任务的目标是任意地址)
|
||||
var ssl = new SslStream(stream, false, (_, certificate, _, _) => pin is null || (certificate is not null && Pinned(certificate, pin)));
|
||||
#pragma warning restore CA5359
|
||||
await ssl.AuthenticateAsClientAsync(new SslClientAuthenticationOptions { TargetHost = env.PeerAddress!.ToString() }, ct);
|
||||
stream = ssl;
|
||||
tlsUs = env.Clock.NowUs() - tlsStart;
|
||||
}
|
||||
|
||||
if (sendAuth)
|
||||
{
|
||||
DataSession session = env.Session() ?? throw new InvalidOperationException("no data session");
|
||||
byte[] auth = new byte[TcpWire.AuthBytes];
|
||||
Span<byte> nonce = stackalloc byte[TcpWire.NonceBytes];
|
||||
RandomNumberGenerator.Fill(nonce);
|
||||
TcpWire.WriteAuth(auth, session.SessionId, streamId, nonce, session.Key);
|
||||
await stream.WriteAsync(auth, ct);
|
||||
}
|
||||
|
||||
return new TcpLink(client, stream, tcpUs, tlsUs);
|
||||
}
|
||||
catch
|
||||
{
|
||||
client.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stream.Dispose();
|
||||
_client.Dispose();
|
||||
}
|
||||
|
||||
private static bool Pinned(X509Certificate certificate, byte[] expected)
|
||||
{
|
||||
X509Certificate2 full = certificate as X509Certificate2 ?? X509CertificateLoader.LoadCertificate(certificate.GetRawCertData());
|
||||
return CryptographicOperations.FixedTimeEquals(SHA256.HashData(full.PublicKey.ExportSubjectPublicKeyInfo()), expected);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>监听方收到的一条已经认证的数据面 TCP 连接。</summary>
|
||||
public sealed class TcpConnection
|
||||
{
|
||||
public TcpConnection(Stream stream, Socket socket, DataSession session, uint streamId)
|
||||
{
|
||||
Stream = stream;
|
||||
Socket = socket;
|
||||
Session = session;
|
||||
StreamId = streamId;
|
||||
}
|
||||
|
||||
public Stream Stream { get; }
|
||||
|
||||
/// <summary>底层套接字(内核采样用)。</summary>
|
||||
public Socket Socket { get; }
|
||||
|
||||
public DataSession Session { get; }
|
||||
|
||||
public uint StreamId { get; }
|
||||
}
|
||||
|
||||
public delegate Task TcpResponder(TcpConnection connection, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// 监听方的 TCP 数据面:每个端口一个监听器。连接的第一个字节是 0x16 就先做 TLS(节点自己的证书),否则直接是明文。
|
||||
/// 随后是认证首帧(<c>"MN" | ver | session_id | stream_id | nonce | mac</c>,随机数只能用一次),通过之后交给按 <c>stream_id</c> 登记的处理函数。
|
||||
/// 首帧在限定时间内没有到达或者校验失败,直接关闭连接并计数。
|
||||
/// </summary>
|
||||
public sealed class TcpAcceptor
|
||||
{
|
||||
private static readonly TimeSpan s_authTimeout = TimeSpan.FromSeconds(5);
|
||||
|
||||
private readonly IPAddress _bind;
|
||||
private readonly IReadOnlyList<int> _ports;
|
||||
private readonly X509Certificate2 _certificate;
|
||||
private readonly IDataSessions _sessions;
|
||||
private readonly DataPlaneCounters _counters;
|
||||
private readonly ConcurrentDictionary<uint, TcpResponder> _handlers = new();
|
||||
private readonly List<TcpListener> _listeners = [];
|
||||
|
||||
public TcpAcceptor(IPAddress bind, IReadOnlyList<int> ports, X509Certificate2 certificate, IDataSessions sessions, DataPlaneCounters counters)
|
||||
{
|
||||
_bind = bind;
|
||||
_ports = ports;
|
||||
_certificate = certificate;
|
||||
_sessions = sessions;
|
||||
_counters = counters;
|
||||
}
|
||||
|
||||
public IReadOnlyList<int> BoundPorts { get; private set; } = [];
|
||||
|
||||
public void Register(uint streamId, TcpResponder handler) => _handlers[streamId] = handler;
|
||||
|
||||
public void Unregister(uint streamId) => _handlers.TryRemove(streamId, out _);
|
||||
|
||||
/// <summary>开始监听(端口被占用等错误在这里抛出)。</summary>
|
||||
public void Bind()
|
||||
{
|
||||
var bound = new List<int>();
|
||||
foreach (int port in _ports)
|
||||
{
|
||||
var listener = new TcpListener(_bind, port);
|
||||
listener.Start(128);
|
||||
_listeners.Add(listener);
|
||||
bound.Add(((IPEndPoint)listener.LocalEndpoint).Port);
|
||||
}
|
||||
|
||||
BoundPorts = bound;
|
||||
}
|
||||
|
||||
public async Task RunAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(_listeners.Select(l => AcceptLoopAsync(l, ct)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (TcpListener listener in _listeners)
|
||||
{
|
||||
listener.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AcceptLoopAsync(TcpListener listener, CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
TcpClient client;
|
||||
try
|
||||
{
|
||||
client = await listener.AcceptTcpClientAsync(ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_ = Task.Run(() => HandleAsync(client, ct), CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleAsync(TcpClient client, CancellationToken ct)
|
||||
{
|
||||
using (client)
|
||||
{
|
||||
client.NoDelay = true;
|
||||
try
|
||||
{
|
||||
Stream stream = client.GetStream();
|
||||
using var authCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
authCts.CancelAfter(s_authTimeout);
|
||||
|
||||
byte[] first = new byte[1];
|
||||
if (await stream.ReadAsync(first, authCts.Token) != 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
stream = new PrefixedStream(stream, first[0]);
|
||||
if (first[0] == 0x16)
|
||||
{
|
||||
var ssl = new SslStream(stream, leaveInnerStreamOpen: false);
|
||||
await ssl.AuthenticateAsServerAsync(new SslServerAuthenticationOptions { ServerCertificate = _certificate }, authCts.Token);
|
||||
stream = ssl;
|
||||
}
|
||||
|
||||
byte[] auth = new byte[TcpWire.AuthBytes];
|
||||
await stream.ReadExactlyAsync(auth, authCts.Token);
|
||||
if (!TcpWire.TryParseAuth(auth, out long sessionId, out uint streamId, out ReadOnlySpan<byte> nonce)
|
||||
|| _sessions.Find(sessionId) is not { } session
|
||||
|| !_handlers.TryGetValue(streamId, out TcpResponder? handler)
|
||||
|| !TcpWire.VerifyAuth(auth, session.Key)
|
||||
|| !session.UseNonce(nonce))
|
||||
{
|
||||
_counters.DropInvalid();
|
||||
return;
|
||||
}
|
||||
|
||||
await handler(new TcpConnection(stream, client.Client, session, streamId), ct);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or SocketException or OperationCanceledException or ObjectDisposedException or AuthenticationException or InvalidDataException or EndOfStreamException)
|
||||
{
|
||||
// 连接被对端关闭、超时或者格式错误:直接结束这个连接
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>把已经读出的第一个字节放回流的开头(用于探测是 TLS 还是明文)。</summary>
|
||||
private sealed class PrefixedStream : Stream
|
||||
{
|
||||
private readonly Stream _inner;
|
||||
private byte? _prefix;
|
||||
|
||||
public PrefixedStream(Stream inner, byte prefix)
|
||||
{
|
||||
_inner = inner;
|
||||
_prefix = prefix;
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override long Length => throw new NotSupportedException();
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => throw new NotSupportedException();
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Flush() => _inner.Flush();
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken) => _inner.FlushAsync(cancellationToken);
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) => Read(buffer.AsSpan(offset, count));
|
||||
|
||||
public override int Read(Span<byte> buffer)
|
||||
{
|
||||
if (buffer.IsEmpty)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (_prefix is { } prefix)
|
||||
{
|
||||
buffer[0] = prefix;
|
||||
_prefix = null;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return _inner.Read(buffer);
|
||||
}
|
||||
|
||||
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (buffer.IsEmpty)
|
||||
{
|
||||
return ValueTask.FromResult(0);
|
||||
}
|
||||
|
||||
if (_prefix is { } prefix)
|
||||
{
|
||||
buffer.Span[0] = prefix;
|
||||
_prefix = null;
|
||||
return ValueTask.FromResult(1);
|
||||
}
|
||||
|
||||
return _inner.ReadAsync(buffer, cancellationToken);
|
||||
}
|
||||
|
||||
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count) => _inner.Write(buffer, offset, count);
|
||||
|
||||
public override void Write(ReadOnlySpan<byte> buffer) => _inner.Write(buffer);
|
||||
|
||||
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) => _inner.WriteAsync(buffer, cancellationToken);
|
||||
|
||||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _inner.WriteAsync(buffer, offset, count, cancellationToken);
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_inner.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Net.Sockets;
|
||||
using MoShiro.Protocol;
|
||||
|
||||
namespace MoShiro.Measurement;
|
||||
|
||||
/// <summary>内核对一条 TCP 连接的采样(Linux <c>TCP_INFO</c>)。</summary>
|
||||
public readonly record struct TcpInfoSample(long SrttUs, long RttVarUs, long Cwnd, long TotalRetrans, long SegsOut, long BytesAcked);
|
||||
|
||||
/// <summary>
|
||||
/// 读 Linux 的 <c>TCP_INFO</c>:通过 <see cref="Socket.GetRawSocketOption(int, int, Span{byte})"/>,不需要 P/Invoke(NativeAOT、glibc 和 musl 都一样)。
|
||||
/// 缓冲区先清零,并且只读内核实际返回的长度之内的字段:老内核返回的结构更短,缺的字段是 0,而不是垃圾值。
|
||||
/// 其他平台返回 false(没有这一类采样,指标在对应窗口里就是没有数据)。
|
||||
/// </summary>
|
||||
public static class TcpInfo
|
||||
{
|
||||
private const int SolTcp = 6;
|
||||
private const int TcpInfoOption = 11;
|
||||
|
||||
// struct tcp_info 的偏移(include/uapi/linux/tcp.h):8 字节的状态位之后是 u32 字段;total_retrans 之后是对齐到 8 字节的 u64 字段
|
||||
private const int RttOffset = 68;
|
||||
private const int RttVarOffset = 72;
|
||||
private const int SndCwndOffset = 80;
|
||||
private const int TotalRetransOffset = 100;
|
||||
private const int BytesAckedOffset = 120;
|
||||
private const int SegsOutOffset = 136;
|
||||
|
||||
/// <summary>读到 <c>segs_out</c> 为止(内核 ≥ 4.2)。</summary>
|
||||
public const int Length = SegsOutOffset + 4;
|
||||
|
||||
public static bool Supported => OperatingSystem.IsLinux();
|
||||
|
||||
public static bool TrySample(Socket socket, out TcpInfoSample sample)
|
||||
{
|
||||
sample = default;
|
||||
if (!Supported)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[Length];
|
||||
buffer.Clear();
|
||||
int returned = socket.GetRawSocketOption(SolTcp, TcpInfoOption, buffer);
|
||||
if (returned < RttVarOffset + 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> info = buffer[..Math.Min(returned, Length)];
|
||||
sample = new TcpInfoSample(
|
||||
U32(info, RttOffset),
|
||||
U32(info, RttVarOffset),
|
||||
U32(info, SndCwndOffset),
|
||||
U32(info, TotalRetransOffset),
|
||||
U32(info, SegsOutOffset),
|
||||
U64(info, BytesAckedOffset));
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is SocketException or ObjectDisposedException or PlatformNotSupportedException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static long U32(ReadOnlySpan<byte> info, int offset) => offset + 4 <= info.Length ? BinaryPrimitives.ReadUInt32LittleEndian(info[offset..]) : 0;
|
||||
|
||||
private static long U64(ReadOnlySpan<byte> info, int offset) => offset + 8 <= info.Length ? (long)BinaryPrimitives.ReadUInt64LittleEndian(info[offset..]) : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 一条连接上周期性的内核采样(<c>tcp_info</c> 流):每个采样一行 (ts, side, srtt, rttvar, cwnd, retrans_total, segs_out, bytes_acked)。
|
||||
/// 两端各采样自己的套接字:Probe 的重传反映上行方向,Agent 的重传反映下行方向(代理场景里大流量走这个方向)。
|
||||
/// 累计计数器在 Backend 按 side 分别求差。
|
||||
/// </summary>
|
||||
public sealed class TcpInfoRecorder
|
||||
{
|
||||
private readonly MonoClock _clock;
|
||||
private readonly long _side;
|
||||
|
||||
public TcpInfoRecorder(StreamSpec spec, Side side, MonoClock clock)
|
||||
{
|
||||
Rows = new RowStore(spec, side, 4096);
|
||||
_clock = clock;
|
||||
_side = side == Side.Probe ? 0 : 1;
|
||||
}
|
||||
|
||||
public RowStore Rows { get; }
|
||||
|
||||
/// <summary>每隔 <paramref name="interval"/> 采样一次,直到取消或套接字不可用(连接断开)。</summary>
|
||||
public async Task RunAsync(Socket socket, TimeSpan interval, CancellationToken ct)
|
||||
{
|
||||
if (!TcpInfo.Supported)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var timer = new PeriodicTimer(interval, _clock.Time);
|
||||
while (await timer.WaitForNextTickAsync(ct))
|
||||
{
|
||||
if (!TcpInfo.TrySample(socket, out TcpInfoSample s))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long key = Rows.Append();
|
||||
Rows.SetRow(key, [_clock.NowUs(), _side, s.SrttUs, s.RttVarUs, s.Cwnd, s.TotalRetrans, s.SegsOut, s.BytesAcked]);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 连接结束
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Fleet.Core;
|
||||
using MoShiro.Protocol;
|
||||
|
||||
namespace MoShiro.Measurement;
|
||||
|
||||
/// <summary>一个通过校验的 UDP 包,交给按 <c>stream_id</c> 登记的处理函数。处理函数在接收循环里同步执行,不能阻塞。</summary>
|
||||
public readonly ref struct UdpDelivery
|
||||
{
|
||||
private readonly Span<byte> _packet;
|
||||
private readonly Socket _socket;
|
||||
private readonly SocketAddress _source;
|
||||
private readonly DataPlaneCounters _counters;
|
||||
|
||||
internal UdpDelivery(UdpHeader header, Span<byte> packet, long receiveUs, DataSession session, Socket socket, SocketAddress source, DataPlaneCounters counters)
|
||||
{
|
||||
Header = header;
|
||||
_packet = packet;
|
||||
ReceiveUs = receiveUs;
|
||||
Session = session;
|
||||
_socket = socket;
|
||||
_source = source;
|
||||
_counters = counters;
|
||||
}
|
||||
|
||||
public UdpHeader Header { get; }
|
||||
|
||||
public ReadOnlySpan<byte> Packet => _packet;
|
||||
|
||||
/// <summary>包到达的时刻(本节点的单调时钟,微秒):从接收返回的那一刻取,不含校验的耗时。</summary>
|
||||
public long ReceiveUs { get; }
|
||||
|
||||
public DataSession Session { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 原地回复:同样的长度,只把 type 改成 <paramref name="type"/> 并重新计算 MAC,发回请求的实际源地址。
|
||||
/// 回复与请求等长,所以监听方不能被当成放大器。发送失败(源地址已经不可达)不抛出。
|
||||
/// </summary>
|
||||
public void ReplyAs(UdpPacketType type)
|
||||
{
|
||||
_packet[3] = (byte)type;
|
||||
UdpPacket.Seal(_packet, Session.Key);
|
||||
try
|
||||
{
|
||||
_socket.SendTo(_packet, SocketFlags.None, _source);
|
||||
_counters.PacketOut();
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
// 回复丢失由对端的缺失体现
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public delegate void UdpHandler(in UdpDelivery delivery);
|
||||
|
||||
/// <summary>
|
||||
/// UDP 数据面:一个或多个套接字,按 <c>stream_id</c> 把包分发给积木。校验顺序:长度、magic、version、type →
|
||||
/// 流和会话都存在 → MAC → (监听方)<see cref="IAdmission"/>(重放窗口、限速)。任何一步失败都静默丢弃并计数,不回复。
|
||||
/// 接收循环只做校验和分发,不分配对象。两端共用这一份代码:Agent 监听多个端口并带准入策略,Probe 每个 Agent 一个临时端口。
|
||||
/// </summary>
|
||||
public sealed class UdpMux : IDisposable
|
||||
{
|
||||
private const int WindowsUdpConnReset = -1744830452; // SIO_UDP_CONNRESET
|
||||
|
||||
private readonly IDataSessions _sessions;
|
||||
private readonly IAdmission? _admission;
|
||||
private readonly MonoClock _clock;
|
||||
private readonly DataPlaneCounters _counters;
|
||||
private readonly INodeLog _log;
|
||||
private readonly IPAddress _bind;
|
||||
private readonly IReadOnlyList<int> _ports;
|
||||
private readonly List<Socket> _sockets = [];
|
||||
private readonly ConcurrentDictionary<uint, UdpHandler> _handlers = new();
|
||||
|
||||
/// <param name="ports">要监听的端口(0 表示由系统分配);空表示一个临时端口。</param>
|
||||
public UdpMux(IDataSessions sessions, IAdmission? admission, MonoClock clock, DataPlaneCounters counters, INodeLog log, IPAddress bind, IReadOnlyList<int> ports)
|
||||
{
|
||||
_sessions = sessions;
|
||||
_admission = admission;
|
||||
_clock = clock;
|
||||
_counters = counters;
|
||||
_log = log;
|
||||
_bind = bind;
|
||||
_ports = ports.Count == 0 ? [0] : ports;
|
||||
}
|
||||
|
||||
public IReadOnlyList<int> BoundPorts { get; private set; } = [];
|
||||
|
||||
public DataPlaneCounters Counters => _counters;
|
||||
|
||||
/// <summary>绑定套接字(端口被占用等错误在这里抛出)。</summary>
|
||||
public void Bind()
|
||||
{
|
||||
var bound = new List<int>();
|
||||
foreach (int port in _ports)
|
||||
{
|
||||
var socket = new Socket(_bind.AddressFamily, SocketType.Dgram, ProtocolType.Udp) { ReceiveBufferSize = 4 << 20, SendBufferSize = 4 << 20 };
|
||||
if (_bind.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
{
|
||||
socket.DualMode = true;
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
// Windows 上对一个已关闭的端口发送之后,下一次接收会抛 ConnectionReset:关掉这个行为
|
||||
socket.IOControl(WindowsUdpConnReset, [0], null);
|
||||
}
|
||||
|
||||
socket.Bind(new IPEndPoint(_bind, port));
|
||||
bound.Add(((IPEndPoint)socket.LocalEndPoint!).Port);
|
||||
_sockets.Add(socket);
|
||||
}
|
||||
|
||||
BoundPorts = bound;
|
||||
}
|
||||
|
||||
public void Register(uint streamId, UdpHandler handler) => _handlers[streamId] = handler;
|
||||
|
||||
public void Unregister(uint streamId) => _handlers.TryRemove(streamId, out _);
|
||||
|
||||
/// <summary>从第一个套接字发出一个已经封好的包。发送失败(没有路由、缓冲区满)不抛出:丢包本身就是测量结果。</summary>
|
||||
public void Send(EndPoint to, ReadOnlySpan<byte> packet)
|
||||
{
|
||||
try
|
||||
{
|
||||
_sockets[0].SendTo(packet, SocketFlags.None, to);
|
||||
_counters.PacketOut();
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
// 记为丢失
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RunAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(_sockets.Select(s => ReceiveLoopAsync(s, ct)));
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// 套接字已释放:停止
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (Socket socket in _sockets)
|
||||
{
|
||||
socket.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReceiveLoopAsync(Socket socket, CancellationToken ct)
|
||||
{
|
||||
byte[] buffer = new byte[UdpPacket.MaxBytes + 64];
|
||||
var source = new SocketAddress(socket.AddressFamily);
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
int length;
|
||||
try
|
||||
{
|
||||
length = await socket.ReceiveFromAsync(buffer.AsMemory(), SocketFlags.None, source, ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
// 个别错误(例如对端的 ICMP 不可达)不能让接收循环退出
|
||||
_log.Warn("udp receive failed", ("err", ex.SocketErrorCode));
|
||||
continue;
|
||||
}
|
||||
|
||||
Dispatch(socket, buffer, length, source, _clock.NowUs());
|
||||
}
|
||||
}
|
||||
|
||||
private void Dispatch(Socket socket, byte[] buffer, int length, SocketAddress source, long receiveUs)
|
||||
{
|
||||
_counters.PacketIn();
|
||||
Span<byte> packet = buffer.AsSpan(0, length);
|
||||
if (!UdpPacket.TryParseHeader(packet, out UdpHeader header)
|
||||
|| !_handlers.TryGetValue(header.StreamId, out UdpHandler? handler)
|
||||
|| _sessions.Find(header.SessionId) is not { } session
|
||||
|| !UdpPacket.VerifyMac(packet, session.Key))
|
||||
{
|
||||
_counters.DropInvalid();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_admission is not null)
|
||||
{
|
||||
switch (_admission.AdmitPacket(session, header, source, receiveUs))
|
||||
{
|
||||
case Admission.Invalid:
|
||||
_counters.DropInvalid();
|
||||
return;
|
||||
case Admission.RateLimited:
|
||||
_counters.DropRateLimited();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
handler(new UdpDelivery(header, packet, receiveUs, session, socket, source, _counters));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user