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/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/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/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..89e5383 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; @@ -23,7 +24,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 +65,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 +89,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; @@ -85,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); @@ -94,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; } @@ -108,19 +128,22 @@ 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 { - await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, closingStatusDescription, linkedTokenSource.Token); + _logger?.LogInformation("Disconnecting from Server"); + await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, closingStatusDescription, linkedTokenSource.Token).ConfigureAwait(false); } catch (Exception exception) { @@ -135,7 +158,6 @@ public async Task DisconnectAsync(string closingStatusDescription = "Closing", C } } } - _client?.Dispose(); } /// @@ -151,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); @@ -161,6 +183,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"); @@ -171,11 +195,12 @@ 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) { + _logger?.LogError(exception, "Error sending message"); throw new WebSocketClientException(message: "Error sending message", innerException: exception); } } @@ -194,45 +219,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).ConfigureAwait(false); - 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).ConfigureAwait(false); + 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..fd296a6 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,41 @@ 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 = true + /// + private int _disposed; + + /// + /// A flag indicating whether the server is disposing. + /// 0 = false, 1 = true + /// + private int _disposing; /// /// A cancellation token source to cancel the server. @@ -96,7 +125,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 +136,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 +145,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(); @@ -133,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); @@ -154,36 +188,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 + { + if (client.WebSocket != null && client.WebSocket.State == WebSocketState.Open) + { + await client.WebSocket.CloseAsync(WebSocketCloseStatus.EndpointUnavailable, "Server is shutting down", linkedTokenSource.Token).ConfigureAwait(false); + ActiveClients.TryRemove(client.Id, out _); + client?.Dispose(); + } + } + catch { - await client.WebSocket.CloseAsync(WebSocketCloseStatus.EndpointUnavailable, "Server is shutting down", linkedTokenSource.Token); - ActiveClients.TryRemove(client.Id, out _); - client?.Dispose(); + // Ignore the exception, because it's not the server's 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 +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); + 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 +265,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 +274,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"); @@ -239,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 { @@ -266,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) @@ -275,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) { @@ -285,7 +345,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 +367,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).ConfigureAwait(false); + + 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).ConfigureAwait(false); + 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 +415,28 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C /// public void Dispose() { - _cancellationTokenSource?.Cancel(); - _tcpListener?.Dispose(); - _tcpListener = null; - GC.SuppressFinalize(this); + if (Interlocked.Exchange(ref _disposing, 1) == 1) + { + return; + } + + try + { + ShutdownServer().GetAwaiter().GetResult(); + _cancellationTokenSource?.Cancel(); + _tcpListener?.Dispose(); + _tcpListener = null; + GC.SuppressFinalize(this); + } + finally + { + Interlocked.Exchange(ref _disposed, 1); + } + } + + private void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(Disposed, this); } } } \ No newline at end of file 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 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..f1ca8ca --- /dev/null +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Jung.SimpleWebSocket.IntegrationTests.csproj @@ -0,0 +1,21 @@ + + + + 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..ee586bd --- /dev/null +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/ProcedureProvider.cs @@ -0,0 +1,84 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Jung.SimpleWebSocket.IntegrationTests.Tests; +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..ec0b4a8 --- /dev/null +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Program.cs @@ -0,0 +1,105 @@ +// 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 {procedureType} could not be loaded.", procedure.ProcedureType.FullName); + } + else + { + logger.LogInformation("Running test: {procedureName} ({procedureDescription})", 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) + .WriteTo.Console(Serilog.Events.LogEventLevel.Information, outputTemplate: "{Level:u3}: {Message:lj}{NewLine}{Exception}") + .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..bbc3e60 --- /dev/null +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/BaseTest.cs @@ -0,0 +1,14 @@ +// 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 +{ + 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..129ae9e --- /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(ILogger logger, SimpleWebSocketServer simpleWebSocketServer) : 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) + { + _logger.LogInformation("Client connected: {ClientId}", e.ClientId); + } + + private void SimpleWebSocketServer_ClientDisconnected(object? sender, ClientDisconnectedArgs e) + { + _logger.LogInformation("Client disconnected: {ClientId}", e.ClientId); + } + + private void SimpleWebSocketServer_MessageReceived(object? sender, ClientMessageReceivedArgs e) + { + _logger.LogInformation("Message received from {ClientId}: {Message}", e.ClientId, e.Message); + } + + private void SimpleWebSocketServer_BinaryMessageReceived(object? sender, ClientBinaryMessageReceivedArgs e) + { + _logger.LogInformation("Binary message received from {ClientId}: {messages}", e.ClientId, string.Join(' ', e.Message)); + } + + private Task ClientUpgradeRequestReceived(object sender, ClientUpgradeRequestReceivedArgs e, CancellationToken cancellationToken) + { + _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 new file mode 100644 index 0000000..9bd26ba --- /dev/null +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs @@ -0,0 +1,114 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Jung.SimpleWebSocket.Exceptions; +using Jung.SimpleWebSocket.Models.EventArguments; +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); + + 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 (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; + } + finally + { + UnsubscribeEvents(client); + } + } + + private async Task SendRandomMessages(SimpleWebSocketClient client, CancellationToken cancellationToken) + { + Random random = new(); + int messageCount = 1; + while (!cancellationToken.IsCancellationRequested) + { + 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); + _logger.LogInformation("Sent: {message}", 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) + { + _logger.LogError(exception, "Error while sending the message."); + } + break; + } + } + } + + private void InitializeClientEvents(SimpleWebSocketClient client) + { + client.Disconnected += Client_Disconnected; + client.MessageReceived += Client_MessageReceived; + client.BinaryMessageReceived += Client_BinaryMessageReceived; + } + + private void UnsubscribeEvents(SimpleWebSocketClient client) + { + client.Disconnected -= Client_Disconnected; + client.MessageReceived -= Client_MessageReceived; + client.BinaryMessageReceived -= Client_BinaryMessageReceived; + } + + + 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"); + } + } +} 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 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(); };