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
|
||||
|
||||
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;
|
||||
|
||||
|
|
|
@ -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
|
||||
_ = builder.Services.AddScoped<IAuthService, AuthService>();
|
||||
_ = builder.Services.AddScoped<ITcpMessageHandler, TcpMessageHandler>();
|
||||
_ = builder.Services.AddTransient<ITcpGameServer>((services) =>
|
||||
{
|
||||
return new TcpGameServer(
|
||||
_ = builder.Services.AddTransient<ITcpGameServer>((services) => new TcpGameServer(
|
||||
new IPEndPoint(IPAddress.Any, ListenPort),
|
||||
services.GetRequiredService<ITcpMessageHandler>()
|
||||
);
|
||||
});
|
||||
));
|
||||
|
||||
// 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<ITcpGameServer>();
|
||||
server.Start();
|
||||
server.StartAsync();
|
||||
|
||||
while (true)
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
/// <summary>
|
||||
/// Bind the server to the given endpoint.
|
||||
/// </summary>
|
||||
Task StartAsync();
|
||||
}
|
||||
|
||||
public class TcpGameServer(
|
||||
IPEndPoint endPoint,
|
||||
ITcpMessageHandler messageHandler
|
||||
) : IDisposable, ITcpGameServer
|
||||
) : ITcpGameServer
|
||||
{
|
||||
private readonly TcpListener _tcpListener = new(endPoint);
|
||||
private readonly ConcurrentDictionary<TcpClient, ClientState> _clients = new();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Bind the server to the given endpoint.
|
||||
/// </summary>
|
||||
public void Start()
|
||||
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 = await _tcpListener.AcceptTcpClientAsync();
|
||||
Console.WriteLine($"Client accepted: {client.Client.RemoteEndPoint}");
|
||||
|
||||
TcpClient client = _tcpListener.AcceptTcpClient();
|
||||
Console.WriteLine("Client accepted!");
|
||||
ClientState clientState = new(client);
|
||||
_clients.TryAdd(client, clientState);
|
||||
|
||||
Thread worker = new(new ParameterizedThreadStart(HandleClient!)); // TODO: use thread pools instead
|
||||
worker.Start(client);
|
||||
// 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<byte>.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<byte>.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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
@ -37,6 +37,10 @@ public class TcpMessageHandler(
|
|||
: 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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue