Server closes connections gracefully so kick/ban/reset/shutdown messages arrive; status dot and HUD yellow until really connected (also while reconnecting)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+15
-7
@@ -79,7 +79,7 @@ public sealed class GameServer
|
||||
|
||||
public void EnqueueCommand(string line) => commands.Enqueue(line);
|
||||
|
||||
int JoinedCount => clients.Values.Count(c => c.State == ClientState.Joined);
|
||||
int JoinedCount => clients.Values.Count(c => c.State == ClientState.Joined && !c.Conn.IsClosing); // gerade getrennte zählen nicht mehr
|
||||
|
||||
// ---------------------------------------------------------------- Start / Schleife
|
||||
|
||||
@@ -142,8 +142,16 @@ public sealed class GameServer
|
||||
{
|
||||
// Grund mitschicken: die Spieler sehen ihn im Hauptmenü
|
||||
if (c.State == ClientState.Joined) c.Conn.Send(MsgType.Reject, Wire.Build(w => w.Write("The server was shut down.")));
|
||||
c.Conn.Close("Server shutting down");
|
||||
c.Conn.CloseAfterSend("Server shutting down");
|
||||
}
|
||||
// Kurz warten, bis die Spieler die Meldung gelesen haben (sauber trennen statt hart abbrechen)
|
||||
var waitUntil = DateTime.UtcNow.AddSeconds(1.5);
|
||||
while (DateTime.UtcNow < waitUntil && clients.Values.Any(c => !c.Conn.IsClosed))
|
||||
{
|
||||
foreach (var c in clients.Values) c.Conn.Poll();
|
||||
Thread.Sleep(20);
|
||||
}
|
||||
foreach (var c in clients.Values) c.Conn.Close("Server shutting down");
|
||||
listener.Stop();
|
||||
SaveWorld();
|
||||
if (!offline)
|
||||
@@ -272,7 +280,7 @@ public sealed class GameServer
|
||||
w.Write(names.Count);
|
||||
foreach (string n in names) w.Write(n);
|
||||
}));
|
||||
c.Conn.Close("Info query");
|
||||
c.Conn.CloseAfterSend("Info query");
|
||||
return;
|
||||
|
||||
case MsgType.Hello:
|
||||
@@ -404,8 +412,8 @@ public sealed class GameServer
|
||||
c.SteamVerified = steam.Enabled && !c.SteamId.StartsWith("name:");
|
||||
foreach (var other in clients.Values)
|
||||
{
|
||||
if (other == c || other.State != ClientState.Joined || other.SteamId != c.SteamId) continue;
|
||||
if (c.SteamVerified) other.Conn.Close("Joined from another location"); // echter Account: alte Verbindung trennen
|
||||
if (other == c || other.State != ClientState.Joined || other.Conn.IsClosing || other.SteamId != c.SteamId) continue;
|
||||
if (c.SteamVerified) other.Conn.CloseAfterSend("Joined from another location"); // echter Account: alte Verbindung trennen
|
||||
else { Reject(c, "A player with this identity is already connected"); return; }
|
||||
}
|
||||
|
||||
@@ -440,7 +448,7 @@ public sealed class GameServer
|
||||
void Reject(Client c, string reason)
|
||||
{
|
||||
c.Conn.Send(MsgType.Reject, Wire.Build(w => w.Write(reason)));
|
||||
c.Conn.Close(reason);
|
||||
c.Conn.CloseAfterSend(reason);
|
||||
Log($"Rejected {c.Name} ({c.Conn.RemoteAddress}): {reason}");
|
||||
}
|
||||
|
||||
@@ -472,7 +480,7 @@ public sealed class GameServer
|
||||
foreach (var c in clients.Values.Where(x => x.State == ClientState.Joined).ToList())
|
||||
{
|
||||
c.Conn.Send(MsgType.Reconnect, Wire.Build(w => w.Write(message)));
|
||||
c.Conn.Close("Reconnect after world change");
|
||||
c.Conn.CloseAfterSend("Reconnect after world change");
|
||||
count++;
|
||||
}
|
||||
if (count > 0) Log($"{count} player(s) will reconnect");
|
||||
|
||||
+41
-1
@@ -89,6 +89,9 @@ public sealed class Connection
|
||||
|
||||
readonly TcpClient client;
|
||||
readonly NetworkStream stream;
|
||||
bool closing; // Meldung gesendet, warten bis der Spieler sie gelesen hat (siehe CloseAfterSend)
|
||||
DateTime closeDeadline;
|
||||
string closingReason;
|
||||
readonly byte[] readBuffer = new byte[64 * 1024];
|
||||
byte[] buffer = new byte[64 * 1024];
|
||||
int count;
|
||||
@@ -106,6 +109,11 @@ public sealed class Connection
|
||||
{
|
||||
var messages = new List<Message>();
|
||||
if (IsClosed) return messages;
|
||||
if (closing)
|
||||
{
|
||||
DrainWhileClosing();
|
||||
return messages;
|
||||
}
|
||||
try
|
||||
{
|
||||
if (client.Client.Poll(0, SelectMode.SelectRead) && client.Available == 0)
|
||||
@@ -131,7 +139,7 @@ public sealed class Connection
|
||||
|
||||
public void Send(MsgType type, byte[] payload = null)
|
||||
{
|
||||
if (IsClosed) return;
|
||||
if (IsClosed || closing) return;
|
||||
try
|
||||
{
|
||||
byte[] frame = Wire.Frame(type, payload);
|
||||
@@ -143,6 +151,38 @@ public sealed class Connection
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sauber trennen, nachdem eine letzte Meldung gesendet wurde (Kick/Ban-Grund, Neu-Verbinden, Shutdown).
|
||||
/// Sofortiges Close() mit noch ungelesenen Daten des Spielers (er schickt ständig Positionen) führt zu einem
|
||||
/// TCP-Reset - dann geht die gerade gesendete Meldung verloren. Deshalb: nur die Sende-Richtung schließen,
|
||||
/// eingehende Daten noch kurz lesen und verwerfen, dann erst schließen.
|
||||
/// </summary>
|
||||
public void CloseAfterSend(string reason)
|
||||
{
|
||||
if (IsClosed || closing) return;
|
||||
closing = true;
|
||||
closingReason = reason;
|
||||
closeDeadline = DateTime.UtcNow.AddSeconds(3);
|
||||
try { client.Client.Shutdown(SocketShutdown.Send); } catch { Close(reason); }
|
||||
}
|
||||
|
||||
public bool IsClosing => closing && !IsClosed;
|
||||
|
||||
void DrainWhileClosing()
|
||||
{
|
||||
try
|
||||
{
|
||||
while (stream.DataAvailable)
|
||||
if (stream.Read(readBuffer, 0, readBuffer.Length) <= 0) break;
|
||||
bool remoteClosed = client.Client.Poll(0, SelectMode.SelectRead) && client.Available == 0;
|
||||
if (remoteClosed || DateTime.UtcNow >= closeDeadline) Close(closingReason);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Close(closingReason);
|
||||
}
|
||||
}
|
||||
|
||||
public void Close(string reason)
|
||||
{
|
||||
if (IsClosed) return;
|
||||
|
||||
@@ -155,6 +155,20 @@ try
|
||||
watcher.Close();
|
||||
await Task.Delay(300);
|
||||
|
||||
Console.WriteLine("== Kick reason arrives while the player keeps sending");
|
||||
var spammer = await TestClient.Join("Spammer", 0, null);
|
||||
var spamming = true;
|
||||
var spamTask = Task.Run(async () => { while (spamming) { spammer.Send(MsgType.Player, PlayerPos(1f, 2f, 3f)); await Task.Delay(5); } });
|
||||
await Task.Delay(300);
|
||||
server.EnqueueCommand("kick " + spammer.Id + " testing");
|
||||
var kickMsg = await spammer.WaitFor(MsgType.Reject, 3000);
|
||||
spamming = false;
|
||||
await spamTask;
|
||||
Check(kickMsg != null && new BinaryReader(new MemoryStream(kickMsg)).ReadString().Contains("testing"),
|
||||
"Kicked player receives the reason even while sending data (no hard reset)");
|
||||
spammer.Close();
|
||||
await Task.Delay(300);
|
||||
|
||||
Console.WriteLine("== reset-world makes everyone reconnect");
|
||||
var r1 = await TestClient.Join("Resetter", 0, null);
|
||||
server.EnqueueCommand("reset-world");
|
||||
|
||||
Reference in New Issue
Block a user