Remote: Server-seitiger Heartbeat mit Timeout für tote WebSocket-Verbindungen

- Server sendet alle 30s Ping an Client, Client antwortet mit Pong
- LastActivityAt wird bei jeder empfangenen Nachricht aktualisiert
- Nach 90s Inaktivität wird Verbindung als tot erkannt und aufgeräumt
- HeartbeatLoop läuft parallel zum ReceiveLoop
- remote.ps1: ping-Handler hinzugefügt
This commit is contained in:
Meleki committed 2026-03-13 15:44:35 +01:00
1 parent dd0bc263e0
commit a13355d7e9
3 files changed
+64 -1

No files matched your search

+58 -1
View File
@@ -50,9 +50,20 @@ public static class RemoteEndpoint
using var ws = await ctx.WebSockets.AcceptWebSocketAsync();
var conn = mgr.Register(ws);
using var heartbeatCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
try
{
await ReceiveLoop(ws, conn, mgr, ct);
var receiveTask = ReceiveLoop(ws, conn, mgr, heartbeatCts.Token);
var heartbeatTask = HeartbeatLoop(ws, conn, heartbeatCts);
// Warten bis einer von beiden endet (Receive-Fehler oder Heartbeat-Timeout)
await Task.WhenAny(receiveTask, heartbeatTask);
heartbeatCts.Cancel();
// Beide Tasks sauber abwarten
try { await receiveTask; } catch { }
try { await heartbeatTask; } catch { }
}
catch (WebSocketException)
{
@@ -104,6 +115,8 @@ public static class RemoteEndpoint
try { msg = JsonSerializer.Deserialize<JsonElement>(json); }
catch { continue; }
conn.LastActivityAt = DateTime.UtcNow;
var type = msg.TryGetProperty("type", out var tp) ? tp.GetString() : "";
switch (type)
@@ -129,10 +142,54 @@ public static class RemoteEndpoint
var pong = Encoding.UTF8.GetBytes("{\"type\":\"pong\"}");
await ws.SendAsync(pong, WebSocketMessageType.Text, true, ct);
break;
case "pong":
// Antwort auf Server-Ping → LastActivityAt ist schon gesetzt
break;
}
}
}
private static async Task HeartbeatLoop(WebSocket ws, RemoteConnection conn, CancellationTokenSource heartbeatCts)
{
const int pingIntervalSeconds = 30;
const int deadTimeoutSeconds = 90;
var pingBytes = Encoding.UTF8.GetBytes("{\"type\":\"ping\"}");
try
{
while (!heartbeatCts.IsCancellationRequested && ws.State == WebSocketState.Open)
{
await Task.Delay(TimeSpan.FromSeconds(pingIntervalSeconds), heartbeatCts.Token);
// Prüfe ob der Client noch lebt
var silenceSec = (DateTime.UtcNow - conn.LastActivityAt).TotalSeconds;
if (silenceSec > deadTimeoutSeconds)
{
Console.WriteLine($"[Remote] Heartbeat timeout ({silenceSec:F0}s): {conn.Id} ({conn.Hostname})");
heartbeatCts.Cancel();
return;
}
// Ping senden
try
{
await ws.SendAsync(pingBytes, WebSocketMessageType.Text, true, heartbeatCts.Token);
}
catch
{
// Send fehlgeschlagen → Verbindung tot
heartbeatCts.Cancel();
return;
}
}
}
catch (OperationCanceledException)
{
// Normal – ReceiveLoop hat beendet
}
}
private static IResult GetConnections(RemoteConnectionManager mgr)
{
var connections = mgr.GetAll().Select(c => new