diff --git a/TanksServer/ClientScreenServer.cs b/TanksServer/ClientScreenServer.cs index 9c5b828..5ab644d 100644 --- a/TanksServer/ClientScreenServer.cs +++ b/TanksServer/ClientScreenServer.cs @@ -1,52 +1,60 @@ using System.Net.WebSockets; +using System.Threading; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace TanksServer; -internal sealed class ClientScreenServer +internal sealed class ClientScreenServer( + ILogger logger, + ILoggerFactory loggerFactory +) : IHostedLifecycleService { private readonly List _connections = new(); - public ClientScreenServerConnection AddClient(WebSocket socket) + public Task HandleClient(WebSocket socket) { - var connection = new ClientScreenServerConnection(socket); + logger.LogDebug("HandleClient"); + var connection = + new ClientScreenServerConnection(socket, loggerFactory.CreateLogger()); _connections.Add(connection); - return connection; + return connection.Done; } public Task Send(DisplayPixelBuffer buf) { + logger.LogDebug("Sending buffer to {} clients", _connections.Count); return Task.WhenAll(_connections.Select(c => c.Send(buf))); } + + public Task StoppingAsync(CancellationToken cancellationToken) + { + logger.LogInformation("closing connections"); + return Task.WhenAll(_connections.Select(c => c.CloseAsync())); + } + + 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; } -internal sealed class ClientScreenServerConnection +internal sealed class ClientScreenServerConnection(WebSocket webSocket, ILogger logger) + : EasyWebSocket(webSocket, logger, ArraySegment.Empty) { - private readonly WebSocket _socket; - private readonly Task _readTask; - private readonly TaskCompletionSource _completionSource = new(); private bool _wantsNewFrame = true; - public ClientScreenServerConnection(WebSocket webSocket) - { - _socket = webSocket; - _readTask = Read(); - } - - public Task Done => _completionSource.Task; - - private async Task Read() - { - while (true) - { - await _socket.ReceiveAsync(ArraySegment.Empty, default); - _wantsNewFrame = true; - } - } - public Task Send(DisplayPixelBuffer buf) { if (!_wantsNewFrame) return Task.CompletedTask; - return _socket.SendAsync(buf.Data, WebSocketMessageType.Binary, true, default); + return SendAsync(buf.Data); + } + + protected override Task ReceiveAsync(ArraySegment buffer) + { + _wantsNewFrame = true; + return Task.CompletedTask; } } diff --git a/TanksServer/DisplayPixelBuffer.cs b/TanksServer/DisplayPixelBuffer.cs index ea688c2..180dcdf 100644 --- a/TanksServer/DisplayPixelBuffer.cs +++ b/TanksServer/DisplayPixelBuffer.cs @@ -1,6 +1,6 @@ namespace TanksServer; -public sealed class DisplayPixelBuffer(byte[] data) +internal sealed class DisplayPixelBuffer(byte[] data) { public byte[] Data => data; diff --git a/TanksServer/EasyWebSocket.cs b/TanksServer/EasyWebSocket.cs new file mode 100644 index 0000000..9261f63 --- /dev/null +++ b/TanksServer/EasyWebSocket.cs @@ -0,0 +1,53 @@ +using System.Net.WebSockets; +using System.Threading; +using Microsoft.Extensions.Logging; + +namespace TanksServer; + +internal abstract class EasyWebSocket +{ + private readonly TaskCompletionSource _completionSource = new(); + + private readonly ILogger _logger; + private readonly WebSocket _socket; + private readonly Task _readLoop; + private readonly ArraySegment _buffer; + + protected EasyWebSocket(WebSocket socket, ILogger logger, ArraySegment buffer) + { + _socket = socket; + _logger = logger; + _buffer = buffer; + _readLoop = ReadLoopAsync(); + } + + public Task Done => _completionSource.Task; + + private async Task ReadLoopAsync() + { + do + { + var response = await _socket.ReceiveAsync(_buffer, CancellationToken.None); + if (response.CloseStatus.HasValue) + break; + + await ReceiveAsync(_buffer[..response.Count]); + } while (_socket.State == WebSocketState.Open); + } + + protected abstract Task ReceiveAsync(ArraySegment buffer); + + protected Task SendAsync(byte[] data) + { + _logger.LogTrace("sending {} bytes of data", _buffer.Count); + return _socket.SendAsync(data, WebSocketMessageType.Binary, true, CancellationToken.None); + } + + public async Task CloseAsync() + { + _logger.LogDebug("closing socket"); + await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); + await _readLoop; + _completionSource.SetResult(); + } +} diff --git a/TanksServer/FixedSizeBitFieldView.cs b/TanksServer/FixedSizeBitFieldView.cs index 20f9076..9b37fd4 100644 --- a/TanksServer/FixedSizeBitFieldView.cs +++ b/TanksServer/FixedSizeBitFieldView.cs @@ -2,7 +2,7 @@ using System.Collections; namespace TanksServer; -public sealed class FixedSizeBitFieldView(Memory data) : IList +internal sealed class FixedSizeBitFieldView(Memory data) : IList { public int Count => data.Length * 8; public bool IsReadOnly => false; diff --git a/TanksServer/MapDrawer.cs b/TanksServer/MapDrawer.cs index f601859..7f34012 100644 --- a/TanksServer/MapDrawer.cs +++ b/TanksServer/MapDrawer.cs @@ -1,6 +1,6 @@ namespace TanksServer; -public class MapDrawer(MapService map) +internal class MapDrawer(MapService map) { private const uint GameFieldPixelCount = MapService.PixelsPerRow * MapService.PixelsPerColumn; diff --git a/TanksServer/MapService.cs b/TanksServer/MapService.cs index 03671b7..6599f71 100644 --- a/TanksServer/MapService.cs +++ b/TanksServer/MapService.cs @@ -1,6 +1,6 @@ namespace TanksServer; -public class MapService +internal class MapService { public const int TilesPerRow = 44; public const int TilesPerColumn = 20; @@ -8,29 +8,30 @@ public class MapService public const int PixelsPerRow = TilesPerRow * TileSize; public const int PixelsPerColumn = TilesPerColumn * TileSize; - private string _map = """ - ############################################ - #...................##.....................# - #...................##.....................# - #.....####......................####.......# - #..........................................# - #............###...........###.............# - #............#...............#.............# - #...##.......#...............#......##.....# - #....#..............................#......# - #....#..##......................##..#......# - #....#..##......................##..#......# - #....#..............................#......# - #...##.......#...............#......##.....# - #............#...............#.............# - #............###...........###.............# - #..........................................# - #.....####......................####.......# - #...................##.....................# - #...................##.....................# - ############################################ - """ - .ReplaceLineEndings(string.Empty); + private readonly string _map = + """ + ############################################ + #...................##.....................# + #...................##.....................# + #.....####......................####.......# + #..........................................# + #............###...........###.............# + #............#...............#.............# + #...##.......#...............#......##.....# + #....#..............................#......# + #....#..##......................##..#......# + #....#..##......................##..#......# + #....#..............................#......# + #...##.......#...............#......##.....# + #............#...............#.............# + #............###...........###.............# + #..........................................# + #.....####......................####.......# + #...................##.....................# + #...................##.....................# + ############################################ + """ + .ReplaceLineEndings(string.Empty); private char this[int tileX, int tileY] => _map[tileX + tileY * TilesPerRow]; diff --git a/TanksServer/Program.cs b/TanksServer/Program.cs index 1d51777..b6bb9df 100644 --- a/TanksServer/Program.cs +++ b/TanksServer/Program.cs @@ -1,5 +1,6 @@ using System.IO; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; @@ -27,11 +28,13 @@ internal static class Program app.Map("/screen", async context => { if (!context.WebSockets.IsWebSocketRequest) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; return; - var ws = await context.WebSockets.AcceptWebSocketAsync(); - var client = clientScreenServer.AddClient(ws); - await client.Send(buffer); - await client.Done; + } + + using var ws = await context.WebSockets.AcceptWebSocketAsync(); + await clientScreenServer.HandleClient(ws); }); await app.RunAsync(); @@ -45,6 +48,7 @@ internal static class Program builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddHostedService(sp => sp.GetRequiredService()); return builder.Build(); } diff --git a/TanksServer/ServicePointDisplay.cs b/TanksServer/ServicePointDisplay.cs index cf1220d..018fe6e 100644 --- a/TanksServer/ServicePointDisplay.cs +++ b/TanksServer/ServicePointDisplay.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Options; namespace TanksServer; -public class ServicePointDisplay(IOptions options) +internal class ServicePointDisplay(IOptions options) { private readonly UdpClient _udpClient = new(options.Value.Hostname, options.Value.Port); @@ -12,3 +12,9 @@ public class ServicePointDisplay(IOptions opti return _udpClient.SendAsync(buffer.Data); } } + +internal class ServicePointDisplayConfiguration +{ + public string Hostname { get; set; } = string.Empty; + public int Port { get; set; } +} diff --git a/TanksServer/ServicePointDisplayConfiguration.cs b/TanksServer/ServicePointDisplayConfiguration.cs deleted file mode 100644 index 8165a68..0000000 --- a/TanksServer/ServicePointDisplayConfiguration.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace TanksServer; - -public class ServicePointDisplayConfiguration -{ - public string Hostname { get; set; } = string.Empty; - public int Port { get; set; } -}