merge websocket server logic
This commit is contained in:
parent
3cea9c967d
commit
57c0d229f1
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
namespace TanksServer.Interactivity;
|
||||
|
||||
internal interface IWebsocketServerConnection
|
||||
{
|
||||
Task CloseAsync();
|
||||
|
||||
Task Done { get; }
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
87
tanks-backend/TanksServer/Interactivity/WebsocketServer.cs
Normal file
87
tanks-backend/TanksServer/Interactivity/WebsocketServer.cs
Normal 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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue