From 08ff994475795a2b2586d1dcec0709c06f0919b8 Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 5 Apr 2025 21:58:31 +0200 Subject: [PATCH 1/5] Add Jung.SimpleWebSocket.IntegrationTest project Improve race condition safety by using atomic methods Improve dispose processes of the server and the client Improve error handling Add LogLevel property to the server options --- Jung.SimpleWebSocket.sln | 6 + .../EventArguments/ClientDisconnectedArgs.cs | 12 +- .../Models/SimpleWebSocketServerOptions.cs | 5 + .../SimpleWebSocketClient.cs | 106 ++++++++---- .../SimpleWebSocketServer.cs | 157 +++++++++++++----- ...ng.SimpleWebSocket.IntegrationTests.csproj | 24 +++ .../ProcedureProvider.cs | 85 ++++++++++ .../Program.cs | 104 ++++++++++++ .../TestProcedure.cs | 33 ++++ .../Tests/BaseTest.cs | 11 ++ .../Tests/DisplayEventsServerTest.cs | 78 +++++++++ .../Tests/SendMessagesLoopTest.cs | 89 ++++++++++ .../Tests/TestInformationAttribute.cs | 12 ++ 13 files changed, 641 insertions(+), 81 deletions(-) create mode 100644 tests/Jung.SimpleWebSocket.IntegrationTests/Jung.SimpleWebSocket.IntegrationTests.csproj create mode 100644 tests/Jung.SimpleWebSocket.IntegrationTests/ProcedureProvider.cs create mode 100644 tests/Jung.SimpleWebSocket.IntegrationTests/Program.cs create mode 100644 tests/Jung.SimpleWebSocket.IntegrationTests/TestProcedure.cs create mode 100644 tests/Jung.SimpleWebSocket.IntegrationTests/Tests/BaseTest.cs create mode 100644 tests/Jung.SimpleWebSocket.IntegrationTests/Tests/DisplayEventsServerTest.cs create mode 100644 tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs create mode 100644 tests/Jung.SimpleWebSocket.IntegrationTests/Tests/TestInformationAttribute.cs diff --git a/Jung.SimpleWebSocket.sln b/Jung.SimpleWebSocket.sln index d6ee15b..6e0dc3c 100644 --- a/Jung.SimpleWebSocket.sln +++ b/Jung.SimpleWebSocket.sln @@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jung.SimpleWebSocket", "src EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jung.SimpleWebSocket.UnitTests", "tests\Jung.SimpleWebSocket.UnitTests\Jung.SimpleWebSocket.UnitTests.csproj", "{26725C3C-8E90-49AC-9EE4-2A77ADB2229D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jung.SimpleWebSocket.IntegrationTests", "tests\Jung.SimpleWebSocket.IntegrationTests\Jung.SimpleWebSocket.IntegrationTests.csproj", "{D052400A-9F1E-4F2E-98B9-AF74A7A16A2F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {26725C3C-8E90-49AC-9EE4-2A77ADB2229D}.Debug|Any CPU.Build.0 = Debug|Any CPU {26725C3C-8E90-49AC-9EE4-2A77ADB2229D}.Release|Any CPU.ActiveCfg = Release|Any CPU {26725C3C-8E90-49AC-9EE4-2A77ADB2229D}.Release|Any CPU.Build.0 = Release|Any CPU + {D052400A-9F1E-4F2E-98B9-AF74A7A16A2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D052400A-9F1E-4F2E-98B9-AF74A7A16A2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D052400A-9F1E-4F2E-98B9-AF74A7A16A2F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D052400A-9F1E-4F2E-98B9-AF74A7A16A2F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs index fe16b81..51cf852 100644 --- a/src/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs +++ b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs @@ -3,9 +3,9 @@ namespace Jung.SimpleWebSocket.Models.EventArguments; -/// -/// Represents the arguments of the event when a client disconnects from the server. -/// -/// The description why the closing status was initiated. -/// The unique identifier of the client that disconnected from the server. -public record ClientDisconnectedArgs(string ClosingStatusDescription, string ClientId); \ No newline at end of file +/// +/// Represents the arguments of the event when a client disconnects from the server. +/// +/// The reason for the connection closure. if the remote party closed the WebSocket connection without completing the close handshake. +/// The unique identifier of the client that disconnected from the server. +public record ClientDisconnectedArgs(string? ClosingStatusDescription, string ClientId); diff --git a/src/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs b/src/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs index ea58fde..e832858 100644 --- a/src/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs +++ b/src/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs @@ -19,5 +19,10 @@ public class SimpleWebSocketServerOptions /// Gets or sets the port of the server. /// public int Port { get; set; } + + /// + /// Gets or sets the log level of the server. + /// + public string LogLevel { get; set; } = "Information"; } } diff --git a/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs b/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs index 6793e3d..c41f183 100644 --- a/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs +++ b/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs @@ -23,7 +23,7 @@ namespace Jung.SimpleWebSocket /// The port to connect to /// The web socket request path /// A logger to write internal log messages - public class SimpleWebSocketClient(string hostName, int port, string requestPath, ILogger? logger = null) : IWebSocketClient, IDisposable + public class SimpleWebSocketClient(string hostName, int port, string requestPath, ILogger? logger = null) : IWebSocketClient, IDisposable { /// public string HostName { get; } = hostName; @@ -64,8 +64,21 @@ public class SimpleWebSocketClient(string hostName, int port, string requestPath /// /// A value indicating whether the client is disconnecting. + /// 0=Not disconnecting, 1=Disconnecting /// - private bool _clientIsDisconnecting; + private int _clientIsDisconnecting = 0; + + + /// + /// A value indicating whether the client is disposed. + /// 0=Not Disposed, 1=Disposed + /// + private int _disposed = 0; + + /// + /// Gets a value indicating whether the client is disposed. + /// + private bool Disposed => _disposed == 1; /// /// The logger to write internal log messages. @@ -75,6 +88,8 @@ public class SimpleWebSocketClient(string hostName, int port, string requestPath /// public async Task ConnectAsync(CancellationToken? cancellationToken = null) { + ThrowIfDisposed(); + if (IsConnected) throw new WebSocketClientException(message: "Client is already connected"); cancellationToken ??= CancellationToken.None; @@ -108,18 +123,21 @@ public async Task ConnectAsync(CancellationToken? cancellationToken = null) /// public async Task DisconnectAsync(string closingStatusDescription = "Closing", CancellationToken? cancellationToken = null) { - if (_clientIsDisconnecting) throw new WebSocketClientException("Client is already disconnecting"); - _clientIsDisconnecting = true; + // Make sure we only disconnect once + if (Interlocked.Exchange(ref _clientIsDisconnecting, 1) == 1) + { + return; + } cancellationToken ??= CancellationToken.None; var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value, _cancellationTokenSource.Token); - _logger?.LogInformation("Disconnecting from Server"); if (_webSocket != null && (_webSocket.State == WebSocketState.Open || _webSocket.State == WebSocketState.CloseReceived)) { try { + _logger?.LogInformation("Disconnecting from Server"); await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, closingStatusDescription, linkedTokenSource.Token); } catch (Exception exception) @@ -135,7 +153,6 @@ public async Task DisconnectAsync(string closingStatusDescription = "Closing", C } } } - _client?.Dispose(); } /// @@ -161,6 +178,8 @@ private async Task HandleWebSocketInitiation(TcpClientWrapper client, Cancellati /// public async Task SendMessageAsync(string message, CancellationToken? cancellationToken = null) { + ThrowIfDisposed(); + if (!IsConnected) throw new WebSocketClientException(message: "Client is not connected"); if (_webSocket == null) throw new WebSocketClientException(message: "WebSocket is not initialized"); @@ -176,6 +195,7 @@ public async Task SendMessageAsync(string message, CancellationToken? cancellati } catch (Exception exception) { + _logger?.LogError(exception, "Error sending message"); throw new WebSocketClientException(message: "Error sending message", innerException: exception); } } @@ -194,45 +214,61 @@ private async Task ProcessWebSocketMessagesAsync(IWebSocket webSocket, Cancellat throw new InvalidOperationException("WebSocket is not initialized"); } - var buffer = new byte[1024 * 4]; // Buffer for incoming data - while (webSocket.State == WebSocketState.Open) + try { + var buffer = new byte[1024 * 4]; // Buffer for incoming data + while (webSocket.State == WebSocketState.Open) + { - // Read the next message - WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); + // Read the next message + WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); - if (result.MessageType == WebSocketMessageType.Text) - { - // Handle the text message - string receivedMessage = Encoding.UTF8.GetString(buffer, 0, result.Count); - _logger?.LogDebug("Message received: {message}", receivedMessage); - _ = Task.Run(() => MessageReceived?.Invoke(this, new MessageReceivedArgs(receivedMessage)), cancellationToken); - } - else if (result.MessageType == WebSocketMessageType.Binary) - { - // Handle the binary message - _logger?.LogDebug("Binary message received, length: {length} bytes", result.Count); - _ = Task.Run(() => BinaryMessageReceived?.Invoke(this, new BinaryMessageReceivedArgs(buffer[..result.Count])), cancellationToken); - } - // We have to check if the client is disconnecting here, - // because then we already sent the close message and we don't want to send another one - else if (result.MessageType == WebSocketMessageType.Close && !_clientIsDisconnecting) - { - _logger?.LogInformation("Received close message from server"); - _ = Task.Run(() => Disconnected?.Invoke(this, new DisconnectedArgs(result.CloseStatusDescription ?? string.Empty)), cancellationToken); - await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None); - break; + if (result.MessageType == WebSocketMessageType.Text) + { + // Handle the text message + string receivedMessage = Encoding.UTF8.GetString(buffer, 0, result.Count); + _logger?.LogDebug("Message received: {message}", receivedMessage); + _ = Task.Run(() => MessageReceived?.Invoke(this, new MessageReceivedArgs(receivedMessage)), cancellationToken); + } + else if (result.MessageType == WebSocketMessageType.Binary) + { + // Handle the binary message + _logger?.LogDebug("Binary message received, length: {length} bytes", result.Count); + _ = Task.Run(() => BinaryMessageReceived?.Invoke(this, new BinaryMessageReceivedArgs(buffer[..result.Count])), cancellationToken); + } + // We have to check if the client is disconnecting here, + // because then we already sent the close message and we don't want to send another one + else if (result.MessageType == WebSocketMessageType.Close && _clientIsDisconnecting == 0) + { + _logger?.LogInformation("Received close message from server"); + _ = Task.Run(() => Disconnected?.Invoke(this, new DisconnectedArgs(result.CloseStatusDescription ?? string.Empty)), cancellationToken); + await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None); + break; + } } } + catch (Exception exception) + { + _logger?.LogError(exception, "Error processing WebSocket messages. Connection Closed."); + _ = Task.Run(() => Disconnected?.Invoke(this, new DisconnectedArgs(exception.Message)), cancellationToken); + } } /// public void Dispose() { - _cancellationTokenSource?.Cancel(); - _stream?.Dispose(); - _client?.Dispose(); - GC.SuppressFinalize(this); + if (Interlocked.Exchange(ref _disposed, 1) == 0) + { + _cancellationTokenSource?.Cancel(); + _stream?.Dispose(); + _client?.Dispose(); + GC.SuppressFinalize(this); + } + } + + private void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(Disposed, this); } } } \ No newline at end of file diff --git a/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index 4b298cf..10a5941 100644 --- a/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -27,7 +27,7 @@ namespace Jung.SimpleWebSocket /// /// The options for the server /// A logger to write internal log messages - public class SimpleWebSocketServer(SimpleWebSocketServerOptions options, ILogger? logger = null) : IWebSocketServer, IDisposable + public class SimpleWebSocketServer(SimpleWebSocketServerOptions options, ILogger? logger = null) : IWebSocketServer, IDisposable { /// public IPAddress LocalIpAddress { get; } = options.LocalIpAddress; @@ -73,12 +73,35 @@ public class SimpleWebSocketServer(SimpleWebSocketServerOptions options, ILogger /// /// A flag indicating whether the server is started. /// - private bool _isStarted; + public bool IsStarted => _isStarted == 1; /// /// A flag indicating whether the server is shutting down. /// - private bool _serverShuttingDown; + private bool IsShuttingDown => _serverShuttingDown == 1; + + /// + /// A flag indicating whether the server is disposed. + /// + private bool Disposed => _disposed == 1; + + /// + /// A flag indicating whether the server is started. + /// 0 = false, 1 = true + /// + private int _isStarted; + + /// + /// A flag indicating whether the server is shutting down. + /// 0 = false, 1 = true + /// + private int _serverShuttingDown; + + /// + /// A flag indicating whether the server is disposed. + /// 0 = false, 1 = trues + /// + private int _disposed; /// /// A cancellation token source to cancel the server. @@ -96,7 +119,7 @@ public class SimpleWebSocketServer(SimpleWebSocketServerOptions options, ILogger /// /// The options for the server /// A logger to write internal log messages - public SimpleWebSocketServer(IOptions options, ILogger? logger = null) + public SimpleWebSocketServer(IOptions options, ILogger? logger = null) : this(options.Value, logger) { } @@ -107,7 +130,7 @@ public SimpleWebSocketServer(IOptions options, ILo /// The options for the server /// A wrapped tcp listener /// >A logger to write internal log messages - internal SimpleWebSocketServer(SimpleWebSocketServerOptions options, ITcpListener tcpListener, ILogger? logger = null) + internal SimpleWebSocketServer(SimpleWebSocketServerOptions options, ITcpListener tcpListener, ILogger? logger = null) : this(options, logger) { _tcpListener = tcpListener; @@ -116,8 +139,13 @@ internal SimpleWebSocketServer(SimpleWebSocketServerOptions options, ITcpListene /// public void Start(CancellationToken? cancellationToken = null) { - if (_isStarted) throw new WebSocketServerException("Server is already started"); - _isStarted = true; + ThrowIfDisposed(); + + if (Interlocked.Exchange(ref _isStarted, 1) == 1) + { + throw new WebSocketServerException("Server is already started"); + } + cancellationToken ??= CancellationToken.None; _cancellationTokenSource = new CancellationTokenSource(); @@ -154,36 +182,58 @@ public void Start(CancellationToken? cancellationToken = null) /// public async Task ShutdownServer(CancellationToken? cancellationToken = null) { - if (!_isStarted) throw new WebSocketServerException("Server is not started"); - if (_serverShuttingDown) throw new WebSocketServerException("Server is already shutting down"); - _serverShuttingDown = true; + ThrowIfDisposed(); + + if (Interlocked.Exchange(ref _serverShuttingDown, 1) == 1) + { + return; + } + + if (Interlocked.Exchange(ref _isStarted, 0) == 0) + { + Logger?.LogInformation("Server is not started"); + return; + } cancellationToken ??= CancellationToken.None; var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value, _cancellationTokenSource.Token); Logger?.LogInformation("Stopping server..."); + // copying the active clients to avoid a collection modified exception var activeClients = ActiveClients.Values.ToArray(); foreach (var client in activeClients) { - if (client.WebSocket != null && client.WebSocket.State == WebSocketState.Open) + try { - await client.WebSocket.CloseAsync(WebSocketCloseStatus.EndpointUnavailable, "Server is shutting down", linkedTokenSource.Token); - ActiveClients.TryRemove(client.Id, out _); - client?.Dispose(); + if (client.WebSocket != null && client.WebSocket.State == WebSocketState.Open) + { + await client.WebSocket.CloseAsync(WebSocketCloseStatus.EndpointUnavailable, "Server is shutting down", linkedTokenSource.Token); + ActiveClients.TryRemove(client.Id, out _); + client?.Dispose(); + } + } + catch + { + // Ignore the exception, because it's no the servers problem if a client does not close the connection } } + _cancellationTokenSource?.Cancel(); _tcpListener?.Dispose(); _tcpListener = null; + ActiveClients.Clear(); + _serverShuttingDown = 0; Logger?.LogInformation("Server stopped"); } /// public async Task SendMessageAsync(string clientId, string message, CancellationToken? cancellationToken = null) { + ThrowIfDisposed(); + // Find and check the client if (!ActiveClients.TryGetValue(clientId, out var client)) throw new WebSocketServerException(message: "Client not found"); if (client.WebSocket == null) throw new WebSocketServerException(message: "Client is not connected"); @@ -195,7 +245,7 @@ public async Task SendMessageAsync(string clientId, string message, Cancellation { // Send the message var buffer = Encoding.UTF8.GetBytes(message); - await client.WebSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, linkedTokenSource.Token); + await client.WebSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, linkedTokenSource.Token).ConfigureAwait(false); ; Logger?.LogDebug("Message sent: {message}.", message); } catch (Exception exception) @@ -209,6 +259,8 @@ public async Task SendMessageAsync(string clientId, string message, Cancellation /// public WebSocketServerClient GetClientById(string clientId) { + ThrowIfDisposed(); + if (!ActiveClients.TryGetValue(clientId, out var client)) throw new WebSocketServerException(message: "Client not found"); return client; } @@ -216,6 +268,8 @@ public WebSocketServerClient GetClientById(string clientId) /// public void ChangeClientId(WebSocketServerClient client, string newId) { + ThrowIfDisposed(); + // if the client is not found or the new id is already in use, throw an exception if (!ActiveClients.TryGetValue(client.Id, out var _)) throw new ClientNotFoundException(message: "A client with the given id was not found"); if (ActiveClients.ContainsKey(newId)) throw new ClientIdAlreadyExistsException(message: "A client with the new id already exists"); @@ -285,7 +339,7 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT { // If the client was added and the server is not shutting down, handle the disconnected client // The client is not added if the connection was rejected - if (!_serverShuttingDown) + if (!IsShuttingDown) { flow.HandleDisconnectedClient(); } @@ -307,35 +361,47 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C } var webSocket = client.WebSocket; - + string? closeStatusDescription = null; var buffer = new byte[1024 * 4]; // Buffer for incoming data - while (webSocket.State == WebSocketState.Open) - { - cancellationToken.ThrowIfCancellationRequested(); - // Read the next message - WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); - if (result.MessageType == WebSocketMessageType.Text) - { - // Handle the text message - string receivedMessage = Encoding.UTF8.GetString(buffer, 0, result.Count); - Logger?.LogDebug("Message received: {message}", receivedMessage); - AsyncEventRaiser.RaiseAsyncInNewTask(MessageReceived, this, new ClientMessageReceivedArgs(receivedMessage, client.Id), cancellationToken); - } - else if (result.MessageType == WebSocketMessageType.Binary) + try + { + while (webSocket.State == WebSocketState.Open) { - // Handle the binary message - Logger?.LogDebug("Binary message received, length: {length} bytes", result.Count); - AsyncEventRaiser.RaiseAsyncInNewTask(BinaryMessageReceived, this, new ClientBinaryMessageReceivedArgs(buffer[..result.Count], client.Id), cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + // Read the next message + WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); + + if (result.MessageType == WebSocketMessageType.Text) + { + // Handle the text message + string receivedMessage = Encoding.UTF8.GetString(buffer, 0, result.Count); + Logger?.LogDebug("Message received: {message}", receivedMessage); + AsyncEventRaiser.RaiseAsyncInNewTask(MessageReceived, this, new ClientMessageReceivedArgs(receivedMessage, client.Id), cancellationToken); + } + else if (result.MessageType == WebSocketMessageType.Binary) + { + // Handle the binary message + Logger?.LogDebug("Binary message received, length: {length} bytes", result.Count); + AsyncEventRaiser.RaiseAsyncInNewTask(BinaryMessageReceived, this, new ClientBinaryMessageReceivedArgs(buffer[..result.Count], client.Id), cancellationToken); + } + // We have to check if the is shutting down here, + // because then we already sent the close message and we don't want to send another one + else if (result.MessageType == WebSocketMessageType.Close && !IsShuttingDown) + { + Logger?.LogInformation("Received close message from Client"); + closeStatusDescription = result.CloseStatusDescription; + await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None); + break; + } } - // We have to check if the is shutting down here, - // because then we already sent the close message and we don't want to send another one - else if (result.MessageType == WebSocketMessageType.Close && !_serverShuttingDown) + } + finally + { + // if we leave the loop, the client disconnected + if (!IsShuttingDown) { - Logger?.LogInformation("Received close message from Client"); - AsyncEventRaiser.RaiseAsyncInNewTask(ClientDisconnected, this, new ClientDisconnectedArgs(result.CloseStatusDescription ?? string.Empty, client.Id), cancellationToken); - await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None); - break; + AsyncEventRaiser.RaiseAsyncInNewTask(ClientDisconnected, this, new ClientDisconnectedArgs(closeStatusDescription, client.Id), cancellationToken); } } } @@ -343,10 +409,21 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C /// public void Dispose() { + if (Interlocked.Exchange(ref _disposed, 1) == 1) + { + return; + } + + ShutdownServer().GetAwaiter().GetResult(); _cancellationTokenSource?.Cancel(); _tcpListener?.Dispose(); _tcpListener = null; GC.SuppressFinalize(this); } + + private void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(Disposed, this); + } } } \ No newline at end of file diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Jung.SimpleWebSocket.IntegrationTests.csproj b/tests/Jung.SimpleWebSocket.IntegrationTests/Jung.SimpleWebSocket.IntegrationTests.csproj new file mode 100644 index 0000000..38a2488 --- /dev/null +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Jung.SimpleWebSocket.IntegrationTests.csproj @@ -0,0 +1,24 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/ProcedureProvider.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/ProcedureProvider.cs new file mode 100644 index 0000000..88aad7b --- /dev/null +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/ProcedureProvider.cs @@ -0,0 +1,85 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Jung.SimpleWebSocket.IntegrationTests.Tests; +using Microsoft.Extensions.DependencyInjection; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace Jung.SimpleWebSocket.IntegrationTests +{ + internal class ProcedureProvider + { + private IOrderedEnumerable _procedures; + + public ProcedureProvider() + { + _procedures = LoadProcedures(); + } + + private IOrderedEnumerable LoadProcedures() + { + var result = new List(); + + var types = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsSubclassOf(typeof(BaseTest))); + foreach (var type in types) + { + var testAttribute = (TestInformationAttribute?)Attribute.GetCustomAttribute(type, typeof(TestInformationAttribute)); + if (testAttribute == null) + { + Console.WriteLine($"The test class {type.Name} has no TestInformationAttribute."); + continue; + } + + result.Add(new TestProcedure(testAttribute.Role, testAttribute.Description, type)); + } + return result.OrderBy(x => x.Role); + } + + /// + /// Get the names of the procedures. + /// + /// The names of the procedures. + public string[] GetNames() + { + return [.. _procedures.Select(x => $"{x.Role} - {x.Name}: {x.Description}")]; + } + + /// + /// Get a procedure by its index + /// + /// The index of the procedure + /// + public TestProcedure GetProcedure(int index) + { + if (!HasIndex(index)) + { + throw new IndexOutOfRangeException("There is no procedure at the given index."); + } + + return _procedures.ElementAt(index); + } + + /// + /// Try to get a procedure by name. + /// + /// The name of the procedure. + /// The procedure. + /// True if the procedure was found, false otherwise. + public bool TryGetProcedure(int index, [NotNullWhen(true)] out TestProcedure? procedure) + { + procedure = null; + if (HasIndex(index)) + { + procedure = GetProcedure(index); + return true; + } + return false; + } + + internal bool HasIndex(int index) + { + return _procedures.Count() > index && index >= 0; + } + } +} \ No newline at end of file diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Program.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/Program.cs new file mode 100644 index 0000000..868e403 --- /dev/null +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Program.cs @@ -0,0 +1,104 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Jung.SimpleWebSocket.IntegrationTests.Tests; +using Jung.SimpleWebSocket.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog; + +namespace Jung.SimpleWebSocket.IntegrationTests +{ + public class Program + { + /// + /// Main entry point for the application. + /// + /// The command line arguments. + public static async Task Main(string[] args) + { + var procedureProvider = new ProcedureProvider(); + + Console.WriteLine("Available tests:\n"); + + string[] procedureNames = procedureProvider.GetNames(); + for (int i = 0; i < procedureNames.Length; i++) + { + Console.WriteLine($"{i + 1}: {procedureNames[i]}"); + } + + int chosenProcedureIndex; + do + { + Console.Write("\nEnter the number of the test you want to run: "); + var userInput = Console.ReadLine(); + if (userInput != null) + { + userInput = userInput.Trim().ToLower(); + if (userInput == "exit") + { + return; + } + + if (!int.TryParse(userInput, out int procedureNumber)) + { + Console.WriteLine("Invalid input. Please enter a number."); + continue; + } + + if (procedureNumber >= 1 && procedureNumber <= procedureNames.Length) + { + chosenProcedureIndex = procedureNumber - 1; + break; + } + } + } while (true); + + var procedure = procedureProvider.GetProcedure(chosenProcedureIndex); + var serviceProvider = CreateServiceProvider(procedure); + var logger = serviceProvider.GetRequiredService>(); + + try + { + if (serviceProvider.GetService(procedure.ProcedureType) is not BaseTest test) + { + logger.LogError("The chosen test procedure could not be loaded"); + } + else + { + Console.WriteLine($"\nRunning test: {procedure.Name}: {procedure.Description}"); + await test.RunAsync(); + } + } + catch (Exception exception) + { + logger.LogError(exception, "An error occurred while running the procedure."); + } + } + + private static ServiceProvider CreateServiceProvider(TestProcedure procedure) + { + var serviceCollection = new ServiceCollection(); + + Log.Logger = new LoggerConfiguration() + .WriteTo.File($"{procedure.Name}-{DateTime.Now:g}-{Guid.NewGuid():n}.txt", rollingInterval: RollingInterval.Day) + .MinimumLevel.Debug() + .CreateLogger(); + + serviceCollection.AddSerilog(); + serviceCollection.AddLogging(); + + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + + serviceCollection.Configure(options => + { + options.LocalIpAddress = System.Net.IPAddress.Any; + options.Port = 8085; + }); + return serviceCollection.BuildServiceProvider(); + } + } +} \ No newline at end of file diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/TestProcedure.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/TestProcedure.cs new file mode 100644 index 0000000..312a4d8 --- /dev/null +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/TestProcedure.cs @@ -0,0 +1,33 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + + +namespace Jung.SimpleWebSocket.IntegrationTests +{ + internal class TestProcedure + { + public string Role; + public string Description; + public Type ProcedureType; + + public TestProcedure(string role, string description, Type type) + { + Role = role; + Description = description; + ProcedureType = type; + } + + public string Name + { + get + { + var result = ProcedureType.Name; + if (ProcedureType.Name.EndsWith("Test")) + { + result = result[0..^4]; + } + return result; + } + } + } +} \ No newline at end of file diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/BaseTest.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/BaseTest.cs new file mode 100644 index 0000000..1e5f3a5 --- /dev/null +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/BaseTest.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.Logging; + +namespace Jung.SimpleWebSocket.IntegrationTests.Tests +{ + internal abstract class BaseTest(ILogger logger) + { + protected readonly ILogger _logger = logger; + + internal abstract Task RunAsync(); + } +} diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/DisplayEventsServerTest.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/DisplayEventsServerTest.cs new file mode 100644 index 0000000..e165705 --- /dev/null +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/DisplayEventsServerTest.cs @@ -0,0 +1,78 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Jung.SimpleWebSocket.Models.EventArguments; +using Microsoft.Extensions.Logging; + +namespace Jung.SimpleWebSocket.IntegrationTests.Tests +{ + [TestInformation(Role = "Server", Description = "Display the events of the server.")] + internal class DisplayEventsTest(SimpleWebSocketServer simpleWebSocketServer, ILogger logger) : BaseTest(logger) + { + /// + /// The SimpleWebSocketServer instance. + /// + public SimpleWebSocketServer SimpleWebSocketServer { get; } = simpleWebSocketServer; + + /// + /// Runs the server instance. + /// + internal override async Task RunAsync() + { + InitializeEventHandlers(); + + SimpleWebSocketServer.Start(); + + Console.WriteLine("Press any key to stop the SimpleWebSocketServer..."); + Console.ReadKey(); + + UnsubscribeEventHandlers(); + + await SimpleWebSocketServer.ShutdownServer(); + } + + private void InitializeEventHandlers() + { + SimpleWebSocketServer.ClientConnected += SimpleWebSocketServer_ClientConnected; + SimpleWebSocketServer.ClientDisconnected += SimpleWebSocketServer_ClientDisconnected; + SimpleWebSocketServer.MessageReceived += SimpleWebSocketServer_MessageReceived; + SimpleWebSocketServer.BinaryMessageReceived += SimpleWebSocketServer_BinaryMessageReceived; + SimpleWebSocketServer.ClientUpgradeRequestReceivedAsync += ClientUpgradeRequestReceived; + } + + private void UnsubscribeEventHandlers() + { + SimpleWebSocketServer.ClientConnected -= SimpleWebSocketServer_ClientConnected; + SimpleWebSocketServer.ClientDisconnected -= SimpleWebSocketServer_ClientDisconnected; + SimpleWebSocketServer.MessageReceived -= SimpleWebSocketServer_MessageReceived; + SimpleWebSocketServer.BinaryMessageReceived -= SimpleWebSocketServer_BinaryMessageReceived; + SimpleWebSocketServer.ClientUpgradeRequestReceivedAsync -= ClientUpgradeRequestReceived; + } + + private void SimpleWebSocketServer_ClientConnected(object? sender, ClientConnectedArgs e) + { + Console.WriteLine($"Client connected: {e.ClientId}"); + } + + private void SimpleWebSocketServer_ClientDisconnected(object? sender, ClientDisconnectedArgs e) + { + Console.WriteLine($"Client disconnected: {e.ClientId}"); + } + + private void SimpleWebSocketServer_MessageReceived(object? sender, ClientMessageReceivedArgs e) + { + Console.WriteLine($"Message received from {e.ClientId}: {e.Message}"); + } + + private void SimpleWebSocketServer_BinaryMessageReceived(object? sender, ClientBinaryMessageReceivedArgs e) + { + Console.WriteLine($"Binary message received from {e.ClientId}: {string.Join(' ', e.Message)}"); + } + + private static async Task ClientUpgradeRequestReceived(object sender, ClientUpgradeRequestReceivedArgs e, CancellationToken cancellationToken) + { + Console.WriteLine($"Upgrade request received from {e.Client.Id}."); + await Task.CompletedTask; + } + } +} diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs new file mode 100644 index 0000000..35367ee --- /dev/null +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs @@ -0,0 +1,89 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Jung.SimpleWebSocket.Delegates; +using Microsoft.Extensions.Logging; + +namespace Jung.SimpleWebSocket.IntegrationTests.Tests +{ + [TestInformation(Role = "Client", Description = "Stability test - Sends messages at random times (between 5s and 20s)")] + internal class SendMessagesLoopTest(ILogger logger, ILogger clientLogger) : BaseTest(logger) + { + internal override async Task RunAsync() + { + var cancellationTokenSource = new CancellationTokenSource(); + var token = cancellationTokenSource.Token; + + using var client = new SimpleWebSocketClient("localhost", 8085, "", clientLogger); + + InitializeClientEvents(client, cancellationTokenSource); + + try + { + await client.ConnectAsync(); + } + catch (Exception exception) + { + _logger.LogError("Failed to connect to the server: {ExceptionMessage}", exception.Message); + return; + } + + var task = Task.Run(async () => await SendRandomMessages(client, token)); + + Console.WriteLine("Press any key to disconnect from the server..."); + Console.ReadKey(); + + await client.DisconnectAsync(); + cancellationTokenSource.Cancel(); + await task; + } + + private static async Task SendRandomMessages(SimpleWebSocketClient client, CancellationToken cancellationToken) + { + Random random = new(); + int messageCount = 1; + while (!cancellationToken.IsCancellationRequested) + { + try + { + string message = $"Message {messageCount++} sent at {DateTime.Now}"; + await client.SendMessageAsync(message, cancellationToken).ConfigureAwait(false); + Console.WriteLine($"Sent: {message}"); + + int delay = random.Next(5000, 20001); // Random delay between 5s (5000ms) and 20s (20000ms) + await Task.Delay(delay, cancellationToken); + } + catch (Exception exception) + { + if (exception is not OperationCanceledException) + { + Console.WriteLine($"Exception: {exception.Message}"); + } + break; + } + } + } + + private static void InitializeClientEvents(SimpleWebSocketClient client, CancellationTokenSource cancellationTokenSource) + { + DisconnectedEventHandler? disconnectedHandler = null; + MessageReceivedEventHandler? messageReceivedHandler = null; + + disconnectedHandler = (sender, e) => + { + Console.WriteLine("Disconnected"); + cancellationTokenSource.Cancel(); + client.Disconnected -= disconnectedHandler; + client.MessageReceived -= messageReceivedHandler; + }; + + messageReceivedHandler = (sender, e) => + { + Console.WriteLine($"Message received: {e.Message}"); + }; + + client.Disconnected += disconnectedHandler; + client.MessageReceived += messageReceivedHandler; + } + } +} diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/TestInformationAttribute.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/TestInformationAttribute.cs new file mode 100644 index 0000000..e386c96 --- /dev/null +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/TestInformationAttribute.cs @@ -0,0 +1,12 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + + +namespace Jung.SimpleWebSocket.IntegrationTests.Tests +{ + internal class TestInformationAttribute : Attribute + { + public string Role { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + } +} \ No newline at end of file From a099665d29607198255d4a5b51f00beb3d9beb88 Mon Sep 17 00:00:00 2001 From: Christoph Date: Thu, 9 Oct 2025 13:20:37 +0200 Subject: [PATCH 2/5] Use logger instead of console directly to output information Add WebSocketConnectionException --- .../WebSocketConnectionException.cs | 31 +++++++ .../SimpleWebSocketClient.cs | 7 +- ...ng.SimpleWebSocket.IntegrationTests.csproj | 5 +- .../Program.cs | 5 +- .../Tests/DisplayEventsServerTest.cs | 16 ++-- .../Tests/SendMessagesLoopTest.cs | 87 ++++++++++++------- 6 files changed, 105 insertions(+), 46 deletions(-) create mode 100644 src/Jung.SimpleWebSocket/Exceptions/WebSocketConnectionException.cs diff --git a/src/Jung.SimpleWebSocket/Exceptions/WebSocketConnectionException.cs b/src/Jung.SimpleWebSocket/Exceptions/WebSocketConnectionException.cs new file mode 100644 index 0000000..c93e501 --- /dev/null +++ b/src/Jung.SimpleWebSocket/Exceptions/WebSocketConnectionException.cs @@ -0,0 +1,31 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +namespace Jung.SimpleWebSocket.Exceptions +{ + /// + /// Represents an exception that occurs during a WebSocket connection attempt. + /// + public class WebSocketConnectionException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public WebSocketConnectionException() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public WebSocketConnectionException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error + /// message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or if no inner exception is + /// specified. + public WebSocketConnectionException(string message, Exception innerException) : base(message, innerException) { } + } +} diff --git a/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs b/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs index c41f183..e60b72f 100644 --- a/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs +++ b/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs @@ -8,6 +8,7 @@ using Jung.SimpleWebSocket.Models.EventArguments; using Jung.SimpleWebSocket.Wrappers; using Microsoft.Extensions.Logging; +using System.Net.Sockets; using System.Net.WebSockets; using System.Text; @@ -109,7 +110,11 @@ public async Task ConnectAsync(CancellationToken? cancellationToken = null) catch (Exception exception) { _logger?.LogError(exception, "Error connecting to Server"); - if (exception is WebSocketException) + if (exception is SocketException) + { + throw new WebSocketConnectionException(message: "Error connecting to Server", innerException: exception); + } + else if (exception is WebSocketException) { throw; } diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Jung.SimpleWebSocket.IntegrationTests.csproj b/tests/Jung.SimpleWebSocket.IntegrationTests/Jung.SimpleWebSocket.IntegrationTests.csproj index 38a2488..f1ca8ca 100644 --- a/tests/Jung.SimpleWebSocket.IntegrationTests/Jung.SimpleWebSocket.IntegrationTests.csproj +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Jung.SimpleWebSocket.IntegrationTests.csproj @@ -10,6 +10,7 @@ + @@ -17,8 +18,4 @@ - - - - diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Program.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/Program.cs index 868e403..ec0b4a8 100644 --- a/tests/Jung.SimpleWebSocket.IntegrationTests/Program.cs +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Program.cs @@ -62,11 +62,11 @@ public static async Task Main(string[] args) { if (serviceProvider.GetService(procedure.ProcedureType) is not BaseTest test) { - logger.LogError("The chosen test procedure could not be loaded"); + logger.LogError("The chosen test procedure {procedureType} could not be loaded.", procedure.ProcedureType.FullName); } else { - Console.WriteLine($"\nRunning test: {procedure.Name}: {procedure.Description}"); + logger.LogInformation("Running test: {procedureName} ({procedureDescription})", procedure.Name, procedure.Description); await test.RunAsync(); } } @@ -82,6 +82,7 @@ private static ServiceProvider CreateServiceProvider(TestProcedure procedure) Log.Logger = new LoggerConfiguration() .WriteTo.File($"{procedure.Name}-{DateTime.Now:g}-{Guid.NewGuid():n}.txt", rollingInterval: RollingInterval.Day) + .WriteTo.Console(Serilog.Events.LogEventLevel.Information, outputTemplate: "{Level:u3}: {Message:lj}{NewLine}{Exception}") .MinimumLevel.Debug() .CreateLogger(); diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/DisplayEventsServerTest.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/DisplayEventsServerTest.cs index e165705..129ae9e 100644 --- a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/DisplayEventsServerTest.cs +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/DisplayEventsServerTest.cs @@ -7,7 +7,7 @@ namespace Jung.SimpleWebSocket.IntegrationTests.Tests { [TestInformation(Role = "Server", Description = "Display the events of the server.")] - internal class DisplayEventsTest(SimpleWebSocketServer simpleWebSocketServer, ILogger logger) : BaseTest(logger) + internal class DisplayEventsTest(ILogger logger, SimpleWebSocketServer simpleWebSocketServer) : BaseTest(logger) { /// /// The SimpleWebSocketServer instance. @@ -51,28 +51,28 @@ private void UnsubscribeEventHandlers() private void SimpleWebSocketServer_ClientConnected(object? sender, ClientConnectedArgs e) { - Console.WriteLine($"Client connected: {e.ClientId}"); + _logger.LogInformation("Client connected: {ClientId}", e.ClientId); } private void SimpleWebSocketServer_ClientDisconnected(object? sender, ClientDisconnectedArgs e) { - Console.WriteLine($"Client disconnected: {e.ClientId}"); + _logger.LogInformation("Client disconnected: {ClientId}", e.ClientId); } private void SimpleWebSocketServer_MessageReceived(object? sender, ClientMessageReceivedArgs e) { - Console.WriteLine($"Message received from {e.ClientId}: {e.Message}"); + _logger.LogInformation("Message received from {ClientId}: {Message}", e.ClientId, e.Message); } private void SimpleWebSocketServer_BinaryMessageReceived(object? sender, ClientBinaryMessageReceivedArgs e) { - Console.WriteLine($"Binary message received from {e.ClientId}: {string.Join(' ', e.Message)}"); + _logger.LogInformation("Binary message received from {ClientId}: {messages}", e.ClientId, string.Join(' ', e.Message)); } - private static async Task ClientUpgradeRequestReceived(object sender, ClientUpgradeRequestReceivedArgs e, CancellationToken cancellationToken) + private Task ClientUpgradeRequestReceived(object sender, ClientUpgradeRequestReceivedArgs e, CancellationToken cancellationToken) { - Console.WriteLine($"Upgrade request received from {e.Client.Id}."); - await Task.CompletedTask; + _logger.LogInformation("Upgrade request received from {ClientId}.", e.Client.Id); + return Task.CompletedTask; } } } diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs index 35367ee..e8d44d6 100644 --- a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs @@ -1,7 +1,8 @@ // This file is part of the Jung SimpleWebSocket project. // The project is licensed under the MIT license. -using Jung.SimpleWebSocket.Delegates; +using Jung.SimpleWebSocket.Exceptions; +using Jung.SimpleWebSocket.Models.EventArguments; using Microsoft.Extensions.Logging; namespace Jung.SimpleWebSocket.IntegrationTests.Tests @@ -16,29 +17,38 @@ internal override async Task RunAsync() using var client = new SimpleWebSocketClient("localhost", 8085, "", clientLogger); - InitializeClientEvents(client, cancellationTokenSource); + InitializeClientEvents(client); try { await client.ConnectAsync(); + + var task = Task.Run(async () => await SendRandomMessages(client, token)); + + Console.WriteLine("Press any key to disconnect from the server..."); + Console.ReadKey(); + + await client.DisconnectAsync(); + cancellationTokenSource.Cancel(); + await task; } - catch (Exception exception) + catch (WebSocketConnectionException exception) { _logger.LogError("Failed to connect to the server: {ExceptionMessage}", exception.Message); + + } + catch (Exception exception) + { + _logger.LogError("An error occurred: {ExceptionMessage}", exception.Message); return; } - - var task = Task.Run(async () => await SendRandomMessages(client, token)); - - Console.WriteLine("Press any key to disconnect from the server..."); - Console.ReadKey(); - - await client.DisconnectAsync(); - cancellationTokenSource.Cancel(); - await task; + finally + { + UnsubscribeEvents(client); + } } - private static async Task SendRandomMessages(SimpleWebSocketClient client, CancellationToken cancellationToken) + private async Task SendRandomMessages(SimpleWebSocketClient client, CancellationToken cancellationToken) { Random random = new(); int messageCount = 1; @@ -46,9 +56,16 @@ private static async Task SendRandomMessages(SimpleWebSocketClient client, Cance { try { + if(!client.IsConnected) + { + // If the client is not connected, exit the loop. + _logger.LogWarning("Client is not connected. Stopping message sending loop."); + break; + } + string message = $"Message {messageCount++} sent at {DateTime.Now}"; await client.SendMessageAsync(message, cancellationToken).ConfigureAwait(false); - Console.WriteLine($"Sent: {message}"); + _logger.LogInformation("Sent: {message}", message); int delay = random.Next(5000, 20001); // Random delay between 5s (5000ms) and 20s (20000ms) await Task.Delay(delay, cancellationToken); @@ -57,33 +74,41 @@ private static async Task SendRandomMessages(SimpleWebSocketClient client, Cance { if (exception is not OperationCanceledException) { - Console.WriteLine($"Exception: {exception.Message}"); + _logger.LogError(exception, "Error while sending the message."); } break; } } } - private static void InitializeClientEvents(SimpleWebSocketClient client, CancellationTokenSource cancellationTokenSource) + private void InitializeClientEvents(SimpleWebSocketClient client) { - DisconnectedEventHandler? disconnectedHandler = null; - MessageReceivedEventHandler? messageReceivedHandler = null; + client.Disconnected += Client_Disconnected; + client.MessageReceived += Client_MessageReceived; + client.BinaryMessageReceived += Client_BinaryMessageReceived; + } - disconnectedHandler = (sender, e) => - { - Console.WriteLine("Disconnected"); - cancellationTokenSource.Cancel(); - client.Disconnected -= disconnectedHandler; - client.MessageReceived -= messageReceivedHandler; - }; + private void UnsubscribeEvents(SimpleWebSocketClient client) + { + client.Disconnected -= Client_Disconnected; + client.MessageReceived -= Client_MessageReceived; + client.BinaryMessageReceived -= Client_BinaryMessageReceived; + } - messageReceivedHandler = (sender, e) => - { - Console.WriteLine($"Message received: {e.Message}"); - }; - client.Disconnected += disconnectedHandler; - client.MessageReceived += messageReceivedHandler; + private void Client_BinaryMessageReceived(object sender, BinaryMessageReceivedArgs e) + { + _logger.LogInformation("Binary message received: {binaryMessage}", BitConverter.ToString(e.Message)); + } + + private void Client_MessageReceived(object sender, MessageReceivedArgs e) + { + _logger.LogInformation("Message received: {message}", e.Message); + } + + private void Client_Disconnected(object sender, DisconnectedArgs e) + { + _logger.LogInformation("Disconnected"); } } } From 798930bef4cd830c573eb08c93d404f428b6ee6a Mon Sep 17 00:00:00 2001 From: Christoph Date: Thu, 9 Oct 2025 14:06:56 +0200 Subject: [PATCH 3/5] Fix Dispose method throwing ObjectDisposedException --- .../SimpleWebSocketServer.cs | 25 ++++++++++++++----- .../ProcedureProvider.cs | 1 - .../Tests/BaseTest.cs | 5 +++- .../SimpleWebSocketTest.cs | 4 +-- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index 10a5941..94df987 100644 --- a/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -103,6 +103,12 @@ public class SimpleWebSocketServer(SimpleWebSocketServerOptions options, ILogger /// private int _disposed; + /// + /// A flag indicating whether the server is disposing. + /// 0 = false, 1 = trues + /// + private int _disposing; + /// /// A cancellation token source to cancel the server. /// @@ -409,16 +415,23 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C /// public void Dispose() { - if (Interlocked.Exchange(ref _disposed, 1) == 1) + if (Interlocked.Exchange(ref _disposing, 1) == 1) { return; } - ShutdownServer().GetAwaiter().GetResult(); - _cancellationTokenSource?.Cancel(); - _tcpListener?.Dispose(); - _tcpListener = null; - GC.SuppressFinalize(this); + try + { + ShutdownServer().GetAwaiter().GetResult(); + _cancellationTokenSource?.Cancel(); + _tcpListener?.Dispose(); + _tcpListener = null; + GC.SuppressFinalize(this); + } + finally + { + Interlocked.Exchange(ref _disposed, 1); + } } private void ThrowIfDisposed() diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/ProcedureProvider.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/ProcedureProvider.cs index 88aad7b..ee586bd 100644 --- a/tests/Jung.SimpleWebSocket.IntegrationTests/ProcedureProvider.cs +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/ProcedureProvider.cs @@ -2,7 +2,6 @@ // The project is licensed under the MIT license. using Jung.SimpleWebSocket.IntegrationTests.Tests; -using Microsoft.Extensions.DependencyInjection; using System.Diagnostics.CodeAnalysis; using System.Reflection; diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/BaseTest.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/BaseTest.cs index 1e5f3a5..bbc3e60 100644 --- a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/BaseTest.cs +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/BaseTest.cs @@ -1,4 +1,7 @@ -using Microsoft.Extensions.Logging; +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Microsoft.Extensions.Logging; namespace Jung.SimpleWebSocket.IntegrationTests.Tests { diff --git a/tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs b/tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs index 2621a9f..762f70d 100644 --- a/tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs +++ b/tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs @@ -18,7 +18,7 @@ namespace Jung.SimpleWebSocket.UnitTests public class SimpleWebSocketTest { private ILoggerMockHelper _serverLoggerMockHelper; - private ILoggerMockHelper _clientLoggerMockHelper; + private ILoggerMockHelper _clientLoggerMockHelper; [OneTimeSetUp] public void SetUpOnce() @@ -151,7 +151,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() server.ClientDisconnected += (sender, obj) => { - receivedClosingDescription = obj.ClosingStatusDescription; + receivedClosingDescription = obj.ClosingStatusDescription ?? string.Empty; disconnectResetEvent.Set(); }; From 431693c555b923597edd9444363134d8c2e6ae20 Mon Sep 17 00:00:00 2001 From: Christoph Date: Thu, 9 Oct 2025 14:49:06 +0200 Subject: [PATCH 4/5] Add ConfigureAwait with continueOnCapturedContext parameter set to false to all awaited methods in the library --- .../Flows/ClientHandlingFlow.cs | 8 +++---- .../SimpleWebSocketClient.cs | 16 +++++++------- .../SimpleWebSocketServer.cs | 22 +++++++++---------- .../Utility/AsyncEventRaiser.cs | 4 ++-- .../Wrappers/NetworkStreamWrapper.cs | 2 +- .../Wrappers/TcpListenerWrapper.cs | 2 +- .../Wrappers/WebSocketUpgradeHandler.cs | 14 ++++++------ 7 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs b/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs index 58772f3..c5f68b8 100644 --- a/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs +++ b/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs @@ -64,7 +64,7 @@ internal async Task LoadRequestContext() { var stream = Client.ClientConnection!.GetStream(); _upgradeHandler = new WebSocketUpgradeHandler(stream); - Request = await _upgradeHandler.AwaitContextAsync(_cancellationToken); + Request = await _upgradeHandler.AwaitContextAsync(_cancellationToken).ConfigureAwait(false); } /// @@ -76,7 +76,7 @@ internal async Task AcceptWebSocketAsync() ThrowForResponseContextNotInitialized(_responseContext); // The client is accepted - await _upgradeHandler!.AcceptWebSocketAsync(Request!, _responseContext, null, _cancellationToken); + await _upgradeHandler!.AcceptWebSocketAsync(Request!, _responseContext, null, _cancellationToken).ConfigureAwait(false); // Use the web socket for the client Client.UseWebSocket(_upgradeHandler.CreateWebSocket(isServer: true)); @@ -90,7 +90,7 @@ internal async Task AcceptWebSocketAsync() internal async Task RejectWebSocketAsync(WebContext responseContext) { // The client is rejected - await _upgradeHandler!.RejectWebSocketAsync(responseContext, _cancellationToken); + await _upgradeHandler!.RejectWebSocketAsync(responseContext, _cancellationToken).ConfigureAwait(false); Cleanup(); } @@ -113,7 +113,7 @@ internal void HandleDisconnectedClient() internal async Task RaiseUpgradeEventAsync(AsyncEventHandler? clientUpgradeRequestReceivedAsync) { var eventArgs = new ClientUpgradeRequestReceivedArgs(Client, Request!, _logger); - await AsyncEventRaiser.RaiseAsync(clientUpgradeRequestReceivedAsync, server, eventArgs, _cancellationToken); + await AsyncEventRaiser.RaiseAsync(clientUpgradeRequestReceivedAsync, server, eventArgs, _cancellationToken).ConfigureAwait(false); _responseContext = eventArgs.ResponseContext; return eventArgs; } diff --git a/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs b/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs index e60b72f..89e5383 100644 --- a/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs +++ b/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs @@ -101,8 +101,8 @@ public async Task ConnectAsync(CancellationToken? cancellationToken = null) try { _client = new TcpClientWrapper(); - await _client.ConnectAsync(HostName, Port); - await HandleWebSocketInitiation(_client, linkedTokenSource.Token); + await _client.ConnectAsync(HostName, Port).ConfigureAwait(false); + await HandleWebSocketInitiation(_client, linkedTokenSource.Token).ConfigureAwait(false); _logger?.LogDebug("Connection upgraded, now listening."); _ = ProcessWebSocketMessagesAsync(_webSocket!, linkedTokenSource.Token); @@ -143,7 +143,7 @@ public async Task DisconnectAsync(string closingStatusDescription = "Closing", C try { _logger?.LogInformation("Disconnecting from Server"); - await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, closingStatusDescription, linkedTokenSource.Token); + await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, closingStatusDescription, linkedTokenSource.Token).ConfigureAwait(false); } catch (Exception exception) { @@ -173,8 +173,8 @@ private async Task HandleWebSocketInitiation(TcpClientWrapper client, Cancellati var socketWrapper = new WebSocketUpgradeHandler(_stream); var requestContext = WebContext.CreateRequest(HostName, Port, RequestPath); - await socketWrapper.SendUpgradeRequestAsync(requestContext, cancellationToken); - var response = await socketWrapper.AwaitContextAsync(cancellationToken); + await socketWrapper.SendUpgradeRequestAsync(requestContext, cancellationToken).ConfigureAwait(false); + var response = await socketWrapper.AwaitContextAsync(cancellationToken).ConfigureAwait(false); WebSocketUpgradeHandler.ValidateUpgradeResponse(response, requestContext); _webSocket = socketWrapper.CreateWebSocket(isServer: false); @@ -195,7 +195,7 @@ public async Task SendMessageAsync(string message, CancellationToken? cancellati { // Send the message var buffer = Encoding.UTF8.GetBytes(message); - await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, linkedTokenSource.Token); + await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, linkedTokenSource.Token).ConfigureAwait(false); _logger?.LogDebug("Message sent: {message}", message); } catch (Exception exception) @@ -226,7 +226,7 @@ private async Task ProcessWebSocketMessagesAsync(IWebSocket webSocket, Cancellat { // Read the next message - WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); + WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken).ConfigureAwait(false); if (result.MessageType == WebSocketMessageType.Text) { @@ -247,7 +247,7 @@ private async Task ProcessWebSocketMessagesAsync(IWebSocket webSocket, Cancellat { _logger?.LogInformation("Received close message from server"); _ = Task.Run(() => Disconnected?.Invoke(this, new DisconnectedArgs(result.CloseStatusDescription ?? string.Empty)), cancellationToken); - await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None); + await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None).ConfigureAwait(false); break; } } diff --git a/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index 94df987..8e2377c 100644 --- a/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -167,7 +167,7 @@ public void Start(CancellationToken? cancellationToken = null) try { // Accept the client - var client = await _tcpListener.AcceptTcpClientAsync(linkedTokenSource.Token); + var client = await _tcpListener.AcceptTcpClientAsync(linkedTokenSource.Token).ConfigureAwait(false); Logger?.LogDebug("Client connected from {endpoint}", client.ClientConnection!.RemoteEndPoint); @@ -215,7 +215,7 @@ public async Task ShutdownServer(CancellationToken? cancellationToken = null) { if (client.WebSocket != null && client.WebSocket.State == WebSocketState.Open) { - await client.WebSocket.CloseAsync(WebSocketCloseStatus.EndpointUnavailable, "Server is shutting down", linkedTokenSource.Token); + await client.WebSocket.CloseAsync(WebSocketCloseStatus.EndpointUnavailable, "Server is shutting down", linkedTokenSource.Token).ConfigureAwait(false); ActiveClients.TryRemove(client.Id, out _); client?.Dispose(); } @@ -251,7 +251,7 @@ public async Task SendMessageAsync(string clientId, string message, Cancellation { // Send the message var buffer = Encoding.UTF8.GetBytes(message); - await client.WebSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, linkedTokenSource.Token).ConfigureAwait(false); ; + await client.WebSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, linkedTokenSource.Token).ConfigureAwait(false); Logger?.LogDebug("Message sent: {message}.", message); } catch (Exception exception) @@ -299,23 +299,23 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT try { // Load the request context - await flow.LoadRequestContext(); + await flow.LoadRequestContext().ConfigureAwait(false); // Raise async client upgrade request received event - var eventArgs = await flow.RaiseUpgradeEventAsync(ClientUpgradeRequestReceivedAsync); + var eventArgs = await flow.RaiseUpgradeEventAsync(ClientUpgradeRequestReceivedAsync).ConfigureAwait(false); // Respond to the upgrade request if (eventArgs.Handle) { // Accept the WebSocket connection - await flow.AcceptWebSocketAsync(); + await flow.AcceptWebSocketAsync().ConfigureAwait(false); if (flow.TryAddClientToActiveUserList()) { Logger?.LogDebug("Connection upgraded, now listening on Client {clientId}", flow.Client.Id); AsyncEventRaiser.RaiseAsyncInNewTask(ClientConnected, this, new ClientConnectedArgs(flow.Client.Id), cancellationToken); // Start listening for messages - await ProcessWebSocketMessagesAsync(flow.Client, cancellationToken); + await ProcessWebSocketMessagesAsync(flow.Client, cancellationToken).ConfigureAwait(false); } else { @@ -326,7 +326,7 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT { // Reject the WebSocket connection Logger?.LogDebug("Client upgrade request rejected by ClientUpgradeRequestReceivedAsync event."); - await flow.RejectWebSocketAsync(eventArgs.ResponseContext); + await flow.RejectWebSocketAsync(eventArgs.ResponseContext).ConfigureAwait(false); } } catch (OperationCanceledException) @@ -335,7 +335,7 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT } catch (UserNotHandledException userNotHandledException) { - await flow.RejectWebSocketAsync(userNotHandledException.ResponseContext); + await flow.RejectWebSocketAsync(userNotHandledException.ResponseContext).ConfigureAwait(false); } catch (Exception exception) { @@ -376,7 +376,7 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C { cancellationToken.ThrowIfCancellationRequested(); // Read the next message - WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); + WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken).ConfigureAwait(false); if (result.MessageType == WebSocketMessageType.Text) { @@ -397,7 +397,7 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C { Logger?.LogInformation("Received close message from Client"); closeStatusDescription = result.CloseStatusDescription; - await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None); + await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None).ConfigureAwait(false); break; } } diff --git a/src/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs b/src/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs index a084e00..c2a0c91 100644 --- a/src/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs +++ b/src/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs @@ -36,13 +36,13 @@ internal static async Task RaiseAsync(AsyncEventHandler? // Post back to the captured context if it's not null syncContext.Post(async _ => { - await asyncHandler(sender, e, cancellationToken); + await asyncHandler(sender, e, cancellationToken).ConfigureAwait(false); }, null); } else { // Execute directly if there's no synchronization context - await asyncHandler(sender, e, cancellationToken); + await asyncHandler(sender, e, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/Jung.SimpleWebSocket/Wrappers/NetworkStreamWrapper.cs b/src/Jung.SimpleWebSocket/Wrappers/NetworkStreamWrapper.cs index 917a18a..13e5ab1 100644 --- a/src/Jung.SimpleWebSocket/Wrappers/NetworkStreamWrapper.cs +++ b/src/Jung.SimpleWebSocket/Wrappers/NetworkStreamWrapper.cs @@ -19,7 +19,7 @@ public void Dispose() public async ValueTask ReadAsync(byte[] buffer, CancellationToken cancellationToken) { - return await stream.ReadAsync(buffer, cancellationToken); + return await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); } public ValueTask WriteAsync(byte[] responseBytes, CancellationToken cancellationToken) diff --git a/src/Jung.SimpleWebSocket/Wrappers/TcpListenerWrapper.cs b/src/Jung.SimpleWebSocket/Wrappers/TcpListenerWrapper.cs index 331d328..a16eb0f 100644 --- a/src/Jung.SimpleWebSocket/Wrappers/TcpListenerWrapper.cs +++ b/src/Jung.SimpleWebSocket/Wrappers/TcpListenerWrapper.cs @@ -13,7 +13,7 @@ internal class TcpListenerWrapper(IPAddress localIpAddress, int port) : TcpListe public bool IsListening => Active; public new async Task AcceptTcpClientAsync(CancellationToken cancellationToken) { - var tcpClient = await WaitAndWrap(AcceptSocketAsync(cancellationToken)); + var tcpClient = await WaitAndWrap(AcceptSocketAsync(cancellationToken)).ConfigureAwait(false); static async ValueTask WaitAndWrap(ValueTask task) => new TcpClientWrapper(await task.ConfigureAwait(false)); diff --git a/src/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs b/src/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs index 5d9f87b..57ded7b 100644 --- a/src/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs +++ b/src/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs @@ -53,7 +53,7 @@ public async Task AwaitContextAsync(CancellationToken cancellationTo while (!readingStarted || _networkStream.DataAvailable) { readingStarted = true; - var bytesRead = await _networkStream.ReadAsync(buffer, cancellationToken); + var bytesRead = await _networkStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); sb.Append(Encoding.ASCII.GetString(buffer[..bytesRead])); } @@ -78,7 +78,7 @@ public async Task AcceptWebSocketAsync(WebContext request, WebContext response, response.Headers.Add("Upgrade", "websocket"); response.Headers.Add("Sec-WebSocket-Accept", secWebSocketAcceptString); response.StatusCode = HttpStatusCode.SwitchingProtocols; - await SendWebSocketResponseHeaders(response, cancellationToken); + await SendWebSocketResponseHeaders(response, cancellationToken).ConfigureAwait(false); _acceptedProtocol = subProtocol; } catch (WebSocketUpgradeException) @@ -99,7 +99,7 @@ private async Task SendWebSocketResponseHeaders(WebContext context, Cancellation CompleteHeaderSection(sb); byte[] responseBytes = Encoding.UTF8.GetBytes(sb.ToString()); - await _networkStream.WriteAsync(responseBytes, cancellationToken); + await _networkStream.WriteAsync(responseBytes, cancellationToken).ConfigureAwait(false); } private async Task SendWebSocketRejectResponse(WebContext context, CancellationToken cancellationToken) @@ -111,7 +111,7 @@ private async Task SendWebSocketRejectResponse(WebContext context, CancellationT AddBody(context, sb); byte[] responseBytes = Encoding.UTF8.GetBytes(sb.ToString()); - await _networkStream.WriteAsync(responseBytes, cancellationToken); + await _networkStream.WriteAsync(responseBytes, cancellationToken).ConfigureAwait(false); } private async Task SendWebSocketRequestHeaders(WebContext context, CancellationToken cancellationToken) @@ -123,7 +123,7 @@ private async Task SendWebSocketRequestHeaders(WebContext context, CancellationT CompleteHeaderSection(sb); byte[] responseBytes = Encoding.UTF8.GetBytes(sb.ToString()); - await _networkStream.WriteAsync(responseBytes, cancellationToken); + await _networkStream.WriteAsync(responseBytes, cancellationToken).ConfigureAwait(false); } private static void AddHeaders(WebContext response, StringBuilder sb) @@ -213,7 +213,7 @@ internal async Task SendUpgradeRequestAsync(WebContext requestContext, Cancellat requestContext.Headers.Add("Sec-WebSocket-Key", secWebSocketKey); requestContext.Headers.Add("Sec-WebSocket-Version", _supportedVersion); - await SendWebSocketRequestHeaders(requestContext, token); + await SendWebSocketRequestHeaders(requestContext, token).ConfigureAwait(false); } private static void ValidateRequestPath(string requestPath) @@ -302,6 +302,6 @@ internal async Task RejectWebSocketAsync(WebContext response, CancellationToken response.Headers.Add("Connection", "close"); response.Headers.Add("Content-Type", "text/plain"); response.Headers.Add("Content-Length", response.BodyContent.Length.ToString()); - await SendWebSocketRejectResponse(response, cancellationToken); + await SendWebSocketRejectResponse(response, cancellationToken).ConfigureAwait(false); } } \ No newline at end of file From a74496e2e35fbf065c6a7d425f070ccfafff2d87 Mon Sep 17 00:00:00 2001 From: Christoph Date: Thu, 9 Oct 2025 15:28:33 +0200 Subject: [PATCH 5/5] Clean up code comments and formatting --- src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 6 +++--- .../Tests/SendMessagesLoopTest.cs | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index 8e2377c..fd296a6 100644 --- a/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -99,13 +99,13 @@ public class SimpleWebSocketServer(SimpleWebSocketServerOptions options, ILogger /// /// A flag indicating whether the server is disposed. - /// 0 = false, 1 = trues + /// 0 = false, 1 = true /// private int _disposed; /// /// A flag indicating whether the server is disposing. - /// 0 = false, 1 = trues + /// 0 = false, 1 = true /// private int _disposing; @@ -222,7 +222,7 @@ public async Task ShutdownServer(CancellationToken? cancellationToken = null) } catch { - // Ignore the exception, because it's no the servers problem if a client does not close the connection + // Ignore the exception, because it's not the server's problem if a client does not close the connection } } diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs index e8d44d6..9bd26ba 100644 --- a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs @@ -56,7 +56,7 @@ private async Task SendRandomMessages(SimpleWebSocketClient client, Cancellation { try { - if(!client.IsConnected) + if (!client.IsConnected) { // If the client is not connected, exit the loop. _logger.LogWarning("Client is not connected. Stopping message sending loop."); @@ -81,14 +81,14 @@ private async Task SendRandomMessages(SimpleWebSocketClient client, Cancellation } } - private void InitializeClientEvents(SimpleWebSocketClient client) + private void InitializeClientEvents(SimpleWebSocketClient client) { client.Disconnected += Client_Disconnected; client.MessageReceived += Client_MessageReceived; client.BinaryMessageReceived += Client_BinaryMessageReceived; } - private void UnsubscribeEvents(SimpleWebSocketClient client) + private void UnsubscribeEvents(SimpleWebSocketClient client) { client.Disconnected -= Client_Disconnected; client.MessageReceived -= Client_MessageReceived; @@ -101,12 +101,12 @@ private void Client_BinaryMessageReceived(object sender, BinaryMessageReceivedAr _logger.LogInformation("Binary message received: {binaryMessage}", BitConverter.ToString(e.Message)); } - private void Client_MessageReceived(object sender, MessageReceivedArgs e) + private void Client_MessageReceived(object sender, MessageReceivedArgs e) { _logger.LogInformation("Message received: {message}", e.Message); } - private void Client_Disconnected(object sender, DisconnectedArgs e) + private void Client_Disconnected(object sender, DisconnectedArgs e) { _logger.LogInformation("Disconnected"); }