From 40eba7a7c7874b269934e9e30750abd1d0b50d2f Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sat, 13 Apr 2024 12:33:08 +0200 Subject: [PATCH] improve ByteChannelWebSocket --- DisplayCommands/Cp437Grid.cs | 5 +- TanksServer/GameLogic/MoveBullets.cs | 4 +- .../Interactivity/ByteChannelWebSocket.cs | 69 ++++--------------- .../Interactivity/ClientScreenServer.cs | 54 ++++++++------- TanksServer/Interactivity/ControlsServer.cs | 58 ++++++++-------- 5 files changed, 79 insertions(+), 111 deletions(-) diff --git a/DisplayCommands/Cp437Grid.cs b/DisplayCommands/Cp437Grid.cs index 3198f37..e8650e4 100644 --- a/DisplayCommands/Cp437Grid.cs +++ b/DisplayCommands/Cp437Grid.cs @@ -6,6 +6,7 @@ namespace DisplayCommands; public sealed class Cp437Grid(ushort width, ushort height) : IEquatable { private readonly ByteGrid _byteGrid = new(width, height); + private readonly Encoding _encoding = Encoding.GetEncoding(437); public ushort Height { get; } = height; @@ -13,8 +14,6 @@ public sealed class Cp437Grid(ushort width, ushort height) : IEquatable Data => _byteGrid.Data; - private readonly Encoding _encoding = Encoding.GetEncoding(437); - public char this[ushort x, ushort y] { get => ByteToChar(_byteGrid[x, y]); @@ -68,4 +67,4 @@ public sealed class Cp437Grid(ushort width, ushort height) : IEquatable HashCode.Combine(_byteGrid, Height, Width); public static bool operator ==(Cp437Grid? left, Cp437Grid? right) => Equals(left, right); public static bool operator !=(Cp437Grid? left, Cp437Grid? right) => !Equals(left, right); -} \ No newline at end of file +} diff --git a/TanksServer/GameLogic/MoveBullets.cs b/TanksServer/GameLogic/MoveBullets.cs index 6338bd8..e29c56c 100644 --- a/TanksServer/GameLogic/MoveBullets.cs +++ b/TanksServer/GameLogic/MoveBullets.cs @@ -14,8 +14,8 @@ internal sealed class MoveBullets(BulletManager bullets, IOptions -/// Hacky class for easier semantics -/// -internal sealed class ByteChannelWebSocket : Channel> +internal sealed class ByteChannelWebSocket(WebSocket socket, ILogger logger, int messageSize) { - private readonly ILogger _logger; - private readonly WebSocket _socket; - private readonly Task _backgroundDone; - private readonly byte[] _buffer; + private readonly byte[] _buffer = new byte[messageSize]; - private readonly Channel> _outgoing = Channel.CreateUnbounded>(); - private readonly Channel> _incoming = Channel.CreateUnbounded>(); + public ValueTask SendAsync(ReadOnlyMemory data) => + socket.SendAsync(data, WebSocketMessageType.Binary, true, CancellationToken.None); - public ByteChannelWebSocket(WebSocket socket, ILogger logger, int messageSize) - { - _socket = socket; - _logger = logger; - _buffer = new byte[messageSize]; - _backgroundDone = Task.WhenAll(ReadLoopAsync(), WriteLoopAsync()); - - Reader = _incoming.Reader; - Writer = _outgoing.Writer; - } - - private async Task ReadLoopAsync() + public async IAsyncEnumerable> ReadAllAsync() { while (true) { - if (_socket.State is not (WebSocketState.Open or WebSocketState.CloseSent)) + if (socket.State is not (WebSocketState.Open or WebSocketState.CloseSent)) break; - var response = await _socket.ReceiveAsync(_buffer, CancellationToken.None); + var response = await socket.ReceiveAsync(_buffer, CancellationToken.None); if (response.MessageType == WebSocketMessageType.Close) { - if (_socket.State == WebSocketState.CloseReceived) - await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, + if (socket.State == WebSocketState.CloseReceived) + await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); break; } if (response.Count != _buffer.Length) { - await _socket.CloseAsync( + await socket.CloseOutputAsync( WebSocketCloseStatus.InvalidPayloadData, "response has unexpected size", CancellationToken.None); break; } - await _incoming.Writer.WriteAsync(_buffer.ToArray()); + yield return _buffer.ToArray(); } - if (_socket.State != WebSocketState.Closed) + if (socket.State != WebSocketState.Closed) Debugger.Break(); - - _incoming.Writer.Complete(); - } - - private async Task WriteLoopAsync() - { - await foreach (var data in _outgoing.Reader.ReadAllAsync()) - { - _logger.LogTrace("sending {} bytes of data", data.Length); - try - { - await _socket.SendAsync(data, WebSocketMessageType.Binary, true, CancellationToken.None); - } - catch (WebSocketException wsEx) - { - _logger.LogDebug(wsEx, "send failed"); - } - } - - await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); } public async Task CloseAsync() { - _logger.LogDebug("closing socket"); - _outgoing.Writer.Complete(); - await _backgroundDone; + logger.LogDebug("closing socket"); + await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); } -} +} \ No newline at end of file diff --git a/TanksServer/Interactivity/ClientScreenServer.cs b/TanksServer/Interactivity/ClientScreenServer.cs index efbf76d..7f7c18d 100644 --- a/TanksServer/Interactivity/ClientScreenServer.cs +++ b/TanksServer/Interactivity/ClientScreenServer.cs @@ -14,6 +14,13 @@ internal sealed class ClientScreenServer( private readonly ConcurrentDictionary _connections = new(); private bool _closing; + public Task StoppingAsync(CancellationToken cancellationToken) + { + logger.LogInformation("closing connections"); + _closing = true; + return Task.WhenAll(_connections.Keys.Select(c => c.CloseAsync())); + } + public Task HandleClient(WebSocket socket) { if (_closing) @@ -30,29 +37,26 @@ internal sealed class ClientScreenServer( return connection.Done; } - public Task StoppingAsync(CancellationToken cancellationToken) + private void Remove(ClientScreenServerConnection connection) { - logger.LogInformation("closing connections"); - _closing = true; - return Task.WhenAll(_connections.Keys.Select(c => c.CloseAsync())); + _connections.TryRemove(connection, out _); } + public IEnumerable GetConnections() => _connections.Keys; + 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 void Remove(ClientScreenServerConnection connection) => _connections.TryRemove(connection, out _); - - public IEnumerable GetConnections() => _connections.Keys; - internal sealed class ClientScreenServerConnection : IDisposable { private readonly ByteChannelWebSocket _channel; - private readonly SemaphoreSlim _wantedFrames = new(1); - private readonly ClientScreenServer _server; private readonly ILogger _logger; + private readonly ClientScreenServer _server; + private readonly SemaphoreSlim _wantedFrames = new(1); + private PixelGrid? _lastSentPixels; public ClientScreenServerConnection(WebSocket webSocket, ILogger logger, @@ -60,12 +64,23 @@ internal sealed class ClientScreenServer( { _server = server; _logger = logger; - _channel = new(webSocket, logger, 0); + _channel = new ByteChannelWebSocket(webSocket, logger, 0); Done = ReceiveAsync(); } - public async Task SendAsync(PixelGrid buf) + public Task Done { get; } + + public void Dispose() { + _wantedFrames.Dispose(); + Done.Dispose(); + } + + public async Task SendAsync(PixelGrid pixels) + { + if (_lastSentPixels == pixels) + return; + if (!await _wantedFrames.WaitAsync(TimeSpan.Zero)) { _logger.LogTrace("client does not want a frame yet"); @@ -75,7 +90,8 @@ internal sealed class ClientScreenServer( _logger.LogTrace("sending"); try { - await _channel.Writer.WriteAsync(buf.Data); + await _channel.SendAsync(pixels.Data); + _lastSentPixels = pixels; } catch (ChannelClosedException) { @@ -85,7 +101,7 @@ internal sealed class ClientScreenServer( private async Task ReceiveAsync() { - await foreach (var _ in _channel.Reader.ReadAllAsync()) + await foreach (var _ in _channel.ReadAllAsync()) _wantedFrames.Release(); _logger.LogTrace("done receiving"); @@ -97,13 +113,5 @@ internal sealed class ClientScreenServer( _logger.LogDebug("closing connection"); return _channel.CloseAsync(); } - - public Task Done { get; } - - public void Dispose() - { - _wantedFrames.Dispose(); - Done.Dispose(); - } } -} \ No newline at end of file +} diff --git a/TanksServer/Interactivity/ControlsServer.cs b/TanksServer/Interactivity/ControlsServer.cs index f23b66a..1d081ad 100644 --- a/TanksServer/Interactivity/ControlsServer.cs +++ b/TanksServer/Interactivity/ControlsServer.cs @@ -8,6 +8,11 @@ internal sealed class ControlsServer(ILogger logger, ILoggerFact { private readonly List _connections = []; + 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); @@ -17,10 +22,7 @@ internal sealed class ControlsServer(ILogger logger, ILoggerFact return sock.Done; } - public Task StoppingAsync(CancellationToken cancellationToken) - { - return Task.WhenAll(_connections.Select(c => c.CloseAsync())); - } + private void Remove(ControlsServerConnection connection) => _connections.Remove(connection); public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; @@ -28,17 +30,12 @@ internal sealed class ControlsServer(ILogger logger, ILoggerFact public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; - private void Remove(ControlsServerConnection connection) - { - _connections.Remove(connection); - } - private sealed class ControlsServerConnection { private readonly ByteChannelWebSocket _binaryWebSocket; private readonly ILogger _logger; - private readonly ControlsServer _server; private readonly Player _player; + private readonly ControlsServer _server; public ControlsServerConnection(WebSocket socket, ILogger logger, ControlsServer server, Player player) @@ -46,30 +43,15 @@ internal sealed class ControlsServer(ILogger logger, ILoggerFact _logger = logger; _server = server; _player = player; - _binaryWebSocket = new(socket, logger, 2); + _binaryWebSocket = new ByteChannelWebSocket(socket, logger, 2); Done = ReceiveAsync(); } public Task Done { get; } - private enum MessageType : byte - { - Enable = 0x01, - Disable = 0x02, - } - - private enum InputType : byte - { - Forward = 0x01, - Backward = 0x02, - Left = 0x03, - Right = 0x04, - Shoot = 0x05 - } - private async Task ReceiveAsync() { - await foreach (var buffer in _binaryWebSocket.Reader.ReadAllAsync()) + await foreach (var buffer in _binaryWebSocket.ReadAllAsync()) { var type = (MessageType)buffer.Span[0]; var control = (InputType)buffer.Span[1]; @@ -84,7 +66,7 @@ internal sealed class ControlsServer(ILogger logger, ILoggerFact }; _player.LastInput = DateTime.Now; - + switch (control) { case InputType.Forward: @@ -110,6 +92,24 @@ internal sealed class ControlsServer(ILogger logger, ILoggerFact _server.Remove(this); } - public Task CloseAsync() => _binaryWebSocket.CloseAsync(); + 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 + } } }