merge websocket server logic

This commit is contained in:
Vinzenz Schroeter 2024-04-21 23:00:44 +02:00
parent 3cea9c967d
commit 57c0d229f1
9 changed files with 199 additions and 162 deletions

View file

@ -47,7 +47,7 @@ internal static class Endpoints
return Results.BadRequest();
using var ws = await context.WebSockets.AcceptWebSocketAsync();
await clientScreenServer.HandleClient(ws, player);
await clientScreenServer.HandleClientAsync(ws, player);
return Results.Empty;
});
@ -60,7 +60,7 @@ internal static class Endpoints
return Results.NotFound();
using var ws = await context.WebSockets.AcceptWebSocketAsync();
await controlsServer.HandleClient(ws, player);
await controlsServer.HandleClientAsync(ws, player);
return Results.Empty;
});

View file

@ -1,7 +1,5 @@
using System.Diagnostics;
using System.Net.WebSockets;
using DisplayCommands;
using Microsoft.Extensions.Hosting;
using TanksServer.Graphics;
namespace TanksServer.Interactivity;
@ -10,54 +8,18 @@ internal sealed class ClientScreenServer(
ILogger<ClientScreenServer> logger,
ILoggerFactory loggerFactory,
IOptions<HostConfiguration> hostConfig
) : IHostedLifecycleService, IFrameConsumer
) : WebsocketServer<ClientScreenServerConnection>(logger), IFrameConsumer
{
private readonly ConcurrentDictionary<ClientScreenServerConnection, byte> _connections = new();
private readonly TimeSpan _minFrameTime = TimeSpan.FromMilliseconds(hostConfig.Value.ClientDisplayMinFrameTimeMs);
private bool _closing;
public Task StoppingAsync(CancellationToken cancellationToken)
{
logger.LogInformation("closing connections");
_closing = true;
return Task.WhenAll(_connections.Keys.Select(c => c.CloseAsync()));
}
public Task HandleClient(WebSocket socket, Guid? playerGuid)
{
if (_closing)
{
logger.LogWarning("ignoring request because connections are closing");
return Task.CompletedTask;
}
logger.LogDebug("HandleClient");
var connection = new ClientScreenServerConnection(
public Task HandleClientAsync(WebSocket socket, Guid? playerGuid)
=> base.HandleClientAsync(new(
socket,
loggerFactory.CreateLogger<ClientScreenServerConnection>(),
this,
_minFrameTime,
playerGuid);
var added = _connections.TryAdd(connection, 0);
Debug.Assert(added);
return connection.Done;
}
public void Remove(ClientScreenServerConnection connection) => _connections.TryRemove(connection, out _);
public IEnumerable<ClientScreenServerConnection> GetConnections() => _connections.Keys;
playerGuid
));
public Task OnFrameDoneAsync(GamePixelGrid gamePixelGrid, PixelGrid observerPixels)
{
var tasks = _connections.Keys
.Select(c => c.SendAsync(observerPixels, gamePixelGrid));
return Task.WhenAll(tasks);
}
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
=> ParallelForEachConnectionAsync(c => c.SendAsync(observerPixels, gamePixelGrid));
}

View file

