proper web socket close

This commit is contained in:
Vinzenz Schroeter 2024-04-06 20:32:54 +02:00
parent bc8d5ad6d0
commit 154539488a
9 changed files with 130 additions and 65 deletions

View file

@ -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<ClientScreenServer> logger,
ILoggerFactory loggerFactory
) : IHostedLifecycleService
{
private readonly List<ClientScreenServerConnection> _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<ClientScreenServerConnection>());
_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<ClientScreenServerConnection> logger)
: EasyWebSocket(webSocket, logger, ArraySegment<byte>.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<byte>.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<byte> buffer)
{
_wantsNewFrame = true;
return Task.CompletedTask;
}
}

View file

@ -1,6 +1,6 @@
namespace TanksServer;
public sealed class DisplayPixelBuffer(byte[] data)
internal sealed class DisplayPixelBuffer(byte[] data)
{
public byte[] Data => data;

View file

@ -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<byte> _buffer;
protected EasyWebSocket(WebSocket socket, ILogger logger, ArraySegment<byte> 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<byte> 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();
}
}

View file

@ -2,7 +2,7 @@ using System.Collections;
namespace TanksServer;
public sealed class FixedSizeBitFieldView(Memory<byte> data) : IList<bool>
internal sealed class FixedSizeBitFieldView(Memory<byte> data) : IList<bool>
{
public int Count => data.Length * 8;
public bool IsReadOnly => false;

View file

@ -1,6 +1,6 @@
namespace TanksServer;
public class MapDrawer(MapService map)
internal class MapDrawer(MapService map)
{
private const uint GameFieldPixelCount = MapService.PixelsPerRow * MapService.PixelsPerColumn;

View file

@ -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];

View file

@ -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<MapService>();
builder.Services.AddSingleton<MapDrawer>();
builder.Services.AddSingleton<ClientScreenServer>();
builder.Services.AddHostedService<ClientScreenServer>(sp => sp.GetRequiredService<ClientScreenServer>());
return builder.Build();
}

View file

@ -3,7 +3,7 @@ using Microsoft.Extensions.Options;
namespace TanksServer;
public class ServicePointDisplay(IOptions<ServicePointDisplayConfiguration> options)
internal class ServicePointDisplay(IOptions<ServicePointDisplayConfiguration> options)
{
private readonly UdpClient _udpClient = new(options.Value.Hostname, options.Value.Port);
@ -12,3 +12,9 @@ public class ServicePointDisplay(IOptions<ServicePointDisplayConfiguration> opti
return _udpClient.SendAsync(buffer.Data);
}
}
internal class ServicePointDisplayConfiguration
{
public string Hostname { get; set; } = string.Empty;
public int Port { get; set; }
}

View file

@ -1,7 +0,0 @@
namespace TanksServer;
public class ServicePointDisplayConfiguration
{
public string Hostname { get; set; } = string.Empty;
public int Port { get; set; }
}