Net: heartbeat liveness, protocol version, message cap, replication reset
CI / build-test (push) Successful in 1m12s
CI / build-test (push) Successful in 1m12s
Robustness pass over MrGameEng.Net: - WebSocketServer heartbeats each connection (configurable interval/timeout) and drops peers idle past the timeout — detects half-open TCP that vanished without a close frame. Tracks last-activity per connection. - Reassembled messages capped (server and client) so a peer cannot exhaust memory with an oversized fragmented message. - Replication snapshots carry a protocol-version byte; a client receiving a mismatched version drops the message instead of decoding garbage, and a truncated snapshot is ignored without throwing. - ReplicationClient.Clear() deletes all replicated entities, so clients can wipe stale state before reconnecting. Tests: heartbeat healthy-survives / silent-peer-dropped, protocol-version mismatch, truncated snapshot, replication clear. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d360093be1
commit
f382fc98ea
@@ -0,0 +1,98 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using MrGameEng.Net;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Net.Tests;
|
||||
|
||||
public class HeartbeatTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task HealthyClient_SurvivesPastIdleTimeout()
|
||||
{
|
||||
var port = FreePort();
|
||||
using var server = new WebSocketServer(
|
||||
port,
|
||||
heartbeatInterval: TimeSpan.FromMilliseconds(100),
|
||||
idleTimeout: TimeSpan.FromMilliseconds(400)
|
||||
);
|
||||
server.Start();
|
||||
|
||||
using var client = await WebSocketClient.ConnectAsync(
|
||||
new Uri($"ws://localhost:{port}/"),
|
||||
new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token
|
||||
);
|
||||
var connection = await WaitFor(
|
||||
() => server.TryAcceptConnection(out var c) ? c : null,
|
||||
"server accept"
|
||||
);
|
||||
|
||||
// Дольше idleTimeout: живой клиент авто-отвечает pong на server-ping и остаётся открыт.
|
||||
await Task.Delay(900, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(connection.IsOpen);
|
||||
Assert.True(client.IsOpen);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SilentPeer_IsDroppedAfterIdleTimeout()
|
||||
{
|
||||
var port = FreePort();
|
||||
using var server = new WebSocketServer(
|
||||
port,
|
||||
heartbeatInterval: TimeSpan.FromMilliseconds(100),
|
||||
idleTimeout: TimeSpan.FromMilliseconds(400)
|
||||
);
|
||||
server.Start();
|
||||
|
||||
// Сырой peer: проходит рукопожатие, но дальше молчит и не отвечает на ping.
|
||||
using var tcp = new TcpClient();
|
||||
await tcp.ConnectAsync(IPAddress.Loopback, port, TestContext.Current.CancellationToken);
|
||||
var request =
|
||||
"GET / HTTP/1.1\r\n"
|
||||
+ "Host: localhost\r\n"
|
||||
+ "Upgrade: websocket\r\n"
|
||||
+ "Connection: Upgrade\r\n"
|
||||
+ "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
|
||||
+ "Sec-WebSocket-Version: 13\r\n"
|
||||
+ "\r\n";
|
||||
var bytes = Encoding.ASCII.GetBytes(request);
|
||||
await tcp.GetStream().WriteAsync(bytes, TestContext.Current.CancellationToken);
|
||||
|
||||
var connection = await WaitFor(
|
||||
() => server.TryAcceptConnection(out var c) ? c : null,
|
||||
"server accept"
|
||||
);
|
||||
Assert.True(connection.IsOpen);
|
||||
|
||||
// Peer не отвечает pong'ом → активность не обновляется → сервер закрывает по простою.
|
||||
await WaitFor(() => connection.IsOpen ? null : "closed", "idle drop");
|
||||
Assert.False(connection.IsOpen);
|
||||
}
|
||||
|
||||
private static async Task<T> WaitFor<T>(Func<T?> poll, string what)
|
||||
where T : class
|
||||
{
|
||||
for (var i = 0; i < 200; i++)
|
||||
{
|
||||
if (poll() is { } result)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
await Task.Delay(25);
|
||||
}
|
||||
|
||||
throw new TimeoutException($"Timed out waiting for {what}.");
|
||||
}
|
||||
|
||||
private static int FreePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
return port;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user