feat(client): add rendezvous traversal coordinators (#12)
quality-gate / quality (push) Successful in 56s
quality-gate / quality (push) Successful in 56s
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
using System.Security.Cryptography;
|
||||
using LiteNetLib;
|
||||
|
||||
namespace FinalFactory.Rendezvous.Client;
|
||||
|
||||
public enum RendezvousConnectionState
|
||||
{
|
||||
Punching = 1,
|
||||
Connecting = 2,
|
||||
Connected = 3,
|
||||
Cancelled = 4,
|
||||
TimedOut = 5,
|
||||
Rejected = 6,
|
||||
ManagerStopped = 7,
|
||||
Disposed = 8,
|
||||
}
|
||||
|
||||
public sealed class RendezvousConnectionCompletedEventArgs(
|
||||
RendezvousConnectionState state,
|
||||
NetPeer? peer = null) : EventArgs
|
||||
{
|
||||
public RendezvousConnectionState State { get; } = state;
|
||||
public NetPeer? Peer { get; } = peer;
|
||||
}
|
||||
|
||||
public sealed class RendezvousCoordinatorOptions
|
||||
{
|
||||
public int MaximumPunchRequests { get; set; } = 5;
|
||||
public int MaximumAttemptChecksPerPoll { get; set; } = 128;
|
||||
public TimeSpan InitialPunchRetryDelay { get; set; } = TimeSpan.FromMilliseconds(200);
|
||||
public TimeSpan MaximumPunchRetryDelay { get; set; } = TimeSpan.FromSeconds(2);
|
||||
public TimeSpan ConnectionTicketLifetime { get; set; } = TimeSpan.FromSeconds(20);
|
||||
public double JitterRatio { get; set; } = 0.2;
|
||||
|
||||
internal RendezvousCoordinatorOptions CopyAndValidate()
|
||||
{
|
||||
if (MaximumPunchRequests is < 1 or > 20
|
||||
|| MaximumAttemptChecksPerPoll is < 1 or > 1_024
|
||||
|| InitialPunchRetryDelay < TimeSpan.FromMilliseconds(10)
|
||||
|| MaximumPunchRetryDelay < InitialPunchRetryDelay
|
||||
|| MaximumPunchRetryDelay > TimeSpan.FromSeconds(10)
|
||||
|| ConnectionTicketLifetime <= TimeSpan.Zero
|
||||
|| ConnectionTicketLifetime > TimeSpan.FromSeconds(20)
|
||||
|| JitterRatio is < 0 or > 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(RendezvousCoordinatorOptions));
|
||||
}
|
||||
|
||||
return new RendezvousCoordinatorOptions
|
||||
{
|
||||
MaximumPunchRequests = MaximumPunchRequests,
|
||||
MaximumAttemptChecksPerPoll = MaximumAttemptChecksPerPoll,
|
||||
InitialPunchRetryDelay = InitialPunchRetryDelay,
|
||||
MaximumPunchRetryDelay = MaximumPunchRetryDelay,
|
||||
ConnectionTicketLifetime = ConnectionTicketLifetime,
|
||||
JitterRatio = JitterRatio,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal interface IRendezvousCoordinatorClock
|
||||
{
|
||||
DateTimeOffset UtcNow { get; }
|
||||
}
|
||||
|
||||
internal sealed class SystemRendezvousCoordinatorClock : IRendezvousCoordinatorClock
|
||||
{
|
||||
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
internal static class RendezvousManagerGuard
|
||||
{
|
||||
internal static void Validate(
|
||||
NetManager manager,
|
||||
RendezvousNetListener networkEvents)
|
||||
{
|
||||
networkEvents.ValidateManager(manager);
|
||||
if (!manager.IsRunning)
|
||||
{
|
||||
throw new InvalidOperationException("The caller-owned LiteNetLib manager must be running.");
|
||||
}
|
||||
|
||||
if (!manager.NatPunchEnabled
|
||||
|| manager.UnsyncedEvents
|
||||
|| manager.NatPunchModule.UnsyncedEvents)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The caller-owned manager must enable NAT punching and synchronized event dispatch.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class RendezvousPunchRetrySchedule(
|
||||
RendezvousCoordinatorOptions options,
|
||||
IRendezvousCoordinatorClock clock)
|
||||
{
|
||||
public int RequestsSent { get; private set; }
|
||||
public DateTimeOffset NextRequestAt { get; private set; } = DateTimeOffset.MinValue;
|
||||
|
||||
public bool IsExhausted => RequestsSent >= options.MaximumPunchRequests;
|
||||
|
||||
public bool IsDue(DateTimeOffset now) => now >= NextRequestAt;
|
||||
|
||||
public void RecordRequest()
|
||||
{
|
||||
int exponent = Math.Min(RequestsSent, 30);
|
||||
RequestsSent++;
|
||||
double milliseconds = Math.Min(
|
||||
options.InitialPunchRetryDelay.TotalMilliseconds * Math.Pow(2, exponent),
|
||||
options.MaximumPunchRetryDelay.TotalMilliseconds);
|
||||
if (options.JitterRatio > 0)
|
||||
{
|
||||
Span<byte> random = stackalloc byte[1];
|
||||
RandomNumberGenerator.Fill(random);
|
||||
double unit = random[0] / 255d;
|
||||
double multiplier = 1 - options.JitterRatio + (2 * options.JitterRatio * unit);
|
||||
milliseconds = Math.Min(
|
||||
milliseconds * multiplier,
|
||||
options.MaximumPunchRetryDelay.TotalMilliseconds);
|
||||
}
|
||||
|
||||
NextRequestAt = clock.UtcNow + TimeSpan.FromMilliseconds(milliseconds);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user