feat: improve scalability of TcpGameServer
This commit is contained in:
parent
ba40a00cf2
commit
2ad81aa54d
6 changed files with 117 additions and 60 deletions
|
@ -56,11 +56,11 @@ public static class Program
|
||||||
|
|
||||||
// Request server list as soon as login has succeeded
|
// Request server list as soon as login has succeeded
|
||||||
|
|
||||||
if (authResultMessage.IsSuccessful)
|
// if (authResultMessage.IsSuccessful)
|
||||||
{
|
// {
|
||||||
Console.WriteLine("Getting server list...");
|
// Console.WriteLine("Getting server list...");
|
||||||
_ = tcpClient.Client.Send(new[] { (byte)ServerPacketIn.ListServers });
|
// _ = tcpClient.Client.Send(new[] { (byte)ServerPacketIn.ListServers });
|
||||||
}
|
// }
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
25
GServer.Server/ClientState.cs
Normal file
25
GServer.Server/ClientState.cs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
using System.Net.Sockets;
|
||||||
|
|
||||||
|
namespace GServer.Server;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Holds information related to a connected client.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client"></param>
|
||||||
|
public class ClientState(
|
||||||
|
TcpClient client)
|
||||||
|
{
|
||||||
|
public TcpClient Client { get; } = client;
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the associated player.
|
||||||
|
/// </summary>
|
||||||
|
public Guid PlayerId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The username of the associated player.
|
||||||
|
/// </summary>
|
||||||
|
public string Username { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The timestamp (UTC) of the last received packet from the client.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime LastHeartbeat { get; set; }
|
||||||
|
}
|
|
@ -17,13 +17,10 @@ internal sealed class Program
|
||||||
// Register services
|
// Register services
|
||||||
_ = builder.Services.AddScoped<IAuthService, AuthService>();
|
_ = builder.Services.AddScoped<IAuthService, AuthService>();
|
||||||
_ = builder.Services.AddScoped<ITcpMessageHandler, TcpMessageHandler>();
|
_ = builder.Services.AddScoped<ITcpMessageHandler, TcpMessageHandler>();
|
||||||
_ = builder.Services.AddTransient<ITcpGameServer>((services) =>
|
_ = builder.Services.AddTransient<ITcpGameServer>((services) => new TcpGameServer(
|
||||||
{
|
new IPEndPoint(IPAddress.Any, ListenPort),
|
||||||
return new TcpGameServer(
|
services.GetRequiredService<ITcpMessageHandler>()
|
||||||
new IPEndPoint(IPAddress.Any, ListenPort),
|
));
|
||||||
services.GetRequiredService<ITcpMessageHandler>()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start service
|
// Start service
|
||||||
using IHost host = builder.Build();
|
using IHost host = builder.Build();
|
||||||
|
@ -33,12 +30,12 @@ internal sealed class Program
|
||||||
|
|
||||||
private static void ApplicationLifetime(IServiceProvider hostProvider)
|
private static void ApplicationLifetime(IServiceProvider hostProvider)
|
||||||
{
|
{
|
||||||
using IServiceScope serviceScope = hostProvider.CreateScope();
|
|
||||||
|
|
||||||
Thread serverWorker = new(() =>
|
Thread serverWorker = new(() =>
|
||||||
{
|
{
|
||||||
|
using IServiceScope serviceScope = hostProvider.CreateScope();
|
||||||
|
|
||||||
ITcpGameServer server = serviceScope.ServiceProvider.GetRequiredService<ITcpGameServer>();
|
ITcpGameServer server = serviceScope.ServiceProvider.GetRequiredService<ITcpGameServer>();
|
||||||
server.Start();
|
server.StartAsync();
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,67 +1,85 @@
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using GServer.Common.Networking.Core;
|
using GServer.Common.Networking.Core;
|
||||||
|
|
||||||
namespace GServer.Server;
|
namespace GServer.Server;
|
||||||
|
|
||||||
public interface ITcpGameServer
|
public interface ITcpGameServer : IDisposable
|
||||||
{
|
{
|
||||||
void Dispose();
|
/// <summary>
|
||||||
void Start();
|
/// Bind the server to the given endpoint.
|
||||||
|
/// </summary>
|
||||||
|
Task StartAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TcpGameServer(
|
public class TcpGameServer(
|
||||||
IPEndPoint endPoint,
|
IPEndPoint endPoint,
|
||||||
ITcpMessageHandler messageHandler
|
ITcpMessageHandler messageHandler
|
||||||
) : IDisposable, ITcpGameServer
|
) : ITcpGameServer
|
||||||
{
|
{
|
||||||
private readonly TcpListener _tcpListener = new(endPoint);
|
private readonly TcpListener _tcpListener = new(endPoint);
|
||||||
|
private readonly ConcurrentDictionary<TcpClient, ClientState> _clients = new();
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
/// <summary>
|
public async Task StartAsync()
|
||||||
/// Bind the server to the given endpoint.
|
|
||||||
/// </summary>
|
|
||||||
public void Start()
|
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Starting ${nameof(TcpGameServer)} listener...");
|
|
||||||
_tcpListener.Start();
|
_tcpListener.Start();
|
||||||
Console.WriteLine($"{nameof(TcpGameServer)} listening on {endPoint}");
|
Console.WriteLine($"{nameof(TcpGameServer)} listening on {endPoint}...");
|
||||||
|
|
||||||
while (true)
|
while (!_disposed)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Console.WriteLine("Waiting for a connection...");
|
TcpClient client = await _tcpListener.AcceptTcpClientAsync();
|
||||||
|
Console.WriteLine($"Client accepted: {client.Client.RemoteEndPoint}");
|
||||||
|
|
||||||
TcpClient client = _tcpListener.AcceptTcpClient();
|
ClientState clientState = new(client);
|
||||||
Console.WriteLine("Client accepted!");
|
_clients.TryAdd(client, clientState);
|
||||||
|
|
||||||
Thread worker = new(new ParameterizedThreadStart(HandleClient!)); // TODO: use thread pools instead
|
// Handle client asynchronously using the thread pool
|
||||||
worker.Start(client);
|
_ = 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)
|
Console.WriteLine($"Processing request from client: {client.Client.RemoteEndPoint} (Player Id = {state.PlayerId}, Username = {state.Username})");
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (tcpClient)
|
using (client)
|
||||||
using (NetworkStream stream = tcpClient.GetStream())
|
await using (NetworkStream stream = client.GetStream())
|
||||||
{
|
{
|
||||||
byte[] data = new byte[tcpClient.ReceiveBufferSize];
|
byte[] buffer = ArrayPool<byte>.Shared.Rent(client.ReceiveBufferSize);
|
||||||
while (stream.Read(data, 0, data.Length) != 0)
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
// Use the in-memory buffer to process the message
|
while (client.Connected)
|
||||||
await messageHandler.HandleMessageAsync(stream.Socket, new MessageMemoryStream(data));
|
{
|
||||||
|
// 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<byte>.Shared.Return(buffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,17 +87,39 @@ public class TcpGameServer(
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Error handling client: {ex.Message}");
|
Console.WriteLine($"Error handling client: {ex.Message}");
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_clients.TryRemove(client, out _);
|
||||||
|
Console.WriteLine($"Client disconnected: {client.Client.RemoteEndPoint}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Stop()
|
private void Stop()
|
||||||
{
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
Console.WriteLine($"Stopping ${nameof(TcpGameServer)} listener...");
|
Console.WriteLine($"Stopping ${nameof(TcpGameServer)} listener...");
|
||||||
|
|
||||||
|
// Stop listening for new TCP connections
|
||||||
_tcpListener.Stop();
|
_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.");
|
Console.WriteLine($"Stopped ${nameof(TcpGameServer)} listener.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
Stop();
|
Stop();
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,14 +12,14 @@ namespace GServer.Server;
|
||||||
|
|
||||||
public interface ITcpMessageHandler
|
public interface ITcpMessageHandler
|
||||||
{
|
{
|
||||||
Task HandleMessageAsync(Socket clientSocket, MessageMemoryStream messageStream);
|
Task HandleMessageAsync(Socket clientSocket, MessageMemoryStream messageStream, ClientState state);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TcpMessageHandler(
|
public class TcpMessageHandler(
|
||||||
IAuthService authService
|
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();
|
ServerPacketIn serverPacketIn = (ServerPacketIn)messageStream.ReadByte();
|
||||||
|
|
||||||
|
@ -37,6 +37,10 @@ public class TcpMessageHandler(
|
||||||
: new AuthResponseMessage(false, null, AuthResponseFailure.IncorrectLoginOrPassword);
|
: new AuthResponseMessage(false, null, AuthResponseFailure.IncorrectLoginOrPassword);
|
||||||
await SendMessageAsync(resp, clientSocket);
|
await SendMessageAsync(resp, clientSocket);
|
||||||
|
|
||||||
|
// TODO: Placeholder for now -- set up actual username and player ID
|
||||||
|
state.Username = msg.Username;
|
||||||
|
state.PlayerId = Guid.NewGuid();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue