fix desync & try to fix delay
This commit is contained in:
parent
b077097a33
commit
dc893baf94
5 changed files with 78 additions and 3 deletions
|
|
@ -145,6 +145,15 @@ public class Utils
|
||||||
public static ConcurrentDictionary<string, List<System.Net.WebSockets.WebSocket>> ActiveWebSockets = new();
|
public static ConcurrentDictionary<string, List<System.Net.WebSockets.WebSocket>> ActiveWebSockets = new();
|
||||||
public static ConcurrentDictionary<string, List<string>> ActiveCalls = new();
|
public static ConcurrentDictionary<string, List<string>> ActiveCalls = new();
|
||||||
|
|
||||||
|
// Bezczynność app-WS = rozłączony klient. Klient wysyła tekstowy "ping" co
|
||||||
|
// 20 s; brak CZEGOKOLWIEK przez 75 s (3-4 przegapione pingi) oznacza martwy
|
||||||
|
// polłączony TCP (zabita aplikacja / utracony NAT mapping). Bez tego
|
||||||
|
// ReceiveAsync wisiał w nieskończoność, a cleanup ActiveCalls w finally
|
||||||
|
// nigdy nie biegł -> wskaźniki rozmów pokazywały uczestników, którzy
|
||||||
|
// dawno wyłączyli aplikację. Timeout wywołuje normalny finally (cleanup
|
||||||
|
// ActiveCalls + broadcast call_update do członków pokoju).
|
||||||
|
public static readonly TimeSpan AppWsIdleTimeout = TimeSpan.FromSeconds(75);
|
||||||
|
|
||||||
public static async Task WsHandler(HttpContext context, System.Net.WebSockets.WebSocket webSocket, IQueryCollection query)
|
public static async Task WsHandler(HttpContext context, System.Net.WebSockets.WebSocket webSocket, IQueryCollection query)
|
||||||
{
|
{
|
||||||
var buffer = new byte[8192];
|
var buffer = new byte[8192];
|
||||||
|
|
@ -210,7 +219,17 @@ public class Utils
|
||||||
{
|
{
|
||||||
while (webSocket.State == System.Net.WebSockets.WebSocketState.Open)
|
while (webSocket.State == System.Net.WebSockets.WebSocketState.Open)
|
||||||
{
|
{
|
||||||
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
using var idleCts = new System.Threading.CancellationTokenSource(AppWsIdleTimeout);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), idleCts.Token);
|
||||||
|
}
|
||||||
|
catch (System.OperationCanceledException)
|
||||||
|
{
|
||||||
|
//Brak ramek przez AppWsIdleTimeout - traktujemy jak rozłączenie:
|
||||||
|
//przerywamy pętlę, finally posprząta ActiveCalls i roześle call_update
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (result.MessageType == System.Net.WebSockets.WebSocketMessageType.Close)
|
if (result.MessageType == System.Net.WebSockets.WebSocketMessageType.Close)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,45 @@ public class Program
|
||||||
// Entries are pruned in the 1s loop below.
|
// Entries are pruned in the 1s loop below.
|
||||||
public static ConcurrentDictionary<string, (int count, long resetAt)> UsersListLimits = new ();
|
public static ConcurrentDictionary<string, (int count, long resetAt)> UsersListLimits = new ();
|
||||||
|
|
||||||
|
// Sesja bez ODBIERANEGO ruchu dłużej niż to = martwy TCP (zabita aplikacja,
|
||||||
|
// utracony mapping NAT). Klient pinguje co 2 s - 15 s to 7+ przegapionych
|
||||||
|
// pingów, więc fałszywe pozytywy przy chwilowych zaniżeniach sieci są
|
||||||
|
// praktycznie niemożliwe, a ghost nie zaśmieca rosteru dłużej niż trzeba.
|
||||||
|
public static readonly TimeSpan SessionIdleTimeout = TimeSpan.FromSeconds(15);
|
||||||
|
|
||||||
public static ulong TotalBytesSent = 0;
|
public static ulong TotalBytesSent = 0;
|
||||||
public static ulong TotalBytesReceived = 0;
|
public static ulong TotalBytesReceived = 0;
|
||||||
private static ulong CurrentBandwidthKps = 0;
|
private static ulong CurrentBandwidthKps = 0;
|
||||||
|
|
||||||
|
// Zamiata sesje bez odebranego ruchu dłużej niż SessionIdleTimeout. Bez
|
||||||
|
// tego zabita aplikacja (half-open TCP, brak Close frame) zostawała w
|
||||||
|
// rosterze pokoju na zawsze: /room/users zwracało ghostów -> ghost kafelki
|
||||||
|
// i "trwa rozmowa" u innych uczestników. TryRemove(KeyValuePair) jest
|
||||||
|
// warunkowe po wartości - nie da się przypadkiem usunąć NOWEJ sesji, która
|
||||||
|
// w międzyczasie nadpisała wpis tego samego userId (reconnect).
|
||||||
|
private static void SweepStaleSessions()
|
||||||
|
{
|
||||||
|
DateTime cutoff = DateTime.UtcNow - SessionIdleTimeout;
|
||||||
|
foreach (var room in Rooms)
|
||||||
|
{
|
||||||
|
foreach (var kvp in room.Value)
|
||||||
|
{
|
||||||
|
if (kvp.Value.LastSeenUtc >= cutoff) continue;
|
||||||
|
if (!room.Value.TryRemove(new KeyValuePair<ulong, Session>(kvp.Key, kvp.Value))) continue;
|
||||||
|
|
||||||
|
//Zamknięcie kolejki kończy task-pump; Abort wywala zawieszone
|
||||||
|
//ReceiveAsync/SendAsync martwego socketu -> receive-loop wejdzie
|
||||||
|
//we swój finally i posprząta bufor (guard session==currentSession
|
||||||
|
//zapobiega podwójnemu sprzątaniu)
|
||||||
|
kvp.Value.SendQueue.Writer.TryComplete();
|
||||||
|
try { kvp.Value.Socket.Abort(); } catch { }
|
||||||
|
Console.WriteLine($"Voice: swept idle session room={room.Key} user={kvp.Key}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (room.Value.IsEmpty) Rooms.TryRemove(room.Key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
Utils.LoadEnv();
|
Utils.LoadEnv();
|
||||||
|
|
@ -39,6 +74,8 @@ public class Program
|
||||||
{
|
{
|
||||||
if (entry.Value.resetAt <= nowSec) UsersListLimits.TryRemove(entry.Key, out _);
|
if (entry.Value.resetAt <= nowSec) UsersListLimits.TryRemove(entry.Key, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SweepStaleSessions();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,10 @@ public class Requests
|
||||||
|
|
||||||
} while (!result.EndOfMessage);
|
} while (!result.EndOfMessage);
|
||||||
|
|
||||||
|
//Dowolna kompletna ramka = klient żyje (ping typu 5 przychodzi co 2 s
|
||||||
|
//nawet przy ciszy) - odświeża licznik dla idle-sweepera w Program.cs
|
||||||
|
currentSession.Touch();
|
||||||
|
|
||||||
if (result.MessageType == WebSocketMessageType.Close) break;
|
if (result.MessageType == WebSocketMessageType.Close) break;
|
||||||
if (result.MessageType == WebSocketMessageType.Binary)
|
if (result.MessageType == WebSocketMessageType.Binary)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,16 @@ public class Session
|
||||||
public WebSocket Socket { get; }
|
public WebSocket Socket { get; }
|
||||||
public Channel<(byte[] Buffer, int Length)> SendQueue { get; }
|
public Channel<(byte[] Buffer, int Length)> SendQueue { get; }
|
||||||
|
|
||||||
|
// Ostatni znak życia (dowolna odebrana ramka - audio, wideo, ping typu 5).
|
||||||
|
// Klient pinguje co 2 s, więc sesja bez ruchu przez VOICE_IDLE_TIMEOUT to
|
||||||
|
// martwy polłączony TCP (zabita aplikacja, utracony NAT mapping) - taki wpis
|
||||||
|
// zamiata task w Program.cs. Bez tego ghost-sesje wiszą w rosterze wiecznie
|
||||||
|
// (/room/users zwraca nieżyjących uczestników -> ghost kafelki u innych,
|
||||||
|
// a relay marnuje bandwidth na zamknięte sockety).
|
||||||
|
private long _lastSeenUtcTicks;
|
||||||
|
|
||||||
|
public DateTime LastSeenUtc => new(Interlocked.Read(ref _lastSeenUtcTicks), DateTimeKind.Utc);
|
||||||
|
|
||||||
public Session(WebSocket socket)
|
public Session(WebSocket socket)
|
||||||
{
|
{
|
||||||
Socket = socket;
|
Socket = socket;
|
||||||
|
|
@ -15,5 +25,8 @@ public class Session
|
||||||
{
|
{
|
||||||
FullMode = BoundedChannelFullMode.Wait
|
FullMode = BoundedChannelFullMode.Wait
|
||||||
});
|
});
|
||||||
|
_lastSeenUtcTicks = DateTime.UtcNow.Ticks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Touch() => Interlocked.Exchange(ref _lastSeenUtcTicks, DateTime.UtcNow.Ticks);
|
||||||
}
|
}
|
||||||
|
|
@ -8,7 +8,9 @@ public class Utils
|
||||||
public static string MAIN_URL = "";
|
public static string MAIN_URL = "";
|
||||||
|
|
||||||
// Rate limits, requests per minute (fixed 60s window).
|
// Rate limits, requests per minute (fixed 60s window).
|
||||||
public static int ROOM_USERS_RATE_LIMIT = 120;
|
// 240/min: klient odpytuje roster co ~5 s przez całą rozmowę (12/min) -
|
||||||
|
// kilka osób za jednym NAT-em + początkowe retrye mieszczą się z zapasem.
|
||||||
|
public static int ROOM_USERS_RATE_LIMIT = 240;
|
||||||
|
|
||||||
// Shared with the main server: authenticates internal validation requests.
|
// Shared with the main server: authenticates internal validation requests.
|
||||||
public static string VOICE_KEY = "";
|
public static string VOICE_KEY = "";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue