proper web socket close
This commit is contained in:
parent
bc8d5ad6d0
commit
154539488a
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
namespace TanksServer;
|
||||
|
||||
public sealed class DisplayPixelBuffer(byte[] data)
|
||||
internal sealed class DisplayPixelBuffer(byte[] data)
|
||||
{
|
||||
public byte[] Data => data;
|
||||
|
||||
|
|
53
TanksServer/EasyWebSocket.cs
Normal file
53
TanksServer/EasyWebSocket.cs
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
namespace TanksServer;
|
||||
|
||||
public class MapDrawer(MapService map)
|
||||
internal class MapDrawer(MapService map)
|
||||
{
|
||||
private const uint GameFieldPixelCount = MapService.PixelsPerRow * MapService.PixelsPerColumn;
|
||||
|
||||
|
|
|
@ -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];
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
namespace TanksServer;
|
||||
|
||||
public class ServicePointDisplayConfiguration
|
||||
{
|
||||
public string Hostname { get; set; } = string.Empty;
|
||||
public int Port { get; set; }
|
||||
}
|
Loading…
Reference in a new issue