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:
1 parent
dd0bc263e0
commit
a13355d7e9
3 files changed
+64
-1
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user