merge websocket server logic
This commit is contained in:
parent
3cea9c967d
commit
57c0d229f1
|
@ -47,7 +47,7 @@ internal static class Endpoints
|
||||||
return Results.BadRequest();
|
return Results.BadRequest();
|
||||||
|
|
||||||
using var ws = await context.WebSockets.AcceptWebSocketAsync();
|
using var ws = await context.WebSockets.AcceptWebSocketAsync();
|
||||||
await clientScreenServer.HandleClient(ws, player);
|
await clientScreenServer.HandleClientAsync(ws, player);
|
||||||
return Results.Empty;
|
return Results.Empty;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ internal static class Endpoints
|
||||||
return Results.NotFound();
|
return Results.NotFound();
|
||||||
|
|
||||||
using var ws = await context.WebSockets.AcceptWebSocketAsync();
|
using var ws = await context.WebSockets.AcceptWebSocketAsync();
|
||||||
await controlsServer.HandleClient(ws, player);
|
await controlsServer.HandleClientAsync(ws, player);
|
||||||
return Results.Empty;
|
return Results.Empty;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using DisplayCommands;
|
using DisplayCommands;
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using TanksServer.Graphics;
|
using TanksServer.Graphics;
|
||||||
|
|
||||||
namespace TanksServer.Interactivity;
|
namespace TanksServer.Interactivity;
|
||||||
|
@ -10,54 +8,18 @@ internal sealed class ClientScreenServer(
|
||||||
ILogger<ClientScreenServer> logger,
|
ILogger<ClientScreenServer> logger,
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
IOptions<HostConfiguration> hostConfig
|
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 readonly TimeSpan _minFrameTime = TimeSpan.FromMilliseconds(hostConfig.Value.ClientDisplayMinFrameTimeMs);
|
||||||
private bool _closing;
|
|
||||||
|
|
||||||
public Task StoppingAsync(CancellationToken cancellationToken)
|
public Task HandleClientAsync(WebSocket socket, Guid? playerGuid)
|
||||||
{
|
=> base.HandleClientAsync(new(
|
||||||
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(
|
|
||||||
socket,
|
socket,
|
||||||
loggerFactory.CreateLogger<ClientScreenServerConnection>(),
|
loggerFactory.CreateLogger<ClientScreenServerConnection>(),
|
||||||
this,
|
|
||||||
_minFrameTime,
|
_minFrameTime,
|
||||||
playerGuid);
|
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;
|
|
||||||
|
|
||||||
|
|
||||||
public Task OnFrameDoneAsync(GamePixelGrid gamePixelGrid, PixelGrid observerPixels)
|
public Task OnFrameDoneAsync(GamePixelGrid gamePixelGrid, PixelGrid observerPixels)
|
||||||
{
|
=> ParallelForEachConnectionAsync(c => c.SendAsync(observerPixels, gamePixelGrid));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,10 @@ using TanksServer.Graphics;
|
||||||
|
|
||||||
namespace TanksServer.Interactivity;
|
namespace TanksServer.Interactivity;
|
||||||
|
|
||||||
internal sealed class ClientScreenServerConnection : IDisposable
|
internal sealed class ClientScreenServerConnection : IWebsocketServerConnection, IDisposable
|
||||||
{
|
{
|
||||||
private readonly ByteChannelWebSocket _channel;
|
private readonly ByteChannelWebSocket _channel;
|
||||||
private readonly ILogger<ClientScreenServerConnection> _logger;
|
private readonly ILogger<ClientScreenServerConnection> _logger;
|
||||||
private readonly ClientScreenServer _server;
|
|
||||||
private readonly SemaphoreSlim _wantedFrames = new(1);
|
private readonly SemaphoreSlim _wantedFrames = new(1);
|
||||||
private readonly Guid? _playerGuid;
|
private readonly Guid? _playerGuid;
|
||||||
private readonly PlayerScreenData? _playerScreenData;
|
private readonly PlayerScreenData? _playerScreenData;
|
||||||
|
@ -20,12 +19,10 @@ internal sealed class ClientScreenServerConnection : IDisposable
|
||||||
public ClientScreenServerConnection(
|
public ClientScreenServerConnection(
|
||||||
WebSocket webSocket,
|
WebSocket webSocket,
|
||||||
ILogger<ClientScreenServerConnection> logger,
|
ILogger<ClientScreenServerConnection> logger,
|
||||||
ClientScreenServer server,
|
|
||||||
TimeSpan minFrameTime,
|
TimeSpan minFrameTime,
|
||||||
Guid? playerGuid = null
|
Guid? playerGuid = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_server = server;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_minFrameTime = minFrameTime;
|
_minFrameTime = minFrameTime;
|
||||||
|
|
||||||
|
@ -91,9 +88,7 @@ internal sealed class ClientScreenServerConnection : IDisposable
|
||||||
{
|
{
|
||||||
await foreach (var _ in _channel.ReadAllAsync())
|
await foreach (var _ in _channel.ReadAllAsync())
|
||||||
_wantedFrames.Release();
|
_wantedFrames.Release();
|
||||||
|
|
||||||
_logger.LogTrace("done receiving");
|
_logger.LogTrace("done receiving");
|
||||||
_server.Remove(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task CloseAsync()
|
public Task CloseAsync()
|
||||||
|
|
|
@ -1,115 +1,19 @@
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
|
|
||||||
namespace TanksServer.Interactivity;
|
namespace TanksServer.Interactivity;
|
||||||
|
|
||||||
internal sealed class ControlsServer(ILogger<ControlsServer> logger, ILoggerFactory loggerFactory)
|
internal sealed class ControlsServer(
|
||||||
: IHostedLifecycleService
|
ILogger<ControlsServer> logger,
|
||||||
|
ILoggerFactory loggerFactory
|
||||||
|
) : WebsocketServer<ControlsServerConnection>(logger)
|
||||||
{
|
{
|
||||||
private readonly List<ControlsServerConnection> _connections = [];
|
public async Task HandleClientAsync(WebSocket ws, Player player)
|
||||||
|
|
||||||
public Task StoppingAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return Task.WhenAll(_connections.Select(c => c.CloseAsync()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task HandleClient(WebSocket ws, Player player)
|
|
||||||
{
|
{
|
||||||
logger.LogDebug("control client connected {}", player.Id);
|
logger.LogDebug("control client connected {}", player.Id);
|
||||||
var clientLogger = loggerFactory.CreateLogger<ControlsServerConnection>();
|
var clientLogger = loggerFactory.CreateLogger<ControlsServerConnection>();
|
||||||
var sock = new ControlsServerConnection(ws, clientLogger, this, player);
|
var sock = new ControlsServerConnection(ws, clientLogger, player);
|
||||||
_connections.Add(sock);
|
await AddConnection(sock);
|
||||||
return sock.Done;
|
await sock.Done;
|
||||||
}
|
await RemoveConnection(sock);
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
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();
|
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;
|
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
|
public double Ratio
|
||||||
{
|
{
|
||||||
|
@ -14,7 +14,7 @@ internal sealed record class Scores(int Kills = 0, int Deaths = 0)
|
||||||
return 0;
|
return 0;
|
||||||
if (Deaths == 0)
|
if (Deaths == 0)
|
||||||
return Kills;
|
return Kills;
|
||||||
return Kills / (double)Deaths;
|
return Math.Round(Kills / (double)Deaths, 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue