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;
}