@ -5,11 +5,10 @@ using TanksServer.Graphics;
namespace TanksServer.Interactivity;
internal sealed class ClientScreenServerConnection : IDisposable
internal sealed class ClientScreenServerConnection : IWebsocketServerConnection, IDisposable
{
private readonly ByteChannelWebSocket _channel;
private readonly ILogger<ClientScreenServerConnection> _logger;
private readonly ClientScreenServer _server;
private readonly SemaphoreSlim _wantedFrames = new(1);
private readonly Guid? _playerGuid;
private readonly PlayerScreenData? _playerScreenData;
@ -20,12 +19,10 @@ internal sealed class ClientScreenServerConnection : IDisposable
public ClientScreenServerConnection(
WebSocket webSocket,
ILogger<ClientScreenServerConnection> logger,
ClientScreenServer server,
TimeSpan minFrameTime,
Guid? playerGuid = null
)
{
_server = server;
_logger = logger;
_minFrameTime = minFrameTime;
@ -91,9 +88,7 @@ internal sealed class ClientScreenServerConnection : IDisposable
{
await foreach (var _ in _channel.ReadAllAsync())
_wantedFrames.Release();
_logger.LogTrace("done receiving");
_server.Remove(this);
}
public Task CloseAsync()

View file

@ -1,115 +1,19 @@
using System.Net.WebSockets;
using Microsoft.Extensions.Hosting;
namespace TanksServer.Interactivity;
internal sealed class ControlsServer(ILogger<ControlsServer> logger, ILoggerFactory loggerFactory)
: IHostedLifecycleService
internal sealed class ControlsServer(
ILogger<ControlsServer> logger,
ILoggerFactory loggerFactory
) : WebsocketServer<ControlsServerConnection>(logger)
{
private readonly List<ControlsServerConnection> _connections = [];
public Task StoppingAsync(CancellationToken cancellationToken)
{
return Task.WhenAll(_connections.Select(c => c.CloseAsync()));
}
public Task HandleClient(WebSocket ws, Player player)
public async Task HandleClientAsync(WebSocket ws, Player player)
{
logger.LogDebug("control client connected {}", player.Id);
var clientLogger = loggerFactory.CreateLogger<ControlsServerConnection>();
var sock = new ControlsServerConnection(ws, clientLogger, this, player);
_connections.Add(sock);
return sock.Done;
}
private void Remove(ControlsServerConnection connection) => _connections.Remove(connection);
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
private sealed class ControlsServerConnection
{
private readonly ByteChannelWebSocket _binaryWebSocket;
private readonly ILogger<ControlsServerConnection> _logger;
private readonly Player _player;
private readonly ControlsServer _server;
public ControlsServerConnection(WebSocket socket, ILogger<ControlsServerConnection> logger,
ControlsServer server, Player player)
{
_logger = logger;
_server = server;
_player = player;
_binaryWebSocket = new ByteChannelWebSocket(socket, logger, 2);
Done = ReceiveAsync();
}
public Task Done { get; }
private async Task ReceiveAsync()
{
await foreach (var buffer in _binaryWebSocket.ReadAllAsync())
{
var type = (MessageType)buffer.Span[0];
var control = (InputType)buffer.Span[1];
_logger.LogTrace("player input {} {} {}", _player.Id, type, control);
var isEnable = type switch
{
MessageType.Enable => true,
MessageType.Disable => false,
_ => throw new ArgumentException("invalid message type")
};
_player.LastInput = DateTime.Now;
switch (control)
{
case InputType.Forward:
_player.Controls.Forward = isEnable;
break;
case InputType.Backward:
_player.Controls.Backward = isEnable;
break;
case InputType.Left:
_player.Controls.TurnLeft = isEnable;
break;
case InputType.Right:
_player.Controls.TurnRight = isEnable;
break;
case InputType.Shoot:
_player.Controls.Shoot = isEnable;
break;
default:
throw new ArgumentException("invalid control type");
}
}
_server.Remove(this);
}
public Task CloseAsync()
{
return _binaryWebSocket.CloseAsync();
}
private enum MessageType : byte
{
Enable = 0x01,
Disable = 0x02
}
private enum InputType : byte
{
Forward = 0x01,
Backward = 0x02,
Left = 0x03,
Right = 0x04,
Shoot = 0x05
}
var sock = new ControlsServerConnection(ws, clientLogger, player);
await AddConnection(sock);
await sock.Done;
await RemoveConnection(sock);
}
}

View file

@ -0,0 +1,78 @@
using System.Net.WebSockets;
namespace TanksServer.Interactivity;
internal sealed class ControlsServerConnection : IWebsocketServerConnection
{
private readonly ByteChannelWebSocket _binaryWebSocket;
private readonly ILogger<ControlsServerConnection> _logger;
private readonly Player _player;
public ControlsServerConnection(WebSocket socket, ILogger<ControlsServerConnection> logger, Player player)
{
_logger = logger;
_player = player;
_binaryWebSocket = new ByteChannelWebSocket(socket, logger, 2);
Done = ReceiveAsync();
}
public Task Done { get; }
private async Task ReceiveAsync()
{
await foreach (var buffer in _binaryWebSocket.ReadAllAsync())
{
var type = (MessageType)buffer.Span[0];
var control = (InputType)buffer.Span[1];
_logger.LogTrace("player input {} {} {}", _player.Id, type, control);
var isEnable = type switch
{
MessageType.Enable => true,
MessageType.Disable => false,
_ => throw new ArgumentException("invalid message type")
};
_player.LastInput = DateTime.Now;
switch (control)
{
case InputType.Forward:
_player.Controls.Forward = isEnable;
break;
case InputType.Backward:
_player.Controls.Backward = isEnable;
break;
case InputType.Left:
_player.Controls.TurnLeft = isEnable;
break;
case InputType.Right:
_player.Controls.TurnRight = isEnable;
break;
case InputType.Shoot:
_player.Controls.Shoot = isEnable;
break;
default:
throw new ArgumentException("invalid control type");
}
}
}
public Task CloseAsync() => _binaryWebSocket.CloseAsync();
private enum MessageType : byte
{
Enable = 0x01,
Disable = 0x02
}
private enum InputType : byte
{
Forward = 0x01,
Backward = 0x02,
Left = 0x03,
Right = 0x04,
Shoot = 0x05
}
}

View file

@ -0,0 +1,8 @@
namespace TanksServer.Interactivity;
internal interface IWebsocketServerConnection
{
Task CloseAsync();
Task Done { get; }
}

View file

@ -3,7 +3,10 @@ using TanksServer.GameLogic;
namespace TanksServer.Interactivity;
internal sealed class PlayerServer(ILogger<PlayerServer> logger, TankSpawnQueue tankSpawnQueue)
internal sealed class PlayerServer(
ILogger<PlayerServer> logger,
TankSpawnQueue tankSpawnQueue
)
{
private readonly ConcurrentDictionary<string, Player> _players = new();

View file

@ -0,0 +1,87 @@
using Microsoft.Extensions.Hosting;
namespace TanksServer.Interactivity;
internal class WebsocketServer<T>(
ILogger logger
) : IHostedLifecycleService, IDisposable
where T : IWebsocketServerConnection
{
private readonly SemaphoreSlim _mutex = new(1, 1);
private bool _closing;
private readonly HashSet<T> _connections = [];
public async Task StoppingAsync(CancellationToken cancellationToken)
{
logger.LogInformation("closing connections");
await Locked(async () =>
{
_closing = true;
await Task.WhenAll(_connections.Select(c => c.CloseAsync()));
}, cancellationToken);
logger.LogInformation("closed connections");
}
protected Task ParallelForEachConnectionAsync(Func<T, Task> body)
{
_mutex.Wait();
try
{
return Task.WhenAll(_connections.Select(body));
}
finally
{
_mutex.Release();
}
}
protected Task AddConnection(T connection) => Locked(() =>
{
if (_closing)
{
logger.LogWarning("refusing connection because server is shutting down");
return connection.CloseAsync();
}
_connections.Add(connection);
return Task.CompletedTask;
}, CancellationToken.None);
protected Task RemoveConnection(T connection) => Locked(() =>
{
_connections.Remove(connection);
return Task.CompletedTask;
}, CancellationToken.None);
protected async Task HandleClientAsync(T connection)
{
await AddConnection(connection);
await connection.Done;
await RemoveConnection(connection);
}
private async Task Locked(Func<Task> action, CancellationToken cancellationToken)
{
await _mutex.WaitAsync(cancellationToken);
try
{
await action();
}
finally
{
_mutex.Release();
}
}
public void Dispose() => _mutex.Dispose();
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

View file

@ -1,10 +1,10 @@
namespace TanksServer.Models;
internal sealed record class Scores(int Kills = 0, int Deaths = 0)
internal sealed record class Scores
{
public int Kills { get; set; } = Kills;
public int Kills { get; set; }
public int Deaths { get; set; } = Deaths;
public int Deaths { get; set; }
public double Ratio
{
@ -14,7 +14,7 @@ internal sealed record class Scores(int Kills = 0, int Deaths = 0)
return 0;
if (Deaths == 0)
return Kills;
return Kills / (double)Deaths;
return Math.Round(Kills / (double)Deaths, 3);
}
}