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(); using var ws = await ctx.WebSockets.AcceptWebSocketAsync();
var conn = mgr.Register(ws); var conn = mgr.Register(ws);
using var heartbeatCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
try 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) catch (WebSocketException)
{ {
@@ -104,6 +115,8 @@ public static class RemoteEndpoint
try { msg = JsonSerializer.Deserialize<JsonElement>(json); } try { msg = JsonSerializer.Deserialize<JsonElement>(json); }
catch { continue; } catch { continue; }
conn.LastActivityAt = DateTime.UtcNow;
var type = msg.TryGetProperty("type", out var tp) ? tp.GetString() : ""; var type = msg.TryGetProperty("type", out var tp) ? tp.GetString() : "";
switch (type) switch (type)
@@ -129,10 +142,54 @@ public static class RemoteEndpoint
var pong = Encoding.UTF8.GetBytes("{\"type\":\"pong\"}"); var pong = Encoding.UTF8.GetBytes("{\"type\":\"pong\"}");
await ws.SendAsync(pong, WebSocketMessageType.Text, true, ct); await ws.SendAsync(pong, WebSocketMessageType.Text, true, ct);
break; 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) private static IResult GetConnections(RemoteConnectionManager mgr)
{ {
var connections = mgr.GetAll().Select(c => new var connections = mgr.GetAll().Select(c => new
+2
View File
@@ -17,6 +17,7 @@ public class RemoteConnection
public string Username { get; set; } public string Username { get; set; }
public string OsVersion { get; set; } public string OsVersion { get; set; }
public DateTime ConnectedAt { get; set; } public DateTime ConnectedAt { get; set; }
public DateTime LastActivityAt { get; set; }
public string AssignedSessionId { get; set; } public string AssignedSessionId { get; set; }
public WebSocket Socket { get; set; } public WebSocket Socket { get; set; }
@@ -42,6 +43,7 @@ public class RemoteConnectionManager
{ {
Id = Guid.NewGuid().ToString("N")[..12], Id = Guid.NewGuid().ToString("N")[..12],
ConnectedAt = DateTime.UtcNow, ConnectedAt = DateTime.UtcNow,
LastActivityAt = DateTime.UtcNow,
Socket = socket Socket = socket
}; };
_connections[conn.Id] = conn; _connections[conn.Id] = conn;
+4
View File
@@ -153,6 +153,10 @@ try {
Write-Host " [Exit: $exitCode]" -ForegroundColor $(if ($exitCode -eq 0) { "Green" } else { "Red" }) Write-Host " [Exit: $exitCode]" -ForegroundColor $(if ($exitCode -eq 0) { "Green" } else { "Red" })
Write-Host "" Write-Host ""
} }
"ping" {
# Server-Heartbeat → mit pong antworten
Send-WsMessage $ws '{"type":"pong"}' $ct
}
"pong" { "pong" {
# Heartbeat-Antwort - ignorieren # Heartbeat-Antwort - ignorieren
} }