Networking — Graceful Shutdown¶
This document describes the connection lifecycle and graceful-shutdown protocol for the Avalon TCP servers (Auth and World).
Architecture Note — Two Server Code Paths¶
There are two separate TCP server code paths in this codebase. Understanding the distinction is critical when working on networking features:
Production path (Auth + World servers)¶
ServerBase<T>(src/Server/Avalon.Hosting/Networking/ServerBase.cs) — extendsBackgroundService, usesTcpListener+BeginAcceptTcpClient.AuthServer : ServerBase<AuthConnection>— the live auth server.WorldServer : ServerBase<WorldConnection>— the live world server.- Connections implement
IConnection(inAvalon.Hosting.Networking), which exposes: void Send(NetworkPacket)— synchronous send via ring buffer.void Close(bool expected = true)— terminate the connection.
Standalone/client path (AvalonTcpServer)¶
AvalonTcpServer/AvalonSslTcpServer(src/Shared/Avalon.Network.Tcp/) —netstandard2.1standalone server with its ownSocketandInternalServerLoop.- Never instantiated in the auth or world server projects. Only referenced by name in
LayerEnricherlog-filtering helpers. - Used by the development
AvalonTcpClienttest harness. - Connections implement
IAvalonTcpConnection(inAvalon.Network), which exposesTask SendAsync(NetworkPacket).
Rule of thumb: if you are implementing a feature for the running game servers, edit
ServerBase<T>,AuthServer, orWorldServer. Changes toAvalonTcpServerdo not affect live server behaviour.
Connection Lifecycle¶
Client AuthServer / WorldServer (ServerBase<T>)
| |
| TCP SYN |
|-------------------------------->| BeginAcceptTcpClient → OnClientAccepted
| [Handshake / CRequestServerInfo] |
|<-------------------------------> |
| [Auth / World handshake flow] |
|<-------------------------------> |
| |
| (Server shutdown OR kick) |
| SDisconnectPacket |
|<---------------------------------| connection.Send(SDisconnectPacket)
| [client shows reconnect UI] |
| TCP FIN |
|<---------------------------------| connection.Close()
SDisconnectPacket Schema¶
Defined in Avalon.Network.Packets.Generic.SDisconnectPacket. Transmitted unencrypted (NetworkPacketFlags.None) so it is readable regardless of the crypto session state.
Packet type: NetworkPacketType.SMSG_DISCONNECT = 0x3008
| Field | Type | Description |
|---|---|---|
Reason |
string |
Human-readable reason shown to the player |
ReasonCode |
DisconnectReason |
Machine-readable disconnect reason |
Disconnect Reason Codes (DisconnectReason enum)¶
| Value | Name | Trigger |
|---|---|---|
| 0 | Unknown |
Default/unspecified |
| 1 | ServerShutdown |
Server stopping gracefully |
| 2 | DuplicateLogin |
Second authentication for the same account |
| 3 | Kicked |
Manual admin kick (future) |
Factory method¶
NetworkPacket packet = SDisconnectPacket.Create("Server is shutting down", DisconnectReason.ServerShutdown);
AvalonTcpServer Shutdown Behaviour¶
Note:
AvalonTcpServeris not the production server for Auth or World. See the architecture note above.
Connection tracking¶
AvalonTcpServer tracks connections in ConcurrentDictionary<Guid, IAvalonTcpConnection>. AvalonSslTcpServer.HandleNewConnection calls TrackConnection() after accepting a client. The connection is removed when its Disconnected event fires.
StopAsync sequence¶
1. For each tracked connection:
a. Send SDisconnectPacket(ServerShutdown)
b. Await 200 ms drain
c. Call connection.Close() — regardless of send success
2. Cts.Cancel() — stops the accept loop
3. Socket.Close() / Socket.Dispose()
4. Log "Server stopped"
Per-connection exceptions are caught and logged individually; they do not abort the shutdown of remaining connections.
Auth Server Shutdown¶
AuthServer.OnStoppingAsync delegates to GracefulShutdownHelper.NotifyAndClose for each connection.
World Server Shutdown¶
Graceful stop¶
WorldServer.OnStoppingAsync also delegates to GracefulShutdownHelper.NotifyAndClose.
Forced kick notification¶
DelayedDisconnect is called via Redis pub/sub when a duplicate login is detected by the Auth server. It sends SDisconnectPacket(DuplicateLogin) before closing:
private void DelayedDisconnect(RedisChannel channel, RedisValue value)
{
_logger.LogInformation("Disconnecting account {AccountId}", value);
AccountId accountId = value.ToString();
IWorldConnection? connection = Connections.FirstOrDefault(c => c.AccountId == accountId);
if (connection is null) return;
GracefulShutdownHelper.NotifyAndClose(connection,
"Your account has been logged in from another location.",
DisconnectReason.DuplicateLogin,
_logger);
}
GracefulShutdownHelper¶
GracefulShutdownHelper.NotifyAndClose (src/Server/Avalon.Hosting/Networking/GracefulShutdownHelper.cs) is the shared utility used by all three call sites above:
public static void NotifyAndClose(IConnection connection, string reason, DisconnectReason reasonCode, ILogger? logger = null)
- Sends
SDisconnectPacket— exceptions are caught, logged vialogger(optional), and do not abort the close. - Always calls
connection.Close()regardless of send success.
Tests: tests/Avalon.Server.Auth.UnitTests/Networking/GracefulShutdownHelperShould.cs (4 tests).
Test Coverage¶
| Scenario | Expected |
|---|---|
StopAsync with 3 connections |
All 3 receive disconnect packet then are closed |
One Send throws |
Other 2 still closed; exception logged |
StopAsync with 0 connections |
No-op; no exceptions |
DelayedDisconnect matching account |
Kick packet sent; Close() called |
DelayedDisconnect no account found |
No-op; no exception |