Ack Cut 1d (C#): stale generations are duplicates, a stale Connect is answered not obeyed, sessions draw their own sequence
A frame at or before the seeded watermark is a duplicate of an earlier generation and is acknowledged, never delivered again (serial compare within the receive window). A Connect that precedes the current generation's within the window is a stale copy of an earlier attempt: it is answered like a repeat (Accept owed or Ack), changes no state and does not refresh liveness. A Connect unanswered for 3 s is replaced by a fresh Connect with a newly drawn sequence and the same payload. In server mode a Connect from another endpoint is always a new generation. The initial sequence is unset by default and each session draws its own; the public random helper is removed. An accepted peer now stamps its liveness on accept, so one that goes silent times out. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e56c96d6be
commit
1569f013c6
4 changed files with 417 additions and 53 deletions
|
|
@ -543,8 +543,8 @@ namespace GameCult.Mesh
|
|||
public int BindPort { get; set; }
|
||||
/// <summary>Gets or sets a caller-owned bound socket.</summary>
|
||||
public Socket? Socket { get; set; }
|
||||
/// <summary>Gets or sets the first local packet sequence.</summary>
|
||||
public uint InitialSequence { get; set; } = CultNetRudpSession.RandomInitialSequence();
|
||||
/// <summary>Gets or sets the first local packet sequence. Unset, each session draws its own at random.</summary>
|
||||
public uint? InitialSequence { get; set; }
|
||||
/// <summary>Gets or sets the reliable resend delay in milliseconds.</summary>
|
||||
public long ResendDelayMs { get; set; } = 250;
|
||||
/// <summary>Gets or sets the advertised transport id.</summary>
|
||||
|
|
|
|||
|
|
@ -43,9 +43,10 @@ namespace GameCult.Networking
|
|||
/// </summary>
|
||||
public uint ConnectionId { get; set; } = 0x43554c54;
|
||||
/// <summary>
|
||||
/// Gets or sets the first local packet sequence for each accepted peer.
|
||||
/// Gets or sets the first local packet sequence for each accepted peer. Unset, each peer's
|
||||
/// session draws its own at random.
|
||||
/// </summary>
|
||||
public uint InitialSequence { get; set; } = CultNetRudpSession.RandomInitialSequence();
|
||||
public uint? InitialSequence { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the resend delay in milliseconds.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -768,9 +768,11 @@ namespace GameCult.Networking
|
|||
/// </summary>
|
||||
public uint ConnectionId { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the first local packet sequence.
|
||||
/// Gets or sets the first local packet sequence. Unset, each session draws its own at random:
|
||||
/// every session is a new identity to its peer, and an options object reused for several
|
||||
/// must not give them one sequence.
|
||||
/// </summary>
|
||||
public uint InitialSequence { get; set; } = CultNetRudpSession.RandomInitialSequence();
|
||||
public uint? InitialSequence { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the resend delay in milliseconds.
|
||||
/// </summary>
|
||||
|
|
@ -845,9 +847,11 @@ namespace GameCult.Networking
|
|||
/// </summary>
|
||||
public uint ConnectionId { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the first local packet sequence.
|
||||
/// Gets or sets the first local packet sequence. Unset, each session draws its own at random:
|
||||
/// every session is a new identity to its peer, and an options object reused for several
|
||||
/// must not give them one sequence.
|
||||
/// </summary>
|
||||
public uint InitialSequence { get; set; } = CultNetRudpSession.RandomInitialSequence();
|
||||
public uint? InitialSequence { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the resend delay in milliseconds.
|
||||
/// </summary>
|
||||
|
|
@ -882,6 +886,11 @@ namespace GameCult.Networking
|
|||
/// <summary>Maximum reliable packets admitted to the wire before acknowledgements advance the window.</summary>
|
||||
public const int ReliableSendWindowPackets = 32;
|
||||
private const int ReceivedSequenceWindow = 4096;
|
||||
// How long a client keeps retransmitting a Connect nobody answered before it abandons that
|
||||
// attempt for a fresh one. A Connect the server answers with an Ack, never an Accept, is one
|
||||
// the server judged stale (see AcceptConnect); retransmitting the same sequence would never
|
||||
// change its mind.
|
||||
private const long ConnectAttemptMs = 3_000;
|
||||
private sealed class PendingReliablePacket
|
||||
{
|
||||
public CultNetRudpPacket Packet { get; set; } = new CultNetRudpPacket();
|
||||
|
|
@ -914,6 +923,9 @@ namespace GameCult.Networking
|
|||
private uint? _connectSequence;
|
||||
// A Connect this side sent is unanswered. Only then is an Accept honoured.
|
||||
private bool _awaitingAccept;
|
||||
// When the unanswered Connect first went out; DueResends abandons it for a fresh attempt once
|
||||
// ConnectAttemptMs has passed.
|
||||
private long _connectStartedAtMs;
|
||||
private long? _lastReceivedAtMs;
|
||||
private uint? _highestReceivedSequence;
|
||||
private readonly HashSet<uint> _receivedSequences = new HashSet<uint>();
|
||||
|
|
@ -940,31 +952,43 @@ namespace GameCult.Networking
|
|||
ValidateLimits(options.InitialSequence, options.MaxPendingReliablePackets);
|
||||
|
||||
ConnectionId = options.ConnectionId;
|
||||
_nextSequence = options.InitialSequence;
|
||||
_nextSequence = options.InitialSequence ?? DrawSequence(1);
|
||||
ResendDelayMs = options.ResendDelayMs;
|
||||
_maxPendingReliablePackets = options.MaxPendingReliablePackets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A fresh default initial sequence, drawn from a secure source in [1, 2^31). The Connect's
|
||||
/// A sequence drawn from a secure source in [<paramref name="floor"/>, 2^31). The Connect's
|
||||
/// sequence is what tells a peer whether a Connect repeats one it already accepted or starts a
|
||||
/// new session, so two sessions must not share one by default.
|
||||
/// new session, so two sessions must not share one by default. A floor above the range draws
|
||||
/// nothing new: the caller keeps its own sequence.
|
||||
/// </summary>
|
||||
public static uint RandomInitialSequence()
|
||||
private static uint DrawSequence(uint floor)
|
||||
{
|
||||
floor = Math.Max(1u, floor);
|
||||
const uint ceiling = 1u << 31;
|
||||
if (floor >= ceiling)
|
||||
return floor;
|
||||
var bytes = new byte[4];
|
||||
using (var random = System.Security.Cryptography.RandomNumberGenerator.Create())
|
||||
random.GetBytes(bytes);
|
||||
return BitConverter.ToUInt32(bytes, 0) % int.MaxValue + 1;
|
||||
return floor + BitConverter.ToUInt32(bytes, 0) % (ceiling - floor);
|
||||
}
|
||||
|
||||
// Whether `sequence` is at or before `mark` in serial order, within the receive window. The
|
||||
// compare is modular, so it holds across the wrap of the 32-bit space; a sequence further back
|
||||
// than the window is the duplicate test's below-window clause, and one ahead of the mark is
|
||||
// never before it.
|
||||
private static bool AtOrBefore(uint sequence, uint mark) =>
|
||||
unchecked(mark - sequence) < ReceivedSequenceWindow;
|
||||
|
||||
internal static readonly byte[] RefusedPacketReason = Encoding.UTF8.GetBytes("session refused a packet");
|
||||
|
||||
/// <summary>
|
||||
/// A session cannot admit a Connect when its sequence space starts exhausted or its reliable
|
||||
/// queue holds nothing, so neither is a usable configuration.
|
||||
/// </summary>
|
||||
internal static void ValidateLimits(uint initialSequence, int? maxPendingReliablePackets)
|
||||
internal static void ValidateLimits(uint? initialSequence, int? maxPendingReliablePackets)
|
||||
{
|
||||
if (initialSequence == uint.MaxValue)
|
||||
{
|
||||
|
|
@ -1036,29 +1060,46 @@ namespace GameCult.Networking
|
|||
var packet = CreatePacket(CultNetRudpPacketType.Connect, "control", payload ?? Array.Empty<byte>(), reliable: true, ordered: true, sequenced: false);
|
||||
_connectSequence = packet.Sequence;
|
||||
_awaitingAccept = true;
|
||||
_connectStartedAtMs = nowMs;
|
||||
TrackReliable(packet, nowMs);
|
||||
return packet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether <paramref name="packet"/> is a retransmit of the Connect that started this session's
|
||||
/// current generation: the session is connected and the Connect carries that Connect's sequence.
|
||||
/// Servers that keep one session per peer ask this to tell a repeat from a new session; anything
|
||||
/// else that reaches <see cref="AcceptConnect"/> starts a new generation.
|
||||
/// Whether <paramref name="packet"/> is a Connect this session's current generation already owns:
|
||||
/// a retransmit of the Connect that started it (the session is connected and the sequence is
|
||||
/// that Connect's), or a stale copy of an earlier attempt by the same client (a sequence before
|
||||
/// it, within the receive window). Servers that keep one session per peer ask this to tell a
|
||||
/// Connect that starts a new session from one that does not; anything else that reaches
|
||||
/// <see cref="AcceptConnect"/> starts a new generation.
|
||||
/// </summary>
|
||||
public bool ConnectRepeats(CultNetRudpPacket packet)
|
||||
{
|
||||
if (packet == null) throw new ArgumentNullException(nameof(packet));
|
||||
return packet.PacketType == CultNetRudpPacketType.Connect
|
||||
&& _connected
|
||||
&& _connectSequence == packet.Sequence;
|
||||
&& _connectSequence.HasValue
|
||||
&& (_connectSequence.Value == packet.Sequence
|
||||
|| ConnectIsStale(packet.Sequence, _connectSequence.Value));
|
||||
}
|
||||
|
||||
// A Connect that precedes the current generation's within the receive window is the client's
|
||||
// earlier attempt, delayed in the network: a client that retried never sends a lower sequence
|
||||
// again. Restarting on it would strand the client, which honours only the Accept for its newest
|
||||
// Connect. The rule is TCP's answer to a delayed SYN (RFC 5961's challenge ACK): keep the
|
||||
// connection and answer with an Ack. A restarted client whose random initial sequence lands in
|
||||
// this window is answered the same way and abandons the attempt for a fresh draw
|
||||
// (ConnectAttemptMs).
|
||||
private static bool ConnectIsStale(uint sequence, uint current) =>
|
||||
sequence != current && AtOrBefore(sequence, current);
|
||||
|
||||
/// <summary>
|
||||
/// Answers a connect packet. A repeat of the accepted connect queues nothing, so a connect storm
|
||||
/// cannot grow the reliable queue: the reply is the accept still awaiting acknowledgement, or an
|
||||
/// ack once it was acknowledged. Any other connect ends the current generation, forgets the peer
|
||||
/// and accepts a new one with a reliable ordered accept packet.
|
||||
/// ack once it was acknowledged. A stale copy of an earlier attempt gets the same reply and
|
||||
/// changes nothing else: it is not evidence the peer is alive. Any other connect ends the
|
||||
/// current generation, forgets the peer and accepts a new one with a reliable ordered accept
|
||||
/// packet.
|
||||
/// </summary>
|
||||
public CultNetRudpPacket AcceptConnect(CultNetRudpPacket packet, long nowMs = 0, byte[]? payload = null)
|
||||
{
|
||||
|
|
@ -1069,10 +1110,19 @@ namespace GameCult.Networking
|
|||
}
|
||||
|
||||
if (ConnectRepeats(packet))
|
||||
return AnswerRepeatedConnect(packet, nowMs);
|
||||
{
|
||||
if (_connectSequence == packet.Sequence)
|
||||
{
|
||||
ApplyAcknowledgements(packet);
|
||||
RememberReceived(packet.Sequence);
|
||||
_lastReceivedAtMs = nowMs;
|
||||
}
|
||||
return PendingAcceptForResend(nowMs) ?? CreateAck();
|
||||
}
|
||||
ResetPeerState();
|
||||
EnsureReliableCapacity(1);
|
||||
SeedReceived(packet.Sequence);
|
||||
_lastReceivedAtMs = nowMs;
|
||||
_connectSequence = packet.Sequence;
|
||||
_connected = true;
|
||||
_ended = false;
|
||||
|
|
@ -1081,11 +1131,9 @@ namespace GameCult.Networking
|
|||
return response;
|
||||
}
|
||||
|
||||
private CultNetRudpPacket AnswerRepeatedConnect(CultNetRudpPacket packet, long nowMs)
|
||||
// The Accept still awaiting acknowledgement, resent.
|
||||
private CultNetRudpPacket? PendingAcceptForResend(long nowMs)
|
||||
{
|
||||
ApplyAcknowledgements(packet);
|
||||
RememberReceived(packet.Sequence);
|
||||
_lastReceivedAtMs = nowMs;
|
||||
lock (_pendingReliableGate)
|
||||
{
|
||||
foreach (var pending in _pendingReliable.Values)
|
||||
|
|
@ -1097,7 +1145,7 @@ namespace GameCult.Networking
|
|||
}
|
||||
}
|
||||
|
||||
return CreateAck();
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1265,11 +1313,7 @@ namespace GameCult.Networking
|
|||
return new CultNetRudpReceiveResult { ReadyToSend = readyToSend };
|
||||
}
|
||||
|
||||
var duplicate = packet.Reliable
|
||||
&& (_receivedSequences.Contains(packet.Sequence)
|
||||
|| (_highestReceivedSequence.HasValue
|
||||
&& packet.Sequence < _highestReceivedSequence.Value
|
||||
&& _highestReceivedSequence.Value - packet.Sequence >= ReceivedSequenceWindow));
|
||||
var duplicate = packet.Reliable && WasReceived(packet.Sequence);
|
||||
if (packet.Reliable)
|
||||
{
|
||||
RememberReceived(packet.Sequence);
|
||||
|
|
@ -1435,6 +1479,9 @@ namespace GameCult.Networking
|
|||
/// </summary>
|
||||
public IReadOnlyList<CultNetRudpPacket> DueResends(long nowMs)
|
||||
{
|
||||
var fresh = AbandonUnansweredConnect(nowMs);
|
||||
if (fresh != null)
|
||||
return new[] { fresh };
|
||||
var due = new List<CultNetRudpPacket>();
|
||||
lock (_pendingReliableGate)
|
||||
{
|
||||
|
|
@ -1613,6 +1660,26 @@ namespace GameCult.Networking
|
|||
return false;
|
||||
}
|
||||
|
||||
// A Connect unanswered for ConnectAttemptMs is replaced, not retransmitted further, by a Connect
|
||||
// with a newly drawn sequence and the same payload. Nothing was ever sent in an unanswered
|
||||
// generation, so no sequence issued so far is owed; the draw stays above them, so a frame the
|
||||
// peer still remembers from an earlier generation of this session stays at or before the new
|
||||
// Connect.
|
||||
private CultNetRudpPacket? AbandonUnansweredConnect(long nowMs)
|
||||
{
|
||||
if (!_awaitingAccept || nowMs - _connectStartedAtMs < ConnectAttemptMs)
|
||||
return null;
|
||||
byte[] payload;
|
||||
lock (_pendingReliableGate)
|
||||
{
|
||||
if (!_connectSequence.HasValue || !_pendingReliable.TryGetValue(_connectSequence.Value, out var pending))
|
||||
return null;
|
||||
payload = pending.Packet.Payload ?? Array.Empty<byte>();
|
||||
}
|
||||
_nextSequence = DrawSequence(_nextSequence);
|
||||
return CreateConnect(nowMs, payload);
|
||||
}
|
||||
|
||||
// The handshake's one act on the watermark: the peer's Connect or Accept is the first sequence
|
||||
// of the session, whatever else it has sent.
|
||||
private void SeedReceived(uint sequence)
|
||||
|
|
@ -1621,14 +1688,18 @@ namespace GameCult.Networking
|
|||
RememberReceived(sequence);
|
||||
}
|
||||
|
||||
// True for a sequence in the window that was received, and for one below the window, which
|
||||
// Receive treats as a duplicate of something received.
|
||||
// The one duplicate test: true for a sequence in the window that was received, for one below
|
||||
// the window, and for one at or before the watermark. The watermark starts at the handshake's
|
||||
// seed, so a frame the peer sent in an earlier generation (every sequence it issued is below
|
||||
// the Connect that began this one) is a duplicate of something already delivered and is
|
||||
// acknowledged, never delivered again.
|
||||
private bool WasReceived(uint sequence)
|
||||
{
|
||||
return _receivedSequences.Contains(sequence)
|
||||
|| (_highestReceivedSequence.HasValue
|
||||
&& sequence < _highestReceivedSequence.Value
|
||||
&& _highestReceivedSequence.Value - sequence >= ReceivedSequenceWindow);
|
||||
&& _highestReceivedSequence.Value - sequence >= ReceivedSequenceWindow)
|
||||
|| (_receivedThrough.HasValue && AtOrBefore(sequence, _receivedThrough.Value));
|
||||
}
|
||||
|
||||
private void RememberReceived(uint sequence)
|
||||
|
|
@ -2282,6 +2353,10 @@ namespace GameCult.Networking
|
|||
_stats.PacketsDropped++;
|
||||
return true;
|
||||
}
|
||||
// A Connect from another endpoint is a new client, whatever sequence it carries: the endpoint
|
||||
// moves and the Connect starts a new generation. Only a Connect from the peer's own
|
||||
// endpoint can be its repeat.
|
||||
var connectFromNewEndpoint = false;
|
||||
if (_remoteEndPoint == null)
|
||||
{
|
||||
// Only a Connect claims the endpoint of a server-mode transport.
|
||||
|
|
@ -2295,11 +2370,10 @@ namespace GameCult.Networking
|
|||
}
|
||||
else if (!_remoteEndPoint.Equals(remote))
|
||||
{
|
||||
// A Connect from another endpoint claims a server-mode transport for it: the session
|
||||
// decides whether it starts a new generation.
|
||||
if (_mode == CultNetRudpSocketMode.Server && packet.PacketType == CultNetRudpPacketType.Connect)
|
||||
{
|
||||
_remoteEndPoint = remote;
|
||||
connectFromNewEndpoint = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -2318,6 +2392,8 @@ namespace GameCult.Networking
|
|||
{
|
||||
// The session answers a repeat of the connect it accepted with the accept
|
||||
// already owed, and anything else with a new generation.
|
||||
if (connectFromNewEndpoint)
|
||||
_session.ResetPeerState();
|
||||
accept = _session.AcceptConnect(packet, NowMs());
|
||||
DisconnectReason = null;
|
||||
}
|
||||
|
|
@ -2485,9 +2561,10 @@ namespace GameCult.Networking
|
|||
/// </summary>
|
||||
public uint ConnectionId { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the first local packet sequence for each accepted peer session.
|
||||
/// Gets or sets the first local packet sequence for each accepted peer session. Unset, each
|
||||
/// session draws its own at random.
|
||||
/// </summary>
|
||||
public uint InitialSequence { get; set; } = CultNetRudpSession.RandomInitialSequence();
|
||||
public uint? InitialSequence { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the resend delay in milliseconds.
|
||||
/// </summary>
|
||||
|
|
@ -2579,7 +2656,7 @@ namespace GameCult.Networking
|
|||
private const int WireBurstPackets = 32;
|
||||
private readonly Socket _socket;
|
||||
private readonly uint _connectionId;
|
||||
private readonly uint _initialSequence;
|
||||
private readonly uint? _initialSequence;
|
||||
private readonly long _resendDelayMs;
|
||||
private readonly int? _maxFragmentBytes;
|
||||
private readonly int? _maxPendingReliablePackets;
|
||||
|
|
|
|||
|
|
@ -279,22 +279,308 @@ namespace GameCult.Networking.Tests
|
|||
Assert.That(server.Receive(s2, 4).Delivered, Is.Empty, "the Disconnect forgot what was received");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The options say nothing about the initial sequence; each session draws its own, so one options
|
||||
/// object serves many sessions without giving them one sequence.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void DefaultInitialSequencesAreDrawnAtRandomFromOneToTwoToTheThirtyFirst()
|
||||
public void SessionsFromOneOptionsObjectEachDrawTheirOwnInitialSequence()
|
||||
{
|
||||
static void AssertRandom(string label, Func<uint> draw)
|
||||
{
|
||||
var draws = Enumerable.Range(0, 64).Select(_ => draw()).ToArray();
|
||||
Assert.That(draws.Distinct().Count(), Is.GreaterThan(32), $"{label} does not draw at random");
|
||||
Assert.That(draws.All(value => value >= 1 && value < (1u << 31)), Is.True, $"{label} left [1, 2^31)");
|
||||
}
|
||||
Assert.That(new CultNetRudpSessionOptions().InitialSequence, Is.Null);
|
||||
Assert.That(new CultNetRudpSocketTransportOptions().InitialSequence, Is.Null);
|
||||
Assert.That(new CultNetRudpSocketTransportServerOptions().InitialSequence, Is.Null);
|
||||
Assert.That(new RudpCultNetSchemaServerOptions().InitialSequence, Is.Null);
|
||||
Assert.That(new CultMeshRudpSocketOptions().InitialSequence, Is.Null);
|
||||
|
||||
AssertRandom("RandomInitialSequence", CultNetRudpSession.RandomInitialSequence);
|
||||
AssertRandom("session options", () => new CultNetRudpSessionOptions().InitialSequence);
|
||||
AssertRandom("socket transport options", () => new CultNetRudpSocketTransportOptions().InitialSequence);
|
||||
AssertRandom("listener options", () => new CultNetRudpSocketTransportServerOptions().InitialSequence);
|
||||
AssertRandom("schema server options", () => new RudpCultNetSchemaServerOptions().InitialSequence);
|
||||
AssertRandom("cultmesh options", () => new CultMeshRudpSocketOptions().InitialSequence);
|
||||
var options = new CultNetRudpSessionOptions { ConnectionId = ConnectionId };
|
||||
var firsts = Enumerable.Range(0, 64)
|
||||
.Select(_ => new CultNetRudpSession(options).CreateConnect(0).Sequence)
|
||||
.ToArray();
|
||||
Assert.That(firsts.Distinct().Count(), Is.GreaterThan(32), "a default session does not draw its own sequence");
|
||||
Assert.That(firsts.All(value => value >= 1 && value < (1u << 31)), Is.True, "a draw left [1, 2^31)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A transport built from the same options each time (a reconnect loop's factory) opens each
|
||||
/// connection with its own Connect sequence.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void AReusedOptionsObjectYieldsADifferentConnectSequenceForEachTransport()
|
||||
{
|
||||
using var listener = Bind();
|
||||
using var clientSocket = Bind();
|
||||
var options = new CultNetRudpSocketTransportOptions
|
||||
{
|
||||
RuntimeId = "csharp-rudp-client",
|
||||
Socket = clientSocket,
|
||||
Mode = CultNetRudpSocketMode.Client,
|
||||
RemoteEndPoint = listener.LocalEndPoint,
|
||||
ConnectionId = ConnectionId
|
||||
};
|
||||
var sequences = new HashSet<uint>();
|
||||
for (var index = 0; index < 8; index++)
|
||||
{
|
||||
var client = new CultNetRudpSocketTransportConnection(options);
|
||||
client.Connect("hello");
|
||||
sequences.Add(Drain(listener).Last(p => p.PacketType == CultNetRudpPacketType.Connect).Sequence);
|
||||
}
|
||||
Assert.That(sequences, Has.Count.EqualTo(8), "two connections opened with one Connect sequence");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A listener builds a session for every client it admits; each draws its own sequence, so no two
|
||||
/// clients are accepted with the same one.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TheListenerAcceptsEachClientWithItsOwnSequence()
|
||||
{
|
||||
using var serverSocket = Bind();
|
||||
using var server = new CultNetRudpSocketTransportServer(new CultNetRudpSocketTransportServerOptions
|
||||
{
|
||||
RuntimeId = "csharp-rudp-listener",
|
||||
Socket = serverSocket,
|
||||
ConnectionId = ConnectionId
|
||||
});
|
||||
var accepts = new HashSet<uint>();
|
||||
for (var index = 0; index < 8; index++)
|
||||
{
|
||||
using var peerSocket = Bind();
|
||||
peerSocket.SendTo(CultNetRudpPacketCodec.Encode(Session(1).CreateConnect(0)), serverSocket.LocalEndPoint!);
|
||||
server.ReceiveOnce();
|
||||
accepts.Add(Drain(peerSocket).First(p => p.PacketType == CultNetRudpPacketType.Accept).Sequence);
|
||||
}
|
||||
Assert.That(accepts, Has.Count.EqualTo(8), "two clients were accepted with one sequence");
|
||||
}
|
||||
|
||||
// Ack Cut 1d: a frame from an earlier generation is a duplicate, a delayed Connect from an
|
||||
// earlier attempt is stale, a Connect from a new endpoint is a new client.
|
||||
|
||||
private static bool NamesSequence(CultNetRudpPacket ack, uint sequence) =>
|
||||
ack.Ack == sequence
|
||||
|| Enumerable.Range(0, 32).Any(bit => (ack.AckMask & (1u << bit)) != 0 && ack.Ack > bit && ack.Ack - (uint)bit - 1 == sequence);
|
||||
|
||||
/// <summary>
|
||||
/// A frame the client's old generation already delivered, retransmitted after the client
|
||||
/// reconnected, is acknowledged and not delivered into the new generation.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void AFrameFromTheClientsEarlierGenerationIsAcknowledgedNotDeliveredAgain()
|
||||
{
|
||||
var client = Session(10);
|
||||
var server = Session(500);
|
||||
Handshake(client, server);
|
||||
var old = Send(client, "old");
|
||||
Assert.That(Names(server.Receive(old, 1)), Is.EqualTo(new[] { "old" }));
|
||||
|
||||
var connect = client.CreateConnect(2);
|
||||
client.Receive(server.AcceptConnect(connect, 2), 2);
|
||||
Assert.That(old.Sequence, Is.LessThan(connect.Sequence));
|
||||
Assert.That(server.Receive(old, 3).Delivered, Is.Empty, "an earlier generation's frame was delivered again");
|
||||
Assert.That(NamesSequence(server.CreateAckForReceived(old.Sequence), old.Sequence), Is.True, "the duplicate was not acknowledged");
|
||||
Assert.That(Names(server.Receive(Send(client, "fresh"), 4)), Is.EqualTo(new[] { "fresh" }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AFrameFromTheServersEarlierGenerationIsAcknowledgedNotDeliveredAgain()
|
||||
{
|
||||
var client = Session(10);
|
||||
var server = Session(500);
|
||||
Handshake(client, server);
|
||||
var old = Send(server, "old");
|
||||
Assert.That(Names(client.Receive(old, 1)), Is.EqualTo(new[] { "old" }));
|
||||
|
||||
client.Receive(server.AcceptConnect(client.CreateConnect(2), 2), 2);
|
||||
Assert.That(client.Receive(old, 3).Delivered, Is.Empty, "an earlier generation's frame was delivered again");
|
||||
Assert.That(NamesSequence(client.CreateAckForReceived(old.Sequence), old.Sequence), Is.True);
|
||||
Assert.That(Names(client.Receive(Send(server, "fresh"), 4)), Is.EqualTo(new[] { "fresh" }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ADuplicateBelowTheReceiveWindowIsAcknowledgedByName()
|
||||
{
|
||||
var client = Session(10);
|
||||
var server = Session(500);
|
||||
Handshake(client, server);
|
||||
var first = Send(client, "first");
|
||||
Assert.That(Names(server.Receive(first, 1)), Is.EqualTo(new[] { "first" }));
|
||||
for (var index = 0; index < 4_200; index++)
|
||||
{
|
||||
server.Receive(Send(client, "x"), 1);
|
||||
client.Receive(server.CreateAck(), 1);
|
||||
}
|
||||
Assert.That(server.Receive(first, 2).Delivered, Is.Empty);
|
||||
Assert.That(server.CreateAckForReceived(first.Sequence).Ack, Is.EqualTo(first.Sequence), "a duplicate below the window was not acknowledged by name");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A Connect delayed from an earlier attempt of the client that is now connected does not
|
||||
/// restart the session: the server answers with an Ack and the client's ordered frames keep
|
||||
/// flowing.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ADelayedConnectFromAnEarlierAttemptDoesNotStrandTheClient()
|
||||
{
|
||||
var client = Session(10);
|
||||
var server = Session(500);
|
||||
var earlier = client.CreateConnect(0);
|
||||
var retried = client.CreateConnect(300);
|
||||
var accept = server.AcceptConnect(retried, 301);
|
||||
client.Receive(accept, 302);
|
||||
server.Receive(client.CreateAckForReceived(accept.Sequence), 302);
|
||||
Assert.That(client.Connected, Is.True);
|
||||
var a = Send(client, "a");
|
||||
Assert.That(Names(server.Receive(a, 303)), Is.EqualTo(new[] { "a" }));
|
||||
client.Receive(server.CreateAckForReceived(a.Sequence), 303);
|
||||
|
||||
Assert.That(server.ConnectRepeats(earlier), Is.True, "the delayed Connect starts nothing");
|
||||
var reply = server.AcceptConnect(earlier, 304);
|
||||
Assert.That(reply.PacketType, Is.EqualTo(CultNetRudpPacketType.Ack));
|
||||
client.Receive(reply, 305);
|
||||
Assert.That(client.Connected, Is.True);
|
||||
|
||||
var b = Send(client, "b");
|
||||
Assert.That(Names(server.Receive(b, 306)), Is.EqualTo(new[] { "b" }), "the delayed Connect reset the server");
|
||||
client.Receive(server.CreateAckForReceived(b.Sequence), 307);
|
||||
Assert.That(client.PendingReliableSequences, Does.Not.Contain(b.Sequence));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A stale Connect is the peer's echo from the past, not evidence it is alive; a retransmit of
|
||||
/// the accepted Connect is.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ARepeatedConnectRefreshesLivenessAndAStaleOneDoesNot()
|
||||
{
|
||||
var client = Session(10);
|
||||
var server = Session(500);
|
||||
var earlier = client.CreateConnect(0);
|
||||
var current = client.CreateConnect(1);
|
||||
server.AcceptConnect(current, 0);
|
||||
|
||||
server.AcceptConnect(current, 900);
|
||||
Assert.That(server.CheckTimeout(1_000, 500), Is.False, "a repeated Connect did not refresh liveness");
|
||||
|
||||
server.AcceptConnect(earlier, 1_400);
|
||||
Assert.That(server.CheckTimeout(1_600, 500), Is.True, "a stale Connect refreshed liveness");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An accepted peer that never speaks again times out: accepting a Connect is the first thing
|
||||
/// heard from it.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void AnAcceptedPeerThatGoesSilentTimesOut()
|
||||
{
|
||||
var server = Session(500);
|
||||
server.AcceptConnect(Session(10).CreateConnect(0), 0);
|
||||
Assert.That(server.CheckTimeout(100_000, 1_000), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StaleConnectsAreRecognisedBySerialArithmetic()
|
||||
{
|
||||
var server = Session(500);
|
||||
server.AcceptConnect(Session(3).CreateConnect(0), 0);
|
||||
CultNetRudpPacket Connect(uint sequence) => Session(sequence).CreateConnect(0);
|
||||
Assert.That(server.ConnectRepeats(Connect(uint.MaxValue - 1)), Is.True, "just before the wrap");
|
||||
Assert.That(server.ConnectRepeats(Connect(4)), Is.False, "just after");
|
||||
Assert.That(server.ConnectRepeats(Connect(3 + 4_096)), Is.False, "far after");
|
||||
Assert.That(server.ConnectRepeats(Connect(unchecked(3u - 4_095))), Is.True, "the window's edge");
|
||||
Assert.That(server.ConnectRepeats(Connect(unchecked(3u - 4_096))), Is.False, "past the window");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A restarted client whose random first sequence lands just before the server's current
|
||||
/// Connect is answered with an Ack, never an Accept. It does not retransmit that Connect for
|
||||
/// ever: once the attempt times out it starts a fresh one with a newly drawn sequence, and is
|
||||
/// admitted.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ARestartedClientWhoseFirstSequenceIsStaleConnectsAfterRedrawing()
|
||||
{
|
||||
var server = Session(500);
|
||||
var old = Session(1_000);
|
||||
var oldAccept = server.AcceptConnect(old.CreateConnect(0), 0);
|
||||
old.Receive(oldAccept, 0);
|
||||
|
||||
var restarted = Session(900);
|
||||
var first = restarted.CreateConnect(0, Encoding.UTF8.GetBytes("join"));
|
||||
// The old client has not acknowledged its Accept yet, so that is the reply: it names the
|
||||
// old Connect, not this one.
|
||||
var early = server.AcceptConnect(first, 1);
|
||||
Assert.That(early.PacketType, Is.EqualTo(CultNetRudpPacketType.Accept));
|
||||
restarted.Receive(early, 1);
|
||||
Assert.That(restarted.Connected, Is.False, "an Accept for another Connect connected the client");
|
||||
server.Receive(old.CreateAckForReceived(oldAccept.Sequence), 1);
|
||||
var reply = server.AcceptConnect(first, 2);
|
||||
Assert.That(reply.PacketType, Is.EqualTo(CultNetRudpPacketType.Ack));
|
||||
restarted.Receive(reply, 2);
|
||||
Assert.That(restarted.Connected, Is.False);
|
||||
|
||||
var retransmitted = restarted.DueResends(1_000);
|
||||
Assert.That(retransmitted, Has.Count.EqualTo(1));
|
||||
Assert.That(retransmitted[0].Sequence, Is.EqualTo(first.Sequence), "the attempt is still young");
|
||||
|
||||
var fresh = restarted.DueResends(3_500);
|
||||
Assert.That(fresh, Has.Count.EqualTo(1));
|
||||
Assert.That(fresh[0].PacketType, Is.EqualTo(CultNetRudpPacketType.Connect));
|
||||
Assert.That(fresh[0].Sequence, Is.Not.EqualTo(first.Sequence), "the same Connect was retransmitted for ever");
|
||||
Assert.That(Encoding.UTF8.GetString(fresh[0].Payload), Is.EqualTo("join"), "the fresh attempt lost the Connect payload");
|
||||
|
||||
var accept = server.AcceptConnect(fresh[0], 3_500);
|
||||
Assert.That(accept.PacketType, Is.EqualTo(CultNetRudpPacketType.Accept));
|
||||
restarted.Receive(accept, 3_500);
|
||||
Assert.That(restarted.Connected, Is.True);
|
||||
Assert.That(Names(server.Receive(Send(restarted, "hello"), 3_501)), Is.EqualTo(new[] { "hello" }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnAnsweredConnectIsNeverReplacedByTheAttemptTimeout()
|
||||
{
|
||||
var client = Session(10);
|
||||
var server = Session(500);
|
||||
var (connect, _) = Handshake(client, server);
|
||||
var resent = client.DueResends(60_000);
|
||||
Assert.That(resent.All(p => p.PacketType != CultNetRudpPacketType.Connect || p.Sequence == connect.Sequence), Is.True, "a connected client started a new attempt");
|
||||
Assert.That(client.Connected, Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A pinned client that restarts on a new port against a server-mode transport is a new client:
|
||||
/// the Connect starts a new generation and the client is admitted, though its sequence is the
|
||||
/// one the old client's Connect carried.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ServerModeAdmitsAPinnedClientThatRestartsOnANewPort()
|
||||
{
|
||||
using var serverSocket = Bind();
|
||||
using var firstSocket = Bind();
|
||||
using var secondSocket = Bind();
|
||||
using var server = new CultNetRudpSocketTransportConnection(new CultNetRudpSocketTransportOptions
|
||||
{
|
||||
RuntimeId = "csharp-rudp-server",
|
||||
Socket = serverSocket,
|
||||
Mode = CultNetRudpSocketMode.Server,
|
||||
ConnectionId = ConnectionId
|
||||
});
|
||||
var serverEndPoint = serverSocket.LocalEndPoint!;
|
||||
void ToServer(Socket from, CultNetRudpPacket packet) => from.SendTo(CultNetRudpPacketCodec.Encode(packet), serverEndPoint);
|
||||
|
||||
var first = Session(1);
|
||||
ToServer(firstSocket, first.CreateConnect(0, Encoding.UTF8.GetBytes("join")));
|
||||
server.ReceiveOnce();
|
||||
first.Receive(Drain(firstSocket).First(p => p.PacketType == CultNetRudpPacketType.Accept), 0);
|
||||
ToServer(firstSocket, Send(first, "hello"));
|
||||
Assert.That(Encoding.UTF8.GetString(server.ReceiveOnce()!.Payload), Is.EqualTo("hello"));
|
||||
|
||||
var second = Session(1);
|
||||
ToServer(secondSocket, second.CreateConnect(0, Encoding.UTF8.GetBytes("join")));
|
||||
server.ReceiveOnce();
|
||||
var accepts = Drain(secondSocket).Where(p => p.PacketType == CultNetRudpPacketType.Accept).ToArray();
|
||||
Assert.That(accepts, Is.Not.Empty, "the restarted client on a new port was not admitted");
|
||||
second.Receive(accepts[0], 0);
|
||||
ToServer(secondSocket, Send(second, "after"));
|
||||
Assert.That(Encoding.UTF8.GetString(server.ReceiveOnce()!.Payload), Is.EqualTo("after"));
|
||||
}
|
||||
|
||||
// Transports.
|
||||
|
|
@ -315,7 +601,7 @@ namespace GameCult.Networking.Tests
|
|||
var serverEndPoint = serverSocket.LocalEndPoint!;
|
||||
void ToServer(Socket from, CultNetRudpPacket packet) => from.SendTo(CultNetRudpPacketCodec.Encode(packet), serverEndPoint);
|
||||
|
||||
var first = Session(50);
|
||||
var first = Session(50_000);
|
||||
var connect = first.CreateConnect(0, Encoding.UTF8.GetBytes("same"));
|
||||
ToServer(peerSocket, connect);
|
||||
server.ReceiveOnce();
|
||||
|
|
@ -367,7 +653,7 @@ namespace GameCult.Networking.Tests
|
|||
var serverEndPoint = serverSocket.LocalEndPoint!;
|
||||
void ToServer(CultNetRudpPacket packet) => peerSocket.SendTo(CultNetRudpPacketCodec.Encode(packet), serverEndPoint);
|
||||
|
||||
var first = Session(50);
|
||||
var first = Session(50_000);
|
||||
ToServer(first.CreateConnect(0, Encoding.UTF8.GetBytes("same")));
|
||||
server.ReceiveOnce();
|
||||
first.Receive(Drain(peerSocket).First(p => p.PacketType == CultNetRudpPacketType.Accept), 0);
|
||||
|
|
|
|||
Loading…
Reference in a new issue