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.Net.WebSockets;
using System.Threading;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace TanksServer; namespace TanksServer;
internal sealed class ClientScreenServer internal sealed class ClientScreenServer(
ILogger<ClientScreenServer> logger,
ILoggerFactory loggerFactory
) : IHostedLifecycleService
{ {
private readonly List<ClientScreenServerConnection> _connections = new(); 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); _connections.Add(connection);
return connection; return connection.Done;
} }
public Task Send(DisplayPixelBuffer buf) public Task Send(DisplayPixelBuffer buf)
{ {
logger.LogDebug("Sending buffer to {} clients", _connections.Count);
return Task.WhenAll(_connections.Select(c => c.Send(buf))); 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; 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) public Task Send(DisplayPixelBuffer buf)
{ {
if (!_wantsNewFrame) if (!_wantsNewFrame)
return Task.CompletedTask; 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; namespace TanksServer;
public sealed class DisplayPixelBuffer(byte[] data) internal sealed class DisplayPixelBuffer(byte[] data)
{ {
public byte[] Data => 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; 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 int Count => data.Length * 8;
public bool IsReadOnly => false; public bool IsReadOnly => false;

View file

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

View file

@ -1,6 +1,6 @@
namespace TanksServer; namespace TanksServer;
public class MapService internal class MapService
{ {
public const int TilesPerRow = 44; public const int TilesPerRow = 44;
public const int TilesPerColumn = 20; public const int TilesPerColumn = 20;
@ -8,7 +8,8 @@ public class MapService
public const int PixelsPerRow = TilesPerRow * TileSize; public const int PixelsPerRow = TilesPerRow * TileSize;
public const int PixelsPerColumn = TilesPerColumn * TileSize; public const int PixelsPerColumn = TilesPerColumn * TileSize;
private string _map = """ private readonly string _map =
"""
############################################ ############################################
#...................##.....................# #...................##.....................#
#...................##.....................# #...................##.....................#

View file

@ -1,5 +1,6 @@
using System.IO; using System.IO;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
@ -27,11 +28,13 @@ internal static class Program
app.Map("/screen", async context => app.Map("/screen", async context =>
{ {
if (!context.WebSockets.IsWebSocketRequest) if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
return; return;
var ws = await context.WebSockets.AcceptWebSocketAsync(); }
var client = clientScreenServer.AddClient(ws);
await client.Send(buffer); using var ws = await context.WebSockets.AcceptWebSocketAsync();
await client.Done; await clientScreenServer.HandleClient(ws);
}); });
await app.RunAsync(); await app.RunAsync();
@ -45,6 +48,7 @@ internal static class Program
builder.Services.AddSingleton<MapService>(); builder.Services.AddSingleton<MapService>();
builder.Services.AddSingleton<MapDrawer>(); builder.Services.AddSingleton<MapDrawer>();
builder.Services.AddSingleton<ClientScreenServer>(); builder.Services.AddSingleton<ClientScreenServer>();
builder.Services.AddHostedService<ClientScreenServer>(sp => sp.GetRequiredService<ClientScreenServer>());
return builder.Build(); return builder.Build();
} }

View file

@ -3,7 +3,7 @@ using Microsoft.Extensions.Options;
namespace TanksServer; 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); 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); 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; }
}