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();
|
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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user