From 2ad81aa54d0e9cfcc87f60f19163a10adf038449 Mon Sep 17 00:00:00 2001 From: Aaron Yarborough Date: Fri, 21 Feb 2025 22:36:38 +0000 Subject: [PATCH] feat: improve scalability of TcpGameServer --- GServer.Client/Program.cs | 10 +- .../Networking/Messages/IMessageHandler.cs | 9 -- GServer.Server/ClientState.cs | 25 +++++ GServer.Server/Program.cs | 17 ++- GServer.Server/TcpGameServer.cs | 106 ++++++++++++------ GServer.Server/TcpMessageHandler.cs | 10 +- 6 files changed, 117 insertions(+), 60 deletions(-) delete mode 100644 GServer.Common/Networking/Messages/IMessageHandler.cs create mode 100644 GServer.Server/ClientState.cs diff --git a/GServer.Client/Program.cs b/GServer.Client/Program.cs index 471cb98..e695799 100644 --- a/GServer.Client/Program.cs +++ b/GServer.Client/Program.cs @@ -56,11 +56,11 @@ public static class Program // Request server list as soon as login has succeeded - if (authResultMessage.IsSuccessful) - { - Console.WriteLine("Getting server list..."); - _ = tcpClient.Client.Send(new[] { (byte)ServerPacketIn.ListServers }); - } + // if (authResultMessage.IsSuccessful) + // { + // Console.WriteLine("Getting server list..."); + // _ = tcpClient.Client.Send(new[] { (byte)ServerPacketIn.ListServers }); + // } break; diff --git a/GServer.Common/Networking/Messages/IMessageHandler.cs b/GServer.Common/Networking/Messages/IMessageHandler.cs deleted file mode 100644 index 0cb03d4..0000000 --- a/GServer.Common/Networking/Messages/IMessageHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Net.Sockets; -using GServer.Common.Networking.Core; - -namespace GServer.Common.Networking.Messages; - -public interface IMessageHandler -{ - Task HandleMessageAsync(Socket clientSocket, MessageMemoryStream messageStream); -} \ No newline at end of file diff --git a/GServer.Server/ClientState.cs b/GServer.Server/ClientState.cs new file mode 100644 index 0000000..ddb57a7 --- /dev/null +++ b/GServer.Server/ClientState.cs @@ -0,0 +1,25 @@ +using System.Net.Sockets; + +namespace GServer.Server; + +/// +/// Holds information related to a connected client. +/// +/// +public class ClientState( + TcpClient client) +{ + public TcpClient Client { get; } = client; + /// + /// The ID of the associated player. + /// + public Guid PlayerId { get; set; } + /// + /// The username of the associated player. + /// + public string Username { get; set; } + /// + /// The timestamp (UTC) of the last received packet from the client. + /// + public DateTime LastHeartbeat { get; set; } +} \ No newline at end of file diff --git a/GServer.Server/Program.cs b/GServer.Server/Program.cs index 4ee2726..d06b0b0 100644 --- a/GServer.Server/Program.cs +++ b/GServer.Server/Program.cs @@ -17,13 +17,10 @@ internal sealed class Program // Register services _ = builder.Services.AddScoped(); _ = builder.Services.AddScoped(); - _ = builder.Services.AddTransient((services) => - { - return new TcpGameServer( - new IPEndPoint(IPAddress.Any, ListenPort), - services.GetRequiredService() - ); - }); + _ = builder.Services.AddTransient((services) => new TcpGameServer( + new IPEndPoint(IPAddress.Any, ListenPort), + services.GetRequiredService() + )); // Start service using IHost host = builder.Build(); @@ -33,12 +30,12 @@ internal sealed class Program private static void ApplicationLifetime(IServiceProvider hostProvider) { - using IServiceScope serviceScope = hostProvider.CreateScope(); - Thread serverWorker = new(() => { + using IServiceScope serviceScope = hostProvider.CreateScope(); + ITcpGameServer server = serviceScope.ServiceProvider.GetRequiredService(); - server.Start(); + server.StartAsync(); while (true) { diff --git a/GServer.Server/TcpGameServer.cs b/GServer.Server/TcpGameServer.cs index 67e2fa3..ac64c04 100644 --- a/GServer.Server/TcpGameServer.cs +++ b/GServer.Server/TcpGameServer.cs @@ -1,67 +1,85 @@ +using System.Buffers; +using System.Collections.Concurrent; using System.Net; using System.Net.Sockets; using GServer.Common.Networking.Core; namespace GServer.Server; -public interface ITcpGameServer +public interface ITcpGameServer : IDisposable { - void Dispose(); - void Start(); + /// + /// Bind the server to the given endpoint. + /// + Task StartAsync(); } public class TcpGameServer( IPEndPoint endPoint, ITcpMessageHandler messageHandler -) : IDisposable, ITcpGameServer +) : ITcpGameServer { private readonly TcpListener _tcpListener = new(endPoint); - - /// - /// Bind the server to the given endpoint. - /// - public void Start() + private readonly ConcurrentDictionary _clients = new(); + private bool _disposed; + + public async Task StartAsync() { - Console.WriteLine($"Starting ${nameof(TcpGameServer)} listener..."); _tcpListener.Start(); - Console.WriteLine($"{nameof(TcpGameServer)} listening on {endPoint}"); + Console.WriteLine($"{nameof(TcpGameServer)} listening on {endPoint}..."); - while (true) + while (!_disposed) { try { - Console.WriteLine("Waiting for a connection..."); - - TcpClient client = _tcpListener.AcceptTcpClient(); - Console.WriteLine("Client accepted!"); - - Thread worker = new(new ParameterizedThreadStart(HandleClient!)); // TODO: use thread pools instead - worker.Start(client); + TcpClient client = await _tcpListener.AcceptTcpClientAsync(); + Console.WriteLine($"Client accepted: {client.Client.RemoteEndPoint}"); + + ClientState clientState = new(client); + _clients.TryAdd(client, clientState); + + // Handle client asynchronously using the thread pool + _ = Task.Run(() => HandleClientAsync(client, clientState)); } - catch (Exception ex) + catch (Exception ex) when (!_disposed) { - Console.WriteLine($"An error occured while processing a tcp connection: {ex.Message}"); + Console.WriteLine($"Error accepting client: {ex.Message}"); } } } - private async void HandleClient(object clientObj) + private async Task HandleClientAsync(TcpClient client, ClientState state) { - if (clientObj is not TcpClient tcpClient) - { - return; - } - + Console.WriteLine($"Processing request from client: {client.Client.RemoteEndPoint} (Player Id = {state.PlayerId}, Username = {state.Username})"); + try { - using (tcpClient) - using (NetworkStream stream = tcpClient.GetStream()) + using (client) + await using (NetworkStream stream = client.GetStream()) { - byte[] data = new byte[tcpClient.ReceiveBufferSize]; - while (stream.Read(data, 0, data.Length) != 0) + byte[] buffer = ArrayPool.Shared.Rent(client.ReceiveBufferSize); + + try { - // Use the in-memory buffer to process the message - await messageHandler.HandleMessageAsync(stream.Socket, new MessageMemoryStream(data)); + while (client.Connected) + { + // TODO: support cancellation tokens + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); + if (bytesRead == 0) + break; // Client disconnected + + await messageHandler.HandleMessageAsync(stream.Socket, new MessageMemoryStream(buffer), state); + + state.LastHeartbeat = DateTime.UtcNow; + } + } + catch (Exception e) + { + Console.WriteLine($"Error occured reading buffer: {e.Message}"); + } + finally + { + ArrayPool.Shared.Return(buffer); } } } @@ -69,17 +87,39 @@ public class TcpGameServer( { Console.WriteLine($"Error handling client: {ex.Message}"); } + finally + { + _clients.TryRemove(client, out _); + Console.WriteLine($"Client disconnected: {client.Client.RemoteEndPoint}"); + } } private void Stop() { + if (_disposed) + return; + Console.WriteLine($"Stopping ${nameof(TcpGameServer)} listener..."); + + // Stop listening for new TCP connections _tcpListener.Stop(); + // Disconnect all connected clients + foreach (TcpClient client in _clients.Keys) + { + client.Close(); + } + // Stop tracking all clients + _clients.Clear(); + Console.WriteLine($"Stopped ${nameof(TcpGameServer)} listener."); } public void Dispose() { + if (_disposed) + return; + + _disposed = true; Stop(); GC.SuppressFinalize(this); } diff --git a/GServer.Server/TcpMessageHandler.cs b/GServer.Server/TcpMessageHandler.cs index fd8cbd3..93f09d1 100644 --- a/GServer.Server/TcpMessageHandler.cs +++ b/GServer.Server/TcpMessageHandler.cs @@ -12,14 +12,14 @@ namespace GServer.Server; public interface ITcpMessageHandler { - Task HandleMessageAsync(Socket clientSocket, MessageMemoryStream messageStream); + Task HandleMessageAsync(Socket clientSocket, MessageMemoryStream messageStream, ClientState state); } public class TcpMessageHandler( IAuthService authService -) : IMessageHandler, ITcpMessageHandler +) : ITcpMessageHandler { - public async Task HandleMessageAsync(Socket clientSocket, MessageMemoryStream messageStream) + public async Task HandleMessageAsync(Socket clientSocket, MessageMemoryStream messageStream, ClientState state) { ServerPacketIn serverPacketIn = (ServerPacketIn)messageStream.ReadByte(); @@ -36,6 +36,10 @@ public class TcpMessageHandler( ? new AuthResponseMessage(true, Guid.NewGuid().ToString(), failureReason: null) : new AuthResponseMessage(false, null, AuthResponseFailure.IncorrectLoginOrPassword); await SendMessageAsync(resp, clientSocket); + + // TODO: Placeholder for now -- set up actual username and player ID + state.Username = msg.Username; + state.PlayerId = Guid.NewGuid(); break; }