From ddea0ed3fa4274deaecd03b32796d8c51c207353 Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Sat, 21 Sep 2024 12:11:49 +0200 Subject: [PATCH 01/27] Add async event for handling a clients upgrade request Improve WebConext to handle the StatusCode Improve WebSocketUpgradeHandler to handle the status code properly --- .../Contracts/IWebSocketServer.cs | 6 ++ .../Delegates/AsyncEventHandler.cs | 14 ++++ .../ClientUpgradeRequestReceivedArgs.cs | 27 +++++++ Jung.SimpleWebSocket/Models/WebContext.cs | 71 +++++++++++++++++-- .../Models/WebSocketServerClient.cs | 8 +++ Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 17 ++++- .../Utility/AsyncEventRaiser.cs | 51 +++++++++++++ .../Wrappers/WebSocketUpgradeHandler.cs | 22 +++++- .../SimpleWebSocketTest.cs | 37 +++++++++- 9 files changed, 242 insertions(+), 11 deletions(-) create mode 100644 Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs create mode 100644 Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs create mode 100644 Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs diff --git a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs b/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs index 4e9e79b..25e5918 100644 --- a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs +++ b/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs @@ -4,6 +4,7 @@ using Jung.SimpleWebSocket.Delegates; using Jung.SimpleWebSocket.Models; using Jung.SimpleWebSocket.Models.EventArguments; +using Jung.SimpleWebSocket.Utility; using System.Net; namespace Jung.SimpleWebSocket.Contracts; @@ -58,6 +59,11 @@ public interface IWebSocketServer : IDisposable /// event ClientBinaryMessageReceivedEventHandler? BinaryMessageReceived; + /// + /// Async Event that is raised when a client upgrade request is received. + /// + event AsyncEventHandler? ClientUpgradeRequestReceivedAsync; + /// /// Gets a client by its id. /// diff --git a/Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs b/Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs new file mode 100644 index 0000000..ba551fe --- /dev/null +++ b/Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs @@ -0,0 +1,14 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +namespace Jung.SimpleWebSocket.Delegates; + +/// +/// Represents an asynchronous event handler. +/// +/// The type of the event arguments. +/// The sender of the event. +/// The event arguments. +/// The cancellation token. +/// A task that represents the asynchronous operation. +public delegate Task AsyncEventHandler(object sender, TEventArgs e, CancellationToken cancellationToken) where TEventArgs : class; \ No newline at end of file diff --git a/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs b/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs new file mode 100644 index 0000000..0f238b0 --- /dev/null +++ b/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs @@ -0,0 +1,27 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Microsoft.Extensions.Logging; + +namespace Jung.SimpleWebSocket.Models.EventArguments; + +/// +/// Represents the arguments of the event when a upgrade request is received from a client. +/// +/// The client that is sending the upgrade request. +/// The context of the request. +/// The current Logger. +public record ClientUpgradeRequestReceivedArgs(WebSocketServerClient Client, WebContext WebContext, ILogger? Logger) +{ + private WebContext? _responseContext; + + /// + /// Gets or sets a value indicating whether the upgrade request should be handled. + /// + public bool Handle { get; set; } = true; + + /// + /// The context that is being use to response to the client. + /// + public WebContext ResponseContext { get => _responseContext ??= new WebContext(); } +} diff --git a/Jung.SimpleWebSocket/Models/WebContext.cs b/Jung.SimpleWebSocket/Models/WebContext.cs index c2603f6..cf73bde 100644 --- a/Jung.SimpleWebSocket/Models/WebContext.cs +++ b/Jung.SimpleWebSocket/Models/WebContext.cs @@ -3,7 +3,8 @@ using Jung.SimpleWebSocket.Exceptions; using System.Collections.Specialized; -using System.Net.Http.Headers; +using System.Net; +using System.Text.RegularExpressions; namespace Jung.SimpleWebSocket.Models; @@ -15,7 +16,7 @@ namespace Jung.SimpleWebSocket.Models; /// Initializes a new instance of the class. /// /// The content of the web request. -internal class WebContext(string? content = null) +public partial class WebContext(string? content = null) { /// @@ -43,6 +44,17 @@ internal class WebContext(string? content = null) /// private string? _requestPath = null; + /// + /// The status code of the context. + /// + private HttpStatusCode? _statusCode; + + + // A Regular Expression to split a string by uppercase letters. + [GeneratedRegex(@"(? /// Gets the headers of the web request. /// @@ -140,6 +152,33 @@ private set } } + /// + /// Gets or sets the status code of the context + /// + public HttpStatusCode StatusCode + { + get + { + if (_statusCode == null) + { + var parts = StatusLine.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (parts.Length >= 2) + { + _statusCode = (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), parts[1]); + } + else + { + throw new WebSocketUpgradeException("Status code is missing"); + } + } + return _statusCode.Value; + } + set + { + _statusCode = value; + } + } + /// /// Parses the headers of the web request. /// @@ -185,12 +224,12 @@ internal static WebContext CreateRequest(string hostName, int port, string reque /// Checks if the web request contains a specific header with a specific value. /// /// The name of the header. - /// The value of the header. + /// The value of the header. If null, only the header name is checked. /// true if the web request contains the specified header with the specified value; otherwise, false. - internal bool ContainsHeader(string name, string value) + internal bool ContainsHeader(string name, string? value = null) { string? headerValue = Headers[name]; - return headerValue != null && headerValue.Contains(value, StringComparison.OrdinalIgnoreCase); + return headerValue != null && (value == null || headerValue.Contains(value, StringComparison.OrdinalIgnoreCase)); } /// @@ -300,6 +339,28 @@ public string RequestLine } } + /// + /// Gets a value indicating whether the Content is empty. + /// + public bool IsEmpty => string.IsNullOrWhiteSpace(_content); + + /// + /// Gets the status description of the web request. + /// + public string StatusDescription => GetStatusDescription(StatusCode); + + + /// + /// Gets the status description for the given status code. + /// + /// The status code. + /// A string containing the status description. + public static string GetStatusDescription(HttpStatusCode statusCode) + { + var enumName = Enum.GetName(statusCode) ?? throw new WebSocketUpgradeException("Status code is not a valid HttpStatusCode"); + return string.Join(" ", _splitByUppercaseRegex.Split(enumName)); + } + /// /// Gets the content lines of the web request. /// diff --git a/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs b/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs index d737b17..c138a3c 100644 --- a/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs +++ b/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs @@ -16,6 +16,14 @@ public class WebSocketServerClient : IDisposable /// public string Id { get; private set; } = Guid.NewGuid().ToString(); + /// + /// Gets the properties of the WebSocket client. + /// + /// + /// The properties can be used to store additional information about the client. + /// + public Dictionary Properties { get; } = []; + /// /// Gets the connection of the WebSocket client. /// diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index 943ca4a..ef56a5e 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -6,6 +6,7 @@ using Jung.SimpleWebSocket.Exceptions; using Jung.SimpleWebSocket.Models; using Jung.SimpleWebSocket.Models.EventArguments; +using Jung.SimpleWebSocket.Utility; using Jung.SimpleWebSocket.Wrappers; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; @@ -34,6 +35,9 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// public event ClientBinaryMessageReceivedEventHandler? BinaryMessageReceived; + /// + public event AsyncEventHandler? ClientUpgradeRequestReceivedAsync; + /// /// A dictionary of active clients. /// @@ -227,7 +231,18 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT using var stream = client.ClientConnection.GetStream(); var socketWrapper = new WebSocketUpgradeHandler(stream); var request = await socketWrapper.AwaitContextAsync(cancellationToken); - await socketWrapper.AcceptWebSocketAsync(request, cancellationToken); + + // raise async client upgrade request received event + var eventArgs = new ClientUpgradeRequestReceivedArgs(client, request, _logger); + await AsyncEventRaiser.RaiseAsync(ClientUpgradeRequestReceivedAsync, this, eventArgs, cancellationToken); + if (!eventArgs.Handle) + { + _logger?.LogDebug("Client upgrade request rejected by ClientUpgradeRequestReceivedAsync event."); + // send rejection response + return; + } + + await socketWrapper.AcceptWebSocketAsync(request, eventArgs.WebContext, cancellationToken); client.UpdateWebSocket(socketWrapper.CreateWebSocket(isServer: true)); _ = Task.Run(() => ClientConnected?.Invoke(this, new ClientConnectedArgs(client.Id)), cancellationToken); diff --git a/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs b/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs new file mode 100644 index 0000000..b81a851 --- /dev/null +++ b/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs @@ -0,0 +1,51 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Jung.SimpleWebSocket.Delegates; + +namespace Jung.SimpleWebSocket.Utility +{ + /// + /// Helper class to raise an async event. + /// + internal class AsyncEventRaiser + { + /// + /// Helper method to raise an async event. + /// + /// The type of the event arguments. + /// The async event handler + /// The sender of the event. + /// The event arguments. + /// The cancellation token. + /// A task that represents the asynchronous operation. + internal static async Task RaiseAsync(AsyncEventHandler? asyncEvent, object sender, TEventArgs e, CancellationToken cancellationToken) where TEventArgs : class + { + var syncContext = SynchronizationContext.Current; // Capture the current synchronization context + + if (asyncEvent != null) + { + var invocationList = asyncEvent.GetInvocationList(); + + foreach (var handler in invocationList) + { + var asyncHandler = (AsyncEventHandler)handler; + + if (syncContext != null) + { + // Post back to the captured context if it's not null + syncContext.Post(async _ => + { + await asyncHandler(sender, e, cancellationToken); + }, null); + } + else + { + // Execute directly if there's no synchronization context + await asyncHandler(sender, e, cancellationToken); + } + } + } + } + } +} diff --git a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs index 6da2ffc..2823c4e 100644 --- a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs +++ b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs @@ -9,6 +9,7 @@ using Jung.SimpleWebSocket.Exceptions; using Jung.SimpleWebSocket.Helpers; using Jung.SimpleWebSocket.Models; +using System.Net; using System.Net.Sockets; using System.Net.WebSockets; using System.Security.Cryptography; @@ -63,14 +64,23 @@ public async Task AwaitContextAsync(CancellationToken cancellationTo public async Task AcceptWebSocketAsync(WebContext request, CancellationToken cancellationToken) { - await AcceptWebSocketAsync(request, null, cancellationToken); + await AcceptWebSocketAsync(request, new WebContext(), null, cancellationToken); + } + + public async Task AcceptWebSocketAsync(WebContext request, WebContext response, CancellationToken cancellationToken) + { + await AcceptWebSocketAsync(request, response, null, cancellationToken); } public async Task AcceptWebSocketAsync(WebContext request, string? subProtocol, CancellationToken cancellationToken) + { + await AcceptWebSocketAsync(request, new WebContext(), subProtocol, cancellationToken); + } + + public async Task AcceptWebSocketAsync(WebContext request, WebContext response, string? subProtocol, CancellationToken cancellationToken) { try { - var response = new WebContext(); ValidateWebSocketHeaders(request); var protocol = request.GetConcatenatedHeaders("Sec-WebSocket-Protocol"); if (ProcessWebSocketProtocolHeader(protocol, subProtocol, out var acceptProtocol)) @@ -82,6 +92,7 @@ public async Task AcceptWebSocketAsync(WebContext request, string? subProtocol, response.Headers.Add("Connection", "upgrade"); response.Headers.Add("Upgrade", "websocket"); response.Headers.Add("Sec-WebSocket-Accept", secWebSocketAcceptString); + response.StatusCode = HttpStatusCode.SwitchingProtocols; await SendWebSocketResponseHeaders(response, cancellationToken); _acceptedProtocol = subProtocol; } @@ -98,7 +109,7 @@ public async Task AcceptWebSocketAsync(WebContext request, string? subProtocol, private async Task SendWebSocketResponseHeaders(WebContext context, CancellationToken cancellationToken) { var sb = new StringBuilder( - $"HTTP/1.1 101 Switching Protocols\r\n"); + $"HTTP/1.1 {(int)context.StatusCode} {context.StatusDescription}\r\n"); AddHeaders(context, sb); FinishMessage(sb); @@ -219,6 +230,11 @@ private static void ValidateRequestPath(string requestPath) internal static void ValidateUpgradeResponse(WebContext response, WebContext requestContext) { + if (response.IsEmpty) + { + throw new WebSocketUpgradeException("Empty response received"); + } + // Check if the response contains '101 Switching Protocols' if (!response.StatusLine.Contains("101 Switching Protocols")) { diff --git a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs index 0b058ec..1d56d99 100644 --- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs +++ b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs @@ -17,7 +17,7 @@ namespace Jung.SimpleWebSocketTest [TestFixture] public class SimpleWebSocketTest { - private List _logMessages = []; + private readonly List _logMessages = []; private Mock> _serverLogger; private Mock> _clientLogger; @@ -60,7 +60,7 @@ private void SetUpLogger(Mock> mock, string loggerName) var formatter = invocation.Arguments[4]; var invokeMethod = formatter.GetType().GetMethod("Invoke"); - var logMessage = invokeMethod!.Invoke(formatter, new[] { state, exception }); + var logMessage = invokeMethod!.Invoke(formatter, [state, exception]); _logMessages.Add($"{loggerName}({logLevel}): {logMessage}"); })); } @@ -106,6 +106,27 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() messageResetEvent.Set(); }; + server.ClientUpgradeRequestReceivedAsync += async (sender, args, cancellationToken) => + { + var IpAddress = (args.Client.RemoteEndPoint as IPEndPoint)?.Address.ToString(); + if (IpAddress == null) + { + args.Handle = false; + return; + } + + // Check the IpAddress against the database + var isWhitelistedEndPoint = await DbContext_IpAddresses_Contains(IpAddress, cancellationToken); + if (!isWhitelistedEndPoint) + { + args.ResponseContext.StatusCode = HttpStatusCode.Forbidden; + // not yet implemented + // args.ResponseContext.Body = ""; + args.Handle = false; + } + args.Client.Properties["test"] = "test"; + }; + // Act server.Start(); await client.ConnectAsync(); @@ -129,6 +150,18 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() }); } + /// + /// Fake Async method to simulate a database call to check if the IP address is in the database. + /// + /// The IP address to check. + /// The cancellation token. + /// A task that represents the asynchronous operation. The task result contains a value indicating whether the IP address is in the database. + private static async Task DbContext_IpAddresses_Contains(string ipAddress, CancellationToken cancellationToken) + { + await Task.Delay(100, cancellationToken); + return ipAddress == IPAddress.Loopback.ToString(); + } + [Test] [Platform("Windows7,Windows8,Windows8.1,Windows10", Reason = "This test establishes a TCP client-server connection using SimpleWebSocket, which relies on specific networking features and behaviors that are only available and consistent on Windows platforms. Running this test on non-Windows platforms could lead to inconsistent results or failures due to differences in networking stack implementations.")] public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld2() From 1ead9c3fb6f84db731068bea81caef194ba17d7b Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 28 Sep 2024 21:18:01 +0200 Subject: [PATCH 02/27] Add user id header handling Add BodyContent --- .../Contracts/IWebSocketClient.cs | 5 ++ Jung.SimpleWebSocket/Models/WebContext.cs | 58 +++++++++++++- .../Models/WebSocketServerClient.cs | 22 +++-- Jung.SimpleWebSocket/SimpleWebSocketClient.cs | 13 ++- Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 80 +++++++++++++++---- .../Wrappers/WebSocketUpgradeHandler.cs | 52 ++++++++++-- .../SimpleWebSocketTest.cs | 25 +++++- .../WebSocketUpgradeHandlerTests.cs | 4 +- 8 files changed, 216 insertions(+), 43 deletions(-) diff --git a/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs b/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs index 1a73240..5bad1b4 100644 --- a/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs +++ b/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs @@ -30,6 +30,11 @@ public interface IWebSocketClient : IDisposable /// bool IsConnected { get; } + /// + /// The user id of the client. If not set, the server did not sent a user id at websocket upgrade. + /// + string? UserId { get; } + /// /// Event that is raised when a message is received from a client. /// diff --git a/Jung.SimpleWebSocket/Models/WebContext.cs b/Jung.SimpleWebSocket/Models/WebContext.cs index c2603f6..fa97341 100644 --- a/Jung.SimpleWebSocket/Models/WebContext.cs +++ b/Jung.SimpleWebSocket/Models/WebContext.cs @@ -3,7 +3,6 @@ using Jung.SimpleWebSocket.Exceptions; using System.Collections.Specialized; -using System.Net.Http.Headers; namespace Jung.SimpleWebSocket.Models; @@ -43,6 +42,11 @@ internal class WebContext(string? content = null) /// private string? _requestPath = null; + /// + /// The body content. + /// + private string? _bodyContent = null; + /// /// Gets the headers of the web request. /// @@ -55,6 +59,27 @@ public NameValueCollection Headers } } + public string BodyContent + { + get + { + if (_bodyContent == null) + { + var parts = _content.Split("\r\n\r\n", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + _bodyContent = parts.Length > 1 ? parts[1] : string.Empty; + } + return _bodyContent; + } + set + { + if (BodyContent != string.Empty) + { + throw new WebSocketUpgradeException("Body content is already set"); + } + _bodyContent = value; + } + } + /// /// Gets the host name of the web request. /// @@ -170,15 +195,23 @@ private NameValueCollection ParseHeaders() /// The host name of the web request. /// The port of the web request. /// The request path of the web request. + /// The user id of the web request. /// The created web request context. - internal static WebContext CreateRequest(string hostName, int port, string requestPath) + internal static WebContext CreateRequest(string hostName, int port, string requestPath, string? userId = null) { - return new WebContext() + var context = new WebContext() { HostName = hostName, Port = port, - RequestPath = requestPath + RequestPath = requestPath, }; + + if (userId != null) + { + context.Headers.Add("x-user-id", userId); + } + + return context; } /// @@ -300,6 +333,23 @@ public string RequestLine } } + /// + /// Gets the user id of the web request. + /// + public string UserId + { + get + { + var userId = Headers["x-user-id"] ?? throw new WebSocketUpgradeException("UserId header is missing"); + return userId; + } + } + + /// + /// Gets a value indicating whether the web request contains a user id. + /// + public bool ContainsUserId => Headers["x-user-id"] != null; + /// /// Gets the content lines of the web request. /// diff --git a/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs b/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs index d737b17..a30b924 100644 --- a/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs +++ b/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs @@ -19,7 +19,7 @@ public class WebSocketServerClient : IDisposable /// /// Gets the connection of the WebSocket client. /// - internal ITcpClient ClientConnection { get; private set; } + internal ITcpClient? ClientConnection { get; private set; } /// /// Gets the timestamp when the WebSocket client was last seen. @@ -34,7 +34,7 @@ public class WebSocketServerClient : IDisposable /// /// Gets the remote endpoint of the WebSocket client. /// - public EndPoint? RemoteEndPoint => ClientConnection.RemoteEndPoint; + public EndPoint? RemoteEndPoint => ClientConnection?.RemoteEndPoint; /// /// Gets or sets the WebSocket of the client. @@ -62,6 +62,16 @@ internal void UpdateClient(ITcpClient client) ClientConnection = client; } + /// + /// Updates the client with a new WebSocket. + /// + /// The websocket that the client should use. + internal void UseWebSocket(IWebSocket? webSocket) + { + ArgumentNullException.ThrowIfNull(webSocket); + WebSocket = webSocket; + } + /// /// Updates the WebSocket client with a new identifier. /// @@ -82,17 +92,13 @@ internal void UpdateId(string id) Id = id; } - internal void UpdateWebSocket(IWebSocket? webSocket) - { - ArgumentNullException.ThrowIfNull(webSocket); - WebSocket = webSocket; - } - /// public void Dispose() { WebSocket?.Dispose(); + WebSocket = null; ClientConnection?.Dispose(); + ClientConnection = null; GC.SuppressFinalize(this); } } diff --git a/Jung.SimpleWebSocket/SimpleWebSocketClient.cs b/Jung.SimpleWebSocket/SimpleWebSocketClient.cs index b690ab7..7b362cf 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketClient.cs +++ b/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, string? userId = null, ILogger? logger = null) : IWebSocketClient, IDisposable { /// public string HostName { get; } = hostName; @@ -32,6 +32,9 @@ public class SimpleWebSocketClient(string hostName, int port, string requestPath /// public string RequestPath { get; } = requestPath; + /// + public string? UserId { get; private set; } + /// public bool IsConnected => _client?.Connected ?? false; @@ -150,10 +153,16 @@ private async Task HandleWebSocketInitiation(TcpClientWrapper client, Cancellati _stream = client.GetStream(); var socketWrapper = new WebSocketUpgradeHandler(_stream); - var requestContext = WebContext.CreateRequest(HostName, Port, RequestPath); + var requestContext = WebContext.CreateRequest(HostName, Port, RequestPath, userId); await socketWrapper.SendUpgradeRequestAsync(requestContext, cancellationToken); var response = await socketWrapper.AwaitContextAsync(cancellationToken); WebSocketUpgradeHandler.ValidateUpgradeResponse(response, requestContext); + + if (response.ContainsUserId) + { + UserId = response.UserId; + } + _webSocket = socketWrapper.CreateWebSocket(isServer: false); } diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index 943ca4a..e8f3f15 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -129,13 +129,7 @@ public void Start(CancellationToken? cancellationToken = null) // Accept the client var client = await _server.AcceptTcpClientAsync(linkedTokenSource.Token); - _logger?.LogDebug("Client connected from {endpoint}", client.ClientConnection.RemoteEndPoint); - var clientAdded = ActiveClients.TryAdd(client.Id, client); - if (!clientAdded) - { - _logger?.LogError("Error while adding client to active clients, rejecting client."); - continue; - } + _logger?.LogDebug("Client connected from {endpoint}", client.ClientConnection!.RemoteEndPoint); _ = HandleClientAsync(client, linkedTokenSource.Token); } @@ -221,20 +215,64 @@ public WebSocketServerClient GetClientById(string clientId) /// A asynchronous task private async Task HandleClientAsync(WebSocketServerClient client, CancellationToken cancellationToken) { + bool clientAdded = false; try { // Upgrade the connection to a WebSocket - using var stream = client.ClientConnection.GetStream(); + using var stream = client.ClientConnection!.GetStream(); var socketWrapper = new WebSocketUpgradeHandler(stream); var request = await socketWrapper.AwaitContextAsync(cancellationToken); - await socketWrapper.AcceptWebSocketAsync(request, cancellationToken); - client.UpdateWebSocket(socketWrapper.CreateWebSocket(isServer: true)); - _ = Task.Run(() => ClientConnected?.Invoke(this, new ClientConnectedArgs(client.Id)), cancellationToken); + // Check if the request contains a user id + if (request.ContainsUserId) + { + _logger?.LogDebug("User id found in request: {userId}", request.UserId); + // Check if the client is an existing passive client + var clientExists = PassiveClients.ContainsKey(request.UserId); + if (clientExists) + { + _logger?.LogDebug("Passive client found for user id {userId} - reactivating user.", request.UserId); + + // Use the existing client + // Update the client with the new connection + // Remove the client from the passive clients + var passiveClient = PassiveClients[request.UserId]; + passiveClient.UpdateClient(client.ClientConnection); + client = passiveClient; + PassiveClients.TryRemove(request.UserId, out _); + } + else + { + // No passive client found, checking for active clients with the same id + if (ActiveClients.ContainsKey(request.UserId)) + { + _logger?.LogDebug("Active client found for user id {userId} - rejecting connection.", request.UserId); + // Reject the connection + await socketWrapper.RejectWebSocketAsync(cancellationToken); + return; + } + else + { + // Update the client with the new id + client.UpdateId(request.UserId); + } + } + } + + await socketWrapper.AcceptWebSocketAsync(request, client.Id, cancellationToken); + + // Update the client with the new WebSocket + client.UseWebSocket(socketWrapper.CreateWebSocket(isServer: true)); + + clientAdded = ActiveClients.TryAdd(client.Id, client); + if (clientAdded) + { + _ = Task.Run(() => ClientConnected?.Invoke(this, new ClientConnectedArgs(client.Id)), cancellationToken); + // Start listening for messages + _logger?.LogDebug("Connection upgraded, now listening on client {clientId}", client.Id); + await ProcessWebSocketMessagesAsync(client, cancellationToken); + } - // Start listening for messages - _logger?.LogDebug("Connection upgraded, now listening on client {clientId}", client.Id); - await ProcessWebSocketMessagesAsync(client, cancellationToken); } catch (OperationCanceledException) { @@ -246,14 +284,22 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT } finally { - if (!_serverShuttingDown) + // 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 (clientAdded && !_serverShuttingDown) { - ActiveClients.TryRemove(client.Id, out _); - client?.Dispose(); + HandleDisconnectedClient(client); } } } + private void HandleDisconnectedClient(WebSocketServerClient client) + { + ActiveClients.TryRemove(client.Id, out _); + client.Dispose(); + PassiveClients.TryAdd(client.Id, client); + } + /// /// Processes the WebSocket messages. /// diff --git a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs index 6da2ffc..3b8cd9f 100644 --- a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs +++ b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs @@ -21,6 +21,7 @@ internal partial class WebSocketUpgradeHandler { private const string _supportedVersion = "13"; private const string _webSocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + private const string _userIdHeaderName = "x-user-id"; private string? _acceptedProtocol; private readonly INetworkStream _networkStream; @@ -61,12 +62,12 @@ public async Task AwaitContextAsync(CancellationToken cancellationTo return context; } - public async Task AcceptWebSocketAsync(WebContext request, CancellationToken cancellationToken) + public async Task AcceptWebSocketAsync(WebContext request, string userId, CancellationToken cancellationToken) { - await AcceptWebSocketAsync(request, null, cancellationToken); + await AcceptWebSocketAsync(request, userId, null, cancellationToken); } - public async Task AcceptWebSocketAsync(WebContext request, string? subProtocol, CancellationToken cancellationToken) + public async Task AcceptWebSocketAsync(WebContext request, string userId, string? subProtocol, CancellationToken cancellationToken) { try { @@ -82,6 +83,7 @@ public async Task AcceptWebSocketAsync(WebContext request, string? subProtocol, response.Headers.Add("Connection", "upgrade"); response.Headers.Add("Upgrade", "websocket"); response.Headers.Add("Sec-WebSocket-Accept", secWebSocketAcceptString); + response.Headers.Add(_userIdHeaderName, userId); await SendWebSocketResponseHeaders(response, cancellationToken); _acceptedProtocol = subProtocol; } @@ -100,7 +102,19 @@ private async Task SendWebSocketResponseHeaders(WebContext context, Cancellation var sb = new StringBuilder( $"HTTP/1.1 101 Switching Protocols\r\n"); AddHeaders(context, sb); - FinishMessage(sb); + CompleteHeaderSection(sb); + + byte[] responseBytes = Encoding.UTF8.GetBytes(sb.ToString()); + await _networkStream.WriteAsync(responseBytes, cancellationToken); + } + + private async Task SendWebSocketRejectResponse(WebContext context, CancellationToken cancellationToken) + { + var sb = new StringBuilder( + $"HTTP/1.1 409 Conflict\r\n"); + AddHeaders(context, sb); + CompleteHeaderSection(sb); + AddBody(context, sb); byte[] responseBytes = Encoding.UTF8.GetBytes(sb.ToString()); await _networkStream.WriteAsync(responseBytes, cancellationToken); @@ -112,7 +126,7 @@ private async Task SendWebSocketRequestHeaders(WebContext context, CancellationT $"GET {context.RequestPath} HTTP/1.1\r\n" + $"Host: {context.HostName}:{context.Port}\r\n"); AddHeaders(context, sb); - FinishMessage(sb); + CompleteHeaderSection(sb); byte[] responseBytes = Encoding.UTF8.GetBytes(sb.ToString()); await _networkStream.WriteAsync(responseBytes, cancellationToken); @@ -126,11 +140,16 @@ private static void AddHeaders(WebContext response, StringBuilder sb) } } - private static void FinishMessage(StringBuilder sb) + private static void CompleteHeaderSection(StringBuilder sb) { sb.Append("\r\n"); } + private void AddBody(WebContext context, StringBuilder sb) + { + sb.Append(context.BodyContent); + } + private static void ValidateWebSocketHeaders(WebContext context) { if (!context.IsWebSocketRequest) @@ -222,12 +241,16 @@ internal static void ValidateUpgradeResponse(WebContext response, WebContext req // Check if the response contains '101 Switching Protocols' if (!response.StatusLine.Contains("101 Switching Protocols")) { + if (!string.IsNullOrEmpty(response.BodyContent)) + { + throw new WebSocketUpgradeException($"Connection not upgraded. The server returned: {response.BodyContent}"); + } throw new WebSocketUpgradeException("Invalid status code, expected '101 Switching Protocols'."); } - // Check for required headers 'Upgrade: websocket' and 'Connection: Upgrade' + // Check for required headers 'Upgrade: websocket' and 'Connection: upgrade' if (!response.ContainsHeader("Upgrade", "websocket") || - !response.ContainsHeader("Connection", "Upgrade")) + !response.ContainsHeader("Connection", "upgrade")) { throw new WebSocketUpgradeException("Invalid 'Upgrade' or 'Connection' header."); } @@ -274,4 +297,17 @@ internal IWebSocket CreateWebSocket(bool isServer, TimeSpan? keepAliveInterval = keepAliveInterval ??= TimeSpan.FromSeconds(30); return _websocketHelper.CreateFromStream(_networkStream.Stream, isServer, _acceptedProtocol, keepAliveInterval.Value); } + + internal async Task RejectWebSocketAsync(CancellationToken cancellationToken) + { + var response = new WebContext + { + BodyContent = "User ID already connected" + }; + + 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); + } } \ No newline at end of file diff --git a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs index 0b058ec..bfad460 100644 --- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs +++ b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs @@ -71,13 +71,14 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() { // Arrange using var server = new SimpleWebSocketServer(IPAddress.Any, 8010, _serverLogger.Object); - using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", _clientLogger.Object); + using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", logger: _clientLogger.Object); const string Message = "Hello World"; const string ClosingStatusDescription = "closing status test description"; string receivedMessage = string.Empty; string receivedClosingDescription = string.Empty; + string exceptionMessage = string.Empty; var messageResetEvent = new ManualResetEvent(false); var disconnectResetEvent = new ManualResetEvent(false); @@ -118,6 +119,25 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() await client.DisconnectAsync(ClosingStatusDescription); WaitForManualResetEventOrThrow(disconnectResetEvent, 100000); + var client2 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", client.UserId, logger: _clientLogger.Object); + await client2.ConnectAsync(); + + await Task.Delay(1000); + try + { + var client3 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", client.UserId, logger: _clientLogger.Object); + await client3.ConnectAsync(); + } + catch (Exception exception) + { + exceptionMessage = exception.InnerException!.Message; + } + + + await client2.SendMessageAsync("Hello World"); + await Task.Delay(1000); + + await server.ShutdownServer(CancellationToken.None); _logMessages.ForEach(m => Trace.WriteLine(m)); @@ -126,6 +146,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() { Assert.That(receivedMessage, Is.EqualTo(Message)); Assert.That(receivedClosingDescription, Is.EqualTo(ClosingStatusDescription)); + Assert.That(exceptionMessage, Does.Contain("User ID already connected")); }); } @@ -135,7 +156,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld2() { // Arrange using var server = new SimpleWebSocketServer(IPAddress.Any, 8010, _serverLogger.Object); - using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", _clientLogger.Object); + using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", logger: _clientLogger.Object); const string Message = "Hello World"; diff --git a/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs b/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs index f8ba27e..254ddeb 100644 --- a/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs +++ b/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs @@ -64,7 +64,7 @@ public async Task AcceptWebSocketAsync_ShouldSendUpgradeResponse(string hostname _mockNetworkStream.Setup(ns => ns.WriteAsync(It.IsAny(), It.IsAny())).Callback((buffer, ct) => { response = Encoding.UTF8.GetString(buffer); }); // Act - await _socketWrapper.AcceptWebSocketAsync(request, cancellationToken); + await _socketWrapper.AcceptWebSocketAsync(request, Guid.NewGuid().ToString(), cancellationToken); // Assert Assert.That(response, Does.Contain("HTTP/1.1 101 Switching Protocols")); @@ -90,7 +90,7 @@ public async Task AcceptWebSocketAsync_ShouldSendUpgradeResponseWithCorrectProto _mockNetworkStream.Setup(ns => ns.WriteAsync(It.IsAny(), It.IsAny())).Callback((buffer, ct) => { response = Encoding.UTF8.GetString(buffer); }); // Act - await _socketWrapper.AcceptWebSocketAsync(request, serverSubprotocol, cancellationToken); + await _socketWrapper.AcceptWebSocketAsync(request, Guid.NewGuid().ToString(), serverSubprotocol, cancellationToken); // Assert Assert.Multiple(() => From 985cb525bc42945d170283c0e722943495ab469f Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Sun, 29 Sep 2024 01:54:18 +0200 Subject: [PATCH 03/27] Fix merge issues --- Jung.SimpleWebSocket/Models/WebContext.cs | 11 +++++++---- Jung.SimpleWebSocket/SimpleWebSocketClient.cs | 1 + .../Wrappers/WebSocketUpgradeHandler.cs | 12 +----------- Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs | 16 +++++++--------- .../WebSocketUpgradeHandlerTests.cs | 4 ++-- 5 files changed, 18 insertions(+), 26 deletions(-) diff --git a/Jung.SimpleWebSocket/Models/WebContext.cs b/Jung.SimpleWebSocket/Models/WebContext.cs index ffa6f8c..04173cf 100644 --- a/Jung.SimpleWebSocket/Models/WebContext.cs +++ b/Jung.SimpleWebSocket/Models/WebContext.cs @@ -61,7 +61,7 @@ public partial class WebContext(string? content = null) private string? _bodyContent = null; /// - /// Gets the headers of the web request. + /// Gets the headers. /// public NameValueCollection Headers { @@ -72,6 +72,9 @@ public NameValueCollection Headers } } + /// + /// Gets or Sets the body content. + /// public string BodyContent { get @@ -374,7 +377,6 @@ public string RequestLine } /// -<<<<<<< HEAD /// Gets a value indicating whether the Content is empty. /// public bool IsEmpty => string.IsNullOrWhiteSpace(_content); @@ -395,7 +397,9 @@ public static string GetStatusDescription(HttpStatusCode statusCode) var enumName = Enum.GetName(statusCode) ?? throw new WebSocketUpgradeException("Status code is not a valid HttpStatusCode"); return string.Join(" ", _splitByUppercaseRegex.Split(enumName)); } -======= + + + /// /// Gets the user id of the web request. /// public string UserId @@ -411,7 +415,6 @@ public string UserId /// Gets a value indicating whether the web request contains a user id. /// public bool ContainsUserId => Headers["x-user-id"] != null; ->>>>>>> origin/handling-user-id-header /// /// Gets the content lines of the web request. diff --git a/Jung.SimpleWebSocket/SimpleWebSocketClient.cs b/Jung.SimpleWebSocket/SimpleWebSocketClient.cs index 7b362cf..2138cd6 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketClient.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketClient.cs @@ -22,6 +22,7 @@ namespace Jung.SimpleWebSocket /// The host name to connect to /// The port to connect to /// The web socket request path + /// The user id of the client. This is normally created by the server and sent back to the client /// A logger to write internal log messages public class SimpleWebSocketClient(string hostName, int port, string requestPath, string? userId = null, ILogger? logger = null) : IWebSocketClient, IDisposable { diff --git a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs index 80c907b..38f44f6 100644 --- a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs +++ b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs @@ -63,17 +63,7 @@ public async Task AwaitContextAsync(CancellationToken cancellationTo return context; } - public async Task AcceptWebSocketAsync(WebContext request, string userId, CancellationToken cancellationToken) - { - await AcceptWebSocketAsync(request, userId, null, cancellationToken); - } - - public async Task AcceptWebSocketAsync(WebContext request, string userId, string? subProtocol, CancellationToken cancellationToken) - { - await AcceptWebSocketAsync(request, new WebContext(), subProtocol, cancellationToken); - } - - public async Task AcceptWebSocketAsync(WebContext request, WebContext response, string? subProtocol, CancellationToken cancellationToken) + public async Task AcceptWebSocketAsync(WebContext request, WebContext response, string userId, string? subProtocol, CancellationToken cancellationToken) { try { diff --git a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs index 982f0d9..bdda98a 100644 --- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs +++ b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs @@ -61,7 +61,7 @@ private void SetUpLogger(Mock> mock, string loggerName) var invokeMethod = formatter.GetType().GetMethod("Invoke"); var logMessage = invokeMethod!.Invoke(formatter, [state, exception]); - _logMessages.Add($"{loggerName}({logLevel}): {logMessage}"); + _logMessages.Add($"[{DateTime.Now:HH:mm:ss:fff}] {loggerName} ({logLevel}): {logMessage}"); })); } @@ -109,6 +109,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() server.ClientUpgradeRequestReceivedAsync += async (sender, args, cancellationToken) => { + // Get the IP address of the client var IpAddress = (args.Client.RemoteEndPoint as IPEndPoint)?.Address.ToString(); if (IpAddress == null) { @@ -121,8 +122,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() if (!isWhitelistedEndPoint) { args.ResponseContext.StatusCode = HttpStatusCode.Forbidden; - // not yet implemented - // args.ResponseContext.Body = ""; + args.ResponseContext.BodyContent = "Connection only possible via local network."; args.Handle = false; } args.Client.Properties["test"] = "test"; @@ -131,21 +131,22 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() // Act server.Start(); await client.ConnectAsync(); - WaitForManualResetEventOrThrow(connectResetEvent); await client.SendMessageAsync(Message); WaitForManualResetEventOrThrow(messageResetEvent); await client.DisconnectAsync(ClosingStatusDescription); - WaitForManualResetEventOrThrow(disconnectResetEvent, 100000); + WaitForManualResetEventOrThrow(disconnectResetEvent); + // test if the server accepts the client again var client2 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", client.UserId, logger: _clientLogger.Object); await client2.ConnectAsync(); - await Task.Delay(1000); + await Task.Delay(100); try { + // test if two clients with the same user id can connect var client3 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", client.UserId, logger: _clientLogger.Object); await client3.ConnectAsync(); } @@ -153,11 +154,8 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() { exceptionMessage = exception.InnerException!.Message; } - await client2.SendMessageAsync("Hello World"); - await Task.Delay(1000); - await server.ShutdownServer(CancellationToken.None); _logMessages.ForEach(m => Trace.WriteLine(m)); diff --git a/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs b/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs index 254ddeb..aa69078 100644 --- a/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs +++ b/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs @@ -64,7 +64,7 @@ public async Task AcceptWebSocketAsync_ShouldSendUpgradeResponse(string hostname _mockNetworkStream.Setup(ns => ns.WriteAsync(It.IsAny(), It.IsAny())).Callback((buffer, ct) => { response = Encoding.UTF8.GetString(buffer); }); // Act - await _socketWrapper.AcceptWebSocketAsync(request, Guid.NewGuid().ToString(), cancellationToken); + await _socketWrapper.AcceptWebSocketAsync(request,new WebContext(), Guid.NewGuid().ToString(), null, cancellationToken); // Assert Assert.That(response, Does.Contain("HTTP/1.1 101 Switching Protocols")); @@ -90,7 +90,7 @@ public async Task AcceptWebSocketAsync_ShouldSendUpgradeResponseWithCorrectProto _mockNetworkStream.Setup(ns => ns.WriteAsync(It.IsAny(), It.IsAny())).Callback((buffer, ct) => { response = Encoding.UTF8.GetString(buffer); }); // Act - await _socketWrapper.AcceptWebSocketAsync(request, Guid.NewGuid().ToString(), serverSubprotocol, cancellationToken); + await _socketWrapper.AcceptWebSocketAsync(request, new WebContext(), Guid.NewGuid().ToString(), serverSubprotocol, cancellationToken); // Assert Assert.Multiple(() => From 1ad02faac2f41a93dc468d92f7f3ce99a6518658 Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Sun, 29 Sep 2024 01:55:28 +0200 Subject: [PATCH 04/27] Add Expiring dictionary Add event for expired passive user --- .../Contracts/IWebSocketServer.cs | 5 + .../PassiveUserExpiredEventHandler.cs | 13 ++ .../Models/EventArguments/ItemExpiredArgs.cs | 10 + .../PassiveUserExpiredEventArgs.cs | 10 + .../Models/SimpleWebSocketServerOptions.cs | 31 +++ Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 97 +++++---- .../Utility/ExpiringDictionary.cs | 191 ++++++++++++++++++ .../SimpleWebSocketTest.cs | 72 ++++++- 8 files changed, 388 insertions(+), 41 deletions(-) create mode 100644 Jung.SimpleWebSocket/Delegates/PassiveUserExpiredEventHandler.cs create mode 100644 Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs create mode 100644 Jung.SimpleWebSocket/Models/EventArguments/PassiveUserExpiredEventArgs.cs create mode 100644 Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs create mode 100644 Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs diff --git a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs b/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs index 25e5918..464198f 100644 --- a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs +++ b/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs @@ -59,6 +59,11 @@ public interface IWebSocketServer : IDisposable /// event ClientBinaryMessageReceivedEventHandler? BinaryMessageReceived; + /// + /// Occurs when an passive user expired. + /// + event PassiveUserExpiredEventHandler? PassiveUserExpiredEvent; + /// /// Async Event that is raised when a client upgrade request is received. /// diff --git a/Jung.SimpleWebSocket/Delegates/PassiveUserExpiredEventHandler.cs b/Jung.SimpleWebSocket/Delegates/PassiveUserExpiredEventHandler.cs new file mode 100644 index 0000000..de46577 --- /dev/null +++ b/Jung.SimpleWebSocket/Delegates/PassiveUserExpiredEventHandler.cs @@ -0,0 +1,13 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Jung.SimpleWebSocket.Models.EventArguments; + +namespace Jung.SimpleWebSocket.Delegates; + +/// +/// The event handler for the passive user expired event. +/// +/// The sender of the event. +/// The arguments of the event. +public delegate void PassiveUserExpiredEventHandler(object sender, PassiveUserExpiredArgs e); \ No newline at end of file diff --git a/Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs b/Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs new file mode 100644 index 0000000..f2b78be --- /dev/null +++ b/Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs @@ -0,0 +1,10 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +namespace Jung.SimpleWebSocket.Models.EventArguments; + +/// +/// Represents the arguments of the event when an item is expired. +/// +/// The item that is expired. +public record ItemExpiredArgs(TValue Item); \ No newline at end of file diff --git a/Jung.SimpleWebSocket/Models/EventArguments/PassiveUserExpiredEventArgs.cs b/Jung.SimpleWebSocket/Models/EventArguments/PassiveUserExpiredEventArgs.cs new file mode 100644 index 0000000..19cd99a --- /dev/null +++ b/Jung.SimpleWebSocket/Models/EventArguments/PassiveUserExpiredEventArgs.cs @@ -0,0 +1,10 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +namespace Jung.SimpleWebSocket.Models.EventArguments; + +/// +/// Represents the arguments of the event when a passive user expired. +/// +/// The identifier of the user that expired. +public record PassiveUserExpiredArgs(string ClientId); \ No newline at end of file diff --git a/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs b/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs new file mode 100644 index 0000000..2feacc8 --- /dev/null +++ b/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs @@ -0,0 +1,31 @@ +using System.Net; + +namespace Jung.SimpleWebSocket.Models +{ + /// + /// Represents the options for the SimpleWebSocketServer. + /// + public class SimpleWebSocketServerOptions + { + /// + /// Gets or sets the local IP address of the server. + /// + public IPAddress LocalIpAddress { get; set; } = IPAddress.Any; + + + /// + /// Gets or sets the port of the server. + /// + public int Port { get; set; } + + /// + /// Gets or sets the state of the user handling. + /// + public bool ActivateUserHandling { get; set; } = false; + + /// + /// The time after which a passive client is removed from the passive client list. + /// + public TimeSpan PassiveClientLifetime { get; set; } = TimeSpan.FromMinutes(1); + } +} diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index bf5c27a..5e7e44e 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -34,6 +34,8 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable public event ClientMessageReceivedEventHandler? MessageReceived; /// public event ClientBinaryMessageReceivedEventHandler? BinaryMessageReceived; + /// + public event PassiveUserExpiredEventHandler? PassiveUserExpiredEvent; /// public event AsyncEventHandler? ClientUpgradeRequestReceivedAsync; @@ -43,17 +45,20 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// private ConcurrentDictionary ActiveClients { get; } = []; + /// + /// A dictionary of passive clients. + /// + /// + /// The passive clients get removed after a certain time of inactivity. + /// + private ExpiringDictionary PassiveClients { get; } + /// public string[] ClientIds => [.. ActiveClients.Keys]; /// public int ClientCount => ActiveClients.Count; - /// - /// Future: Handle passive (disconnected) clients, delete them after a period of time, configurate this behavior in the WebSocketServerOptions - /// - private ConcurrentDictionary PassiveClients { get; } = []; - /// public bool IsListening => _server?.IsListening ?? false; @@ -86,29 +91,27 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// Initializes a new instance of the class that listens /// for incoming connection attempts on the specified local IP address and port number. /// - /// A local ip address - /// A port on which to listen for incoming connection attempts + /// The options for the server /// A logger to write internal log messages - public SimpleWebSocketServer(IPAddress localIpAddress, int port, ILogger? logger = null) + public SimpleWebSocketServer(SimpleWebSocketServerOptions options, ILogger? logger = null) { - LocalIpAddress = localIpAddress; - Port = port; + LocalIpAddress = options.LocalIpAddress; + Port = options.Port; _logger = logger; + PassiveClients = new(options.PassiveClientLifetime, _logger); + PassiveClients.ItemExpired += PassiveClients_ItemExpired; } /// /// Constructor for dependency injection (used in tests) /// - /// A local ip address - /// A port on which to listen for incoming connection attempts + /// The options for the server /// A wrapped tcp listener /// >A logger to write internal log messages - internal SimpleWebSocketServer(IPAddress localIpAddress, int port, ITcpListener tcpListener, ILogger? logger = null) + internal SimpleWebSocketServer(SimpleWebSocketServerOptions options, ITcpListener tcpListener, ILogger? logger = null) + : this(options, logger) { - LocalIpAddress = localIpAddress; - Port = port; _server = tcpListener; - _logger = logger; } /// @@ -224,8 +227,8 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT { // Upgrade the connection to a WebSocket using var stream = client.ClientConnection!.GetStream(); - var socketWrapper = new WebSocketUpgradeHandler(stream); - var request = await socketWrapper.AwaitContextAsync(cancellationToken); + var upgradeHandler = new WebSocketUpgradeHandler(stream); + var request = await upgradeHandler.AwaitContextAsync(cancellationToken); // Check if the request contains a user id if (request.ContainsUserId) @@ -243,7 +246,7 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT var passiveClient = PassiveClients[request.UserId]; passiveClient.UpdateClient(client.ClientConnection); client = passiveClient; - PassiveClients.TryRemove(request.UserId, out _); + PassiveClients.Remove(request.UserId); } else { @@ -252,7 +255,7 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT { _logger?.LogDebug("Active client found for user id {userId} - rejecting connection.", request.UserId); // Reject the connection - await socketWrapper.RejectWebSocketAsync(cancellationToken); + await upgradeHandler.RejectWebSocketAsync(cancellationToken); return; } else @@ -263,30 +266,32 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT } } - // raise async client upgrade request received event + // raise async client upgrade request received event var eventArgs = new ClientUpgradeRequestReceivedArgs(client, request, _logger); await AsyncEventRaiser.RaiseAsync(ClientUpgradeRequestReceivedAsync, this, eventArgs, cancellationToken); - if (!eventArgs.Handle) + if (eventArgs.Handle) { - _logger?.LogDebug("Client upgrade request rejected by ClientUpgradeRequestReceivedAsync event."); - // send rejection response - return; - } - - await socketWrapper.AcceptWebSocketAsync(request, client.Id, cancellationToken); + // The client is accepted + await upgradeHandler.AcceptWebSocketAsync(request, eventArgs.ResponseContext, client.Id, null, cancellationToken); - // Update the client with the new WebSocket - client.UseWebSocket(socketWrapper.CreateWebSocket(isServer: true)); + // Update the client with the new WebSocket + client.UseWebSocket(upgradeHandler.CreateWebSocket(isServer: true)); - clientAdded = ActiveClients.TryAdd(client.Id, client); - if (clientAdded) + clientAdded = ActiveClients.TryAdd(client.Id, client); + if (clientAdded) + { + _ = Task.Run(() => ClientConnected?.Invoke(this, new ClientConnectedArgs(client.Id)), cancellationToken); + // Start listening for messages + _logger?.LogDebug("Connection upgraded, now listening on client {clientId}", client.Id); + await ProcessWebSocketMessagesAsync(client, cancellationToken); + } + } + else { - _ = Task.Run(() => ClientConnected?.Invoke(this, new ClientConnectedArgs(client.Id)), cancellationToken); - // Start listening for messages - _logger?.LogDebug("Connection upgraded, now listening on client {clientId}", client.Id); - await ProcessWebSocketMessagesAsync(client, cancellationToken); + // The client is rejected + _logger?.LogDebug("Client upgrade request rejected by ClientUpgradeRequestReceivedAsync event."); + await upgradeHandler.RejectWebSocketAsync(cancellationToken); } - } catch (OperationCanceledException) { @@ -311,7 +316,9 @@ private void HandleDisconnectedClient(WebSocketServerClient client) { ActiveClients.TryRemove(client.Id, out _); client.Dispose(); - PassiveClients.TryAdd(client.Id, client); + + _logger?.LogDebug("Client {clientId} is now a passive user.", client.Id); + PassiveClients.Add(client.Id, client); } /// @@ -362,6 +369,20 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C } } + /// + /// Handles the event when a passive user expired. + /// + /// The sender of the event () + /// The arguments of the event + private void PassiveClients_ItemExpired(object? sender, ItemExpiredArgs e) + { + _logger?.LogDebug("Passive client expired: {clientId}", e.Item.Id); + + // Raise the event asynchronously + // We don't want to block the cleanup process + Task.Run(() => PassiveUserExpiredEvent?.Invoke(this, new PassiveUserExpiredArgs(e.Item.Id))); + } + /// public void Dispose() { diff --git a/Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs b/Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs new file mode 100644 index 0000000..b21f4ac --- /dev/null +++ b/Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs @@ -0,0 +1,191 @@ +// 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.Utility; + +/// +/// Creates a dictionary with expiration time for each item. +/// +/// The type of the keys in the dictionary. +/// The type of the values in the dictionary. +/// The expiration time for each item. +/// The logger to log exceptions. +public class ExpiringDictionary(TimeSpan expiration, ILogger? logger = null) where TKey : class +{ + + /// + /// Occurs when an item is expired. + /// + public event EventHandler>? ItemExpired; + + private readonly SortedList _expirationQueue = []; + private readonly Dictionary _dictionary = []; + + private bool _cleanupInProgress = false; + + /// + /// Add the specified key and value to the dictionary. + /// + /// + /// + public void Add(TKey key, TValue value) + { + lock (_dictionary) + { + // Add the item to the dictionary + _dictionary[key] = value; + + // Add item with expiration time to the queue + var expirationTime = DateTime.Now.Add(expiration); + + lock (_expirationQueue) + { + _expirationQueue.Add(expirationTime, key); + } + } + + // Trigger cleanup after the Add operation is done + lock (_expirationQueue) + { + if (!_cleanupInProgress) + { + _cleanupInProgress = true; + + // Run cleanup asynchronously + CleanupExpiredItems().ContinueWith(t => + { + if (t.IsFaulted) + { + // Handle exceptions here + logger?.LogError(t.Exception, "An Exception occurred during cleanup expired items."); + } + }); + } + } + } + + /// + /// Determines whether the dictionary contains the specified key. + /// + /// + /// + public bool ContainsKey(TKey key) + { + lock (_dictionary) + { + return _dictionary.ContainsKey(key); + } + } + /// + /// Removes the value with the specified key from the dictionary. + /// + /// The key of the value to remove. + /// Returns true if the element is successfully found and removed; otherwise, false. + public bool Remove(TKey key) + { + lock (_dictionary) + { + if (_dictionary.Remove(key)) + { + lock (_expirationQueue) + { + // Find and remove the expiration time entry for this key + var expirationTime = _expirationQueue.FirstOrDefault(x => x.Value.Equals(key)).Key; + if (expirationTime != default) + { + _expirationQueue.Remove(expirationTime); + } + } + return true; + } + } + return false; + } + + /// + /// Get or set the value associated with the specified key. + /// + /// The key of the value to get or set. + /// The value associated with the specified key. + public TValue this[TKey key] + { + get + { + lock (_dictionary) + { + return _dictionary[key]; + } + } + set + { + lock (_dictionary) + { + _dictionary[key] = value; + var expirationTime = DateTime.Now.Add(expiration); + lock (_expirationQueue) + { + _expirationQueue[expirationTime] = key; + } + } + } + } + + /// + /// Cleans up expired items. + /// + /// A task that represents the asynchronous cleanup operation. + private async Task CleanupExpiredItems() + { + while (true) + { + DateTime nearestExpiration; + TKey expiredKey; + + // Safely lock and retrieve the first item to expire + lock (_expirationQueue) + { + // If there are no items, stop the cleanup process + if (_expirationQueue.Count == 0) + { + _cleanupInProgress = false; + return; + } + + // Get the first key in expiration queue (FIFO order) + var firstItem = _expirationQueue.First(); + nearestExpiration = firstItem.Key; + expiredKey = firstItem.Value; + } + + // Calculate the delay based on the expiration time + TimeSpan delay = nearestExpiration - DateTime.Now; + + if (delay > TimeSpan.Zero) + { + await Task.Delay(delay); + } + + // Remove the expired item from the dictionary + lock (_dictionary) + { + if (_dictionary.TryGetValue(expiredKey, out var expiredItem)) + { + ItemExpired?.Invoke(this, new ItemExpiredArgs(expiredItem)); + _dictionary.Remove(expiredKey); + } + } + + // Remove from expiration queue, but only if it's still the correct key + lock (_expirationQueue) + { + if (_expirationQueue.Count > 0 && _expirationQueue.First().Value.Equals(expiredKey)) + { + _expirationQueue.RemoveAt(0); // Safely remove the correct item + } + } + } + } +} \ No newline at end of file diff --git a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs index bdda98a..13920cf 100644 --- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs +++ b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs @@ -2,6 +2,7 @@ // The project is licensed under the MIT license. using Jung.SimpleWebSocket; +using Jung.SimpleWebSocket.Models; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; @@ -70,7 +71,15 @@ private void SetUpLogger(Mock> mock, string loggerName) public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() { // Arrange - using var server = new SimpleWebSocketServer(IPAddress.Any, 8010, _serverLogger.Object); + var serverOptions = new SimpleWebSocketServerOptions + { + LocalIpAddress = IPAddress.Any, + Port = 8010, + ActivateUserHandling = true, + PassiveClientLifetime = TimeSpan.FromSeconds(5) + }; + + using var server = new SimpleWebSocketServer(serverOptions, _serverLogger.Object); using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", logger: _clientLogger.Object); @@ -169,6 +178,47 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() }); } + [Test] + [Platform("Windows7,Windows8,Windows8.1,Windows10", Reason = "This test establishes a TCP client-server connection using SimpleWebSocket, which relies on specific networking features and behaviors that are only available and consistent on Windows platforms. Running this test on non-Windows platforms could lead to inconsistent results or failures due to differences in networking stack implementations.")] + public async Task TestClientServerConnection_ShouldRemoveClientFromPassiveClients() + { + // Arrange + string userId = Guid.NewGuid().ToString(); + var serverOptions = new SimpleWebSocketServerOptions + { + LocalIpAddress = IPAddress.Any, + Port = 8010, + ActivateUserHandling = true, + PassiveClientLifetime = TimeSpan.FromSeconds(1) + }; + + using var server = new SimpleWebSocketServer(serverOptions, _serverLogger.Object); + using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", userId, _clientLogger.Object); + + var expiredClientId = string.Empty; + var expiredClientResetEvent = new ManualResetEvent(false); + + server.PassiveUserExpiredEvent += (sender, args) => + { + expiredClientId = args.ClientId; + expiredClientResetEvent.Set(); + }; + + // Act + server.Start(); + await client.ConnectAsync(); + await Task.Delay(100); + await client.DisconnectAsync(); + + WaitForManualResetEventOrThrow(expiredClientResetEvent, 2000); + + await server.ShutdownServer(CancellationToken.None); + _logMessages.ForEach(m => Trace.WriteLine(m)); + + // Assert + Assert.That(expiredClientId, Is.EqualTo(userId)); + } + /// /// Fake Async method to simulate a database call to check if the IP address is in the database. /// @@ -186,7 +236,15 @@ private static async Task DbContext_IpAddresses_Contains(string ipAddress, public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld2() { // Arrange - using var server = new SimpleWebSocketServer(IPAddress.Any, 8010, _serverLogger.Object); + var serverOptions = new SimpleWebSocketServerOptions + { + LocalIpAddress = IPAddress.Any, + Port = 8010, + ActivateUserHandling = true, + PassiveClientLifetime = TimeSpan.FromSeconds(1) + }; + + using var server = new SimpleWebSocketServer(serverOptions, _serverLogger.Object); using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", logger: _clientLogger.Object); @@ -249,7 +307,15 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld2() public async Task TestMultipleClientServerConnection_ShouldSendAndReceiveHelloWorld() { // Arrange - using var server = new SimpleWebSocketServer(IPAddress.Any, 8010); + var serverOptions = new SimpleWebSocketServerOptions + { + LocalIpAddress = IPAddress.Any, + Port = 8010, + ActivateUserHandling = true, + PassiveClientLifetime = TimeSpan.FromSeconds(1) + }; + + using var server = new SimpleWebSocketServer(serverOptions); List clients = []; var message = "Hello World"; const int clientsCount = 200; From 411e4992a285815cfcb62c4bc4f226fdaba246da Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Sun, 29 Sep 2024 02:09:32 +0200 Subject: [PATCH 05/27] Use individual messages when rejecting an upgrade request --- Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 10 +++++-- .../Wrappers/WebSocketUpgradeHandler.cs | 7 +---- .../SimpleWebSocketTest.cs | 28 ++++++++++--------- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index 5e7e44e..d95d39a 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -255,7 +255,13 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT { _logger?.LogDebug("Active client found for user id {userId} - rejecting connection.", request.UserId); // Reject the connection - await upgradeHandler.RejectWebSocketAsync(cancellationToken); + + var responseContext = new WebContext + { + StatusCode = HttpStatusCode.Conflict, + BodyContent = "User id already in use" + }; + await upgradeHandler.RejectWebSocketAsync(responseContext, cancellationToken); return; } else @@ -290,7 +296,7 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT { // The client is rejected _logger?.LogDebug("Client upgrade request rejected by ClientUpgradeRequestReceivedAsync event."); - await upgradeHandler.RejectWebSocketAsync(cancellationToken); + await upgradeHandler.RejectWebSocketAsync(eventArgs.ResponseContext, cancellationToken); } } catch (OperationCanceledException) diff --git a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs index 38f44f6..3c064ef 100644 --- a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs +++ b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs @@ -299,13 +299,8 @@ internal IWebSocket CreateWebSocket(bool isServer, TimeSpan? keepAliveInterval = return _websocketHelper.CreateFromStream(_networkStream.Stream, isServer, _acceptedProtocol, keepAliveInterval.Value); } - internal async Task RejectWebSocketAsync(CancellationToken cancellationToken) + internal async Task RejectWebSocketAsync(WebContext response, CancellationToken cancellationToken) { - var response = new WebContext - { - BodyContent = "User ID already connected" - }; - response.Headers.Add("Connection", "close"); response.Headers.Add("Content-Type", "text/plain"); response.Headers.Add("Content-Length", response.BodyContent.Length.ToString()); diff --git a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs index 13920cf..5e0fb4d 100644 --- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs +++ b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs @@ -174,10 +174,24 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() { Assert.That(receivedMessage, Is.EqualTo(Message)); Assert.That(receivedClosingDescription, Is.EqualTo(ClosingStatusDescription)); - Assert.That(exceptionMessage, Does.Contain("User ID already connected")); + Assert.That(exceptionMessage, Does.Contain("User id already in use")); }); } + + /// + /// Fake Async method to simulate a database call to check if the IP address is in the database. + /// + /// The IP address to check. + /// The cancellation token. + /// A task that represents the asynchronous operation. The task result contains a value indicating whether the IP address is in the database. + private static async Task DbContext_IpAddresses_Contains(string ipAddress, CancellationToken cancellationToken) + { + await Task.Delay(100, cancellationToken); + return ipAddress == IPAddress.Loopback.ToString(); + } + + [Test] [Platform("Windows7,Windows8,Windows8.1,Windows10", Reason = "This test establishes a TCP client-server connection using SimpleWebSocket, which relies on specific networking features and behaviors that are only available and consistent on Windows platforms. Running this test on non-Windows platforms could lead to inconsistent results or failures due to differences in networking stack implementations.")] public async Task TestClientServerConnection_ShouldRemoveClientFromPassiveClients() @@ -219,18 +233,6 @@ public async Task TestClientServerConnection_ShouldRemoveClientFromPassiveClient Assert.That(expiredClientId, Is.EqualTo(userId)); } - /// - /// Fake Async method to simulate a database call to check if the IP address is in the database. - /// - /// The IP address to check. - /// The cancellation token. - /// A task that represents the asynchronous operation. The task result contains a value indicating whether the IP address is in the database. - private static async Task DbContext_IpAddresses_Contains(string ipAddress, CancellationToken cancellationToken) - { - await Task.Delay(100, cancellationToken); - return ipAddress == IPAddress.Loopback.ToString(); - } - [Test] [Platform("Windows7,Windows8,Windows8.1,Windows10", Reason = "This test establishes a TCP client-server connection using SimpleWebSocket, which relies on specific networking features and behaviors that are only available and consistent on Windows platforms. Running this test on non-Windows platforms could lead to inconsistent results or failures due to differences in networking stack implementations.")] public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld2() From 2e3f3a727ef5fdcfd0d9b2339251f66b9181f47b Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Mon, 30 Sep 2024 19:52:52 +0200 Subject: [PATCH 06/27] Use IDictionary for passive users Add switch for remembering disconnected clients Add switch for removing passive users after the end of their lifetime --- .../Models/SimpleWebSocketServerOptions.cs | 21 ++- Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 141 +++++++++++++----- .../Utility/ExpiringDictionary.cs | 94 +++++++++++- .../SimpleWebSocketTest.cs | 20 +-- 4 files changed, 217 insertions(+), 59 deletions(-) diff --git a/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs b/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs index 2feacc8..7e5a486 100644 --- a/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs +++ b/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs @@ -12,20 +12,33 @@ public class SimpleWebSocketServerOptions /// public IPAddress LocalIpAddress { get; set; } = IPAddress.Any; - /// /// Gets or sets the port of the server. /// public int Port { get; set; } /// - /// Gets or sets the state of the user handling. + /// Switch for remembering disconnected clients. + /// + /// + /// If true the server will put disconnected clients into a passive client list. + /// This clients can reidentify themselves with their user id. + /// + public bool RememberDisconnectedClients { get; set; } = false; + + /// + /// Switch for removing passive clients after the end of the . + /// + public bool RemovePassiveClientsAfterClientExpirationTime { get; set; } = false; + + /// + /// Switch for sending the user id to the client. /// - public bool ActivateUserHandling { get; set; } = false; + public bool SendUserIdToClient { get; set; } = false; /// /// The time after which a passive client is removed from the passive client list. /// public TimeSpan PassiveClientLifetime { get; set; } = TimeSpan.FromMinutes(1); } -} +} \ No newline at end of file diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index d95d39a..5861ea9 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -9,6 +9,7 @@ using Jung.SimpleWebSocket.Utility; using Jung.SimpleWebSocket.Wrappers; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using System.Collections.Concurrent; using System.Net; using System.Net.WebSockets; @@ -48,10 +49,7 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// /// A dictionary of passive clients. /// - /// - /// The passive clients get removed after a certain time of inactivity. - /// - private ExpiringDictionary PassiveClients { get; } + private IDictionary PassiveClients { get; set; } = null!; /// public string[] ClientIds => [.. ActiveClients.Keys]; @@ -87,6 +85,11 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// private readonly ILogger? _logger; + /// + /// The options for the server. + /// + private readonly SimpleWebSocketServerOptions _options; + /// /// Initializes a new instance of the class that listens /// for incoming connection attempts on the specified local IP address and port number. @@ -98,8 +101,46 @@ public SimpleWebSocketServer(SimpleWebSocketServerOptions options, ILogger? logg LocalIpAddress = options.LocalIpAddress; Port = options.Port; _logger = logger; - PassiveClients = new(options.PassiveClientLifetime, _logger); - PassiveClients.ItemExpired += PassiveClients_ItemExpired; + _options = options; + InitializePassiveClientDictionary(options); + } + + /// + /// Initializes the passive clients dictionary. + /// + /// + private void InitializePassiveClientDictionary(SimpleWebSocketServerOptions options) + { + if (options.RememberDisconnectedClients) + { + // Initialize the passive clients dictionary + if (options.RemovePassiveClientsAfterClientExpirationTime) + { + var passiveClients = new ExpiringDictionary(options.PassiveClientLifetime, _logger); + passiveClients.ItemExpired += PassiveClients_ItemExpired; + PassiveClients = passiveClients; + } + else + { + PassiveClients = new Dictionary(); + } + } + else + { + // If user handling is not activated, the passive clients are not needed + PassiveClients = null!; + } + } + + /// + /// Initializes a new instance of the class that listens + /// for incoming connection attempts on the specified local IP address and port number. + /// + /// The options for the server + /// A logger to write internal log messages + public SimpleWebSocketServer(IOptions options, ILogger? logger = null) + : this(options.Value, logger) + { } /// @@ -230,44 +271,48 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT var upgradeHandler = new WebSocketUpgradeHandler(stream); var request = await upgradeHandler.AwaitContextAsync(cancellationToken); - // Check if the request contains a user id - if (request.ContainsUserId) + // Check if disconnected clients are remembered and can be reactivated + if (_options.RememberDisconnectedClients) { - _logger?.LogDebug("User id found in request: {userId}", request.UserId); - // Check if the client is an existing passive client - var clientExists = PassiveClients.ContainsKey(request.UserId); - if (clientExists) - { - _logger?.LogDebug("Passive client found for user id {userId} - reactivating user.", request.UserId); - - // Use the existing client - // Update the client with the new connection - // Remove the client from the passive clients - var passiveClient = PassiveClients[request.UserId]; - passiveClient.UpdateClient(client.ClientConnection); - client = passiveClient; - PassiveClients.Remove(request.UserId); - } - else + // Check if the request contains a user id + if (request.ContainsUserId) { - // No passive client found, checking for active clients with the same id - if (ActiveClients.ContainsKey(request.UserId)) + _logger?.LogDebug("User id found in request: {userId}", request.UserId); + // Check if the client is an existing passive client + var clientExists = PassiveClients.ContainsKey(request.UserId); + if (clientExists) { - _logger?.LogDebug("Active client found for user id {userId} - rejecting connection.", request.UserId); - // Reject the connection - - var responseContext = new WebContext - { - StatusCode = HttpStatusCode.Conflict, - BodyContent = "User id already in use" - }; - await upgradeHandler.RejectWebSocketAsync(responseContext, cancellationToken); - return; + _logger?.LogDebug("Passive client found for user id {userId} - reactivating user.", request.UserId); + + // Use the existing client + // Update the client with the new connection + // Remove the client from the passive clients + var passiveClient = PassiveClients[request.UserId]; + passiveClient.UpdateClient(client.ClientConnection); + client = passiveClient; + PassiveClients.Remove(request.UserId); } else { - // Update the client with the new id - client.UpdateId(request.UserId); + // No passive client found, checking for active clients with the same id + if (ActiveClients.ContainsKey(request.UserId)) + { + _logger?.LogDebug("Active client found for user id {userId} - rejecting connection.", request.UserId); + // Reject the connection + + var responseContext = new WebContext + { + StatusCode = HttpStatusCode.Conflict, + BodyContent = "User id already in use" + }; + await upgradeHandler.RejectWebSocketAsync(responseContext, cancellationToken); + return; + } + else + { + // Update the client with the new id + client.UpdateId(request.UserId); + } } } } @@ -280,7 +325,7 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT // The client is accepted await upgradeHandler.AcceptWebSocketAsync(request, eventArgs.ResponseContext, client.Id, null, cancellationToken); - // Update the client with the new WebSocket + // Use the websocket for the client client.UseWebSocket(upgradeHandler.CreateWebSocket(isServer: true)); clientAdded = ActiveClients.TryAdd(client.Id, client); @@ -318,13 +363,24 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT } } + /// + /// Handles the disconnected client. + /// + /// private void HandleDisconnectedClient(WebSocketServerClient client) { ActiveClients.TryRemove(client.Id, out _); client.Dispose(); - _logger?.LogDebug("Client {clientId} is now a passive user.", client.Id); - PassiveClients.Add(client.Id, client); + if (_options.RememberDisconnectedClients) + { + _logger?.LogDebug("Client {clientId} is now a passive user.", client.Id); + PassiveClients.Add(client.Id, client); + } + else + { + _logger?.LogDebug("Client {clientId} is removed.", client.Id); + } } /// @@ -378,6 +434,9 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C /// /// Handles the event when a passive user expired. /// + /// + /// Condition: is set to true. + /// /// The sender of the event () /// The arguments of the event private void PassiveClients_ItemExpired(object? sender, ItemExpiredArgs e) diff --git a/Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs b/Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs index b21f4ac..52d3131 100644 --- a/Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs +++ b/Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs @@ -3,6 +3,8 @@ using Jung.SimpleWebSocket.Models.EventArguments; using Microsoft.Extensions.Logging; +using System.Collections; +using System.Diagnostics.CodeAnalysis; namespace Jung.SimpleWebSocket.Utility; @@ -13,7 +15,7 @@ namespace Jung.SimpleWebSocket.Utility; /// The type of the values in the dictionary. /// The expiration time for each item. /// The logger to log exceptions. -public class ExpiringDictionary(TimeSpan expiration, ILogger? logger = null) where TKey : class +public class ExpiringDictionary(TimeSpan expiration, ILogger? logger = null): IDictionary where TKey : class { /// @@ -24,7 +26,7 @@ public class ExpiringDictionary(TimeSpan expiration, ILogger? logg private readonly SortedList _expirationQueue = []; private readonly Dictionary _dictionary = []; - private bool _cleanupInProgress = false; + private bool _cleanupInProgress = false; /// /// Add the specified key and value to the dictionary. @@ -188,4 +190,92 @@ private async Task CleanupExpiredItems() } } } + + #region NotImplemented + + /// + /// Not implemented. + /// + public ICollection Keys => throw new NotImplementedException(); + + /// + /// Not implemented. + /// + public ICollection Values => throw new NotImplementedException(); + + /// + /// Not implemented. + /// + public int Count => throw new NotImplementedException(); + + /// + /// Not implemented. + /// + public bool IsReadOnly => throw new NotImplementedException(); + + /// + /// Not implemented. + /// + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + throw new NotImplementedException(); + } + + /// + /// Not implemented. + /// + public void Add(KeyValuePair item) + { + throw new NotImplementedException(); + } + + /// + /// Not implemented. + /// + public void Clear() + { + throw new NotImplementedException(); + } + + /// + /// Not implemented. + /// + public bool Contains(KeyValuePair item) + { + throw new NotImplementedException(); + } + + /// + /// Not implemented. + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + /// + /// Not implemented. + /// + public bool Remove(KeyValuePair item) + { + throw new NotImplementedException(); + } + + /// + /// Not implemented. + /// + public IEnumerator> GetEnumerator() + { + throw new NotImplementedException(); + } + + /// + /// Not implemented. + /// + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + + #endregion } \ No newline at end of file diff --git a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs index 5e0fb4d..a4209dc 100644 --- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs +++ b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs @@ -75,8 +75,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() { LocalIpAddress = IPAddress.Any, Port = 8010, - ActivateUserHandling = true, - PassiveClientLifetime = TimeSpan.FromSeconds(5) + RememberDisconnectedClients = true, }; using var server = new SimpleWebSocketServer(serverOptions, _serverLogger.Object); @@ -119,7 +118,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() server.ClientUpgradeRequestReceivedAsync += async (sender, args, cancellationToken) => { // Get the IP address of the client - var IpAddress = (args.Client.RemoteEndPoint as IPEndPoint)?.Address.ToString(); + var IpAddress = (args.Client.RemoteEndPoint as IPEndPoint)?.Address; if (IpAddress == null) { args.Handle = false; @@ -185,10 +184,10 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() /// The IP address to check. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains a value indicating whether the IP address is in the database. - private static async Task DbContext_IpAddresses_Contains(string ipAddress, CancellationToken cancellationToken) + private static async Task DbContext_IpAddresses_Contains(IPAddress ipAddress, CancellationToken cancellationToken) { await Task.Delay(100, cancellationToken); - return ipAddress == IPAddress.Loopback.ToString(); + return ipAddress.Equals(IPAddress.Loopback); } @@ -202,7 +201,8 @@ public async Task TestClientServerConnection_ShouldRemoveClientFromPassiveClient { LocalIpAddress = IPAddress.Any, Port = 8010, - ActivateUserHandling = true, + RememberDisconnectedClients = true, + RemovePassiveClientsAfterClientExpirationTime = true, PassiveClientLifetime = TimeSpan.FromSeconds(1) }; @@ -241,9 +241,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld2() var serverOptions = new SimpleWebSocketServerOptions { LocalIpAddress = IPAddress.Any, - Port = 8010, - ActivateUserHandling = true, - PassiveClientLifetime = TimeSpan.FromSeconds(1) + Port = 8010 }; using var server = new SimpleWebSocketServer(serverOptions, _serverLogger.Object); @@ -312,9 +310,7 @@ public async Task TestMultipleClientServerConnection_ShouldSendAndReceiveHelloWo var serverOptions = new SimpleWebSocketServerOptions { LocalIpAddress = IPAddress.Any, - Port = 8010, - ActivateUserHandling = true, - PassiveClientLifetime = TimeSpan.FromSeconds(1) + Port = 8010 }; using var server = new SimpleWebSocketServer(serverOptions); From ba4809dea53e724a5d9ff9110e935b2a2c9ac37a Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Mon, 30 Sep 2024 21:40:33 +0200 Subject: [PATCH 07/27] Add client handling flow --- Jung.SimpleWebSocket/ClientHandlingFlow.cs | 184 ++++++++++++++++++ Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 162 +++++---------- .../UserNotHandledException.cs | 13 ++ .../Wrappers/WebSocketUpgradeHandler.cs | 4 +- 4 files changed, 249 insertions(+), 114 deletions(-) create mode 100644 Jung.SimpleWebSocket/ClientHandlingFlow.cs create mode 100644 Jung.SimpleWebSocket/UserNotHandledException.cs diff --git a/Jung.SimpleWebSocket/ClientHandlingFlow.cs b/Jung.SimpleWebSocket/ClientHandlingFlow.cs new file mode 100644 index 0000000..221e78a --- /dev/null +++ b/Jung.SimpleWebSocket/ClientHandlingFlow.cs @@ -0,0 +1,184 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Jung.SimpleWebSocket.Models; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using System.Net; + +namespace Jung.SimpleWebSocket +{ + /// + /// A flow that handles the client connection. + /// + /// + /// Creates a new instance of the class. + /// + /// The client to handle. + /// The server that handles the client. + /// The cancellation token of the server. + internal class ClientHandlingFlow(SimpleWebSocketServer server, WebSocketServerClient client, CancellationToken cancellationToken) + { + /// + /// Gets the client associated with the flow. + /// + internal WebSocketServerClient Client { get; set; } = client; + + /// + /// Gets the request context of the client. + /// + internal WebContext Request { get; set; } = null!; + + /// + /// Gets the upgrade handler for the client. + /// + private WebSocketUpgradeHandler _upgradeHandler = null!; + + /// + /// Gets the options of the server. + /// + private readonly SimpleWebSocketServerOptions _options = server.Options; + + /// + /// Gets the active clients of the server. + /// + private readonly ConcurrentDictionary _activeClients = server.ActiveClients; + + /// + /// Gets the passive clients of the server. + /// + private readonly IDictionary _passiveClients = server.PassiveClients; + + /// + /// Gets the logger of the server. + /// + private readonly ILogger? _logger = server.Logger; + + /// + /// Gets the cancellation token of the server. + /// + private readonly CancellationToken _cancellationToken = cancellationToken; + + /// + /// The lock object for the client dictionaries. + /// + private static readonly object _clientLock = new(); + + /// + /// Loads the request context. + /// + internal async Task LoadRequestContext() + { + var stream = Client.ClientConnection!.GetStream(); + _upgradeHandler = new WebSocketUpgradeHandler(stream); + Request = await _upgradeHandler.AwaitContextAsync(_cancellationToken); + } + + /// + /// Handles the client identification. + /// + internal void HandleClientIdentification() + { + // Check if disconnected clients are remembered and can be reactivated + if (_options.RememberDisconnectedClients) + { + // Check if the request contains a user id + if (Request.ContainsUserId) + { + _logger?.LogDebug("User id found in request: {userId}", Request.UserId); + + lock (_clientLock) + { + ThrowForUserAlreadyConnected(); + + // Check if the client is an existing passive client + var clientExists = _passiveClients.ContainsKey(Request.UserId); + if (clientExists) + { + _logger?.LogDebug("Passive Client found for user id {userId} - reactivating user.", Request.UserId); + + // Use the existing client + // Update the client with the new connection + // Remove the client from the passive clients + var passiveClient = _passiveClients[Request.UserId]; + passiveClient.UpdateClient(Client.ClientConnection!); + Client = passiveClient; + _passiveClients.Remove(Request.UserId); + } + else + { + // Client is not a passive client + // Update the clients user id + Client.UpdateId(Request.UserId); + } + } + } + } + } + + /// + /// Throws an exception if the user is already connected. + /// + /// + private void ThrowForUserAlreadyConnected() + { + // No passive client found, checking for active clients with the same id + if (_activeClients.ContainsKey(Request.UserId)) + { + _logger?.LogDebug("Active Client found for user id {userId} - rejecting connection.", Request.UserId); + // Reject the connection + + var responseContext = new WebContext + { + StatusCode = HttpStatusCode.Conflict, + BodyContent = "User id already in use" + }; + throw new UserNotHandledException(responseContext); + } + } + + /// + /// Accepts the websocket connection. + /// + /// The response context to send to the client. + internal async Task AcceptWebSocketAsync(WebContext responseContext) + { + // The client is accepted + await _upgradeHandler.AcceptWebSocketAsync(Request, responseContext, Client.Id, null, _cancellationToken); + + // Use the websocket for the client + Client.UseWebSocket(_upgradeHandler.CreateWebSocket(isServer: true)); + } + + /// + /// Rejects the websocket connection. + /// + /// The response context to send to the client. + internal async Task RejectWebSocketAsync(WebContext responseContext) + { + await _upgradeHandler.RejectWebSocketAsync(responseContext, _cancellationToken); + } + + /// + /// Handles the disconnected client. + /// + internal void HandleDisconnectedClient() + { + lock (_clientLock) + { + _activeClients.TryRemove(Client.Id, out _); + Client.Dispose(); + + if (_options.RememberDisconnectedClients) + { + _logger?.LogDebug("Client {clientId} is now a passive user.", Client.Id); + _passiveClients.Add(Client.Id, Client); + } + else + { + _logger?.LogDebug("Client {clientId} is removed.", Client.Id); + } + } + } + } +} \ No newline at end of file diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index 5861ea9..2634e70 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -14,6 +14,7 @@ using System.Net; using System.Net.WebSockets; using System.Text; +using System.Threading; namespace Jung.SimpleWebSocket { @@ -44,12 +45,12 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// /// A dictionary of active clients. /// - private ConcurrentDictionary ActiveClients { get; } = []; + internal ConcurrentDictionary ActiveClients { get; } = []; /// /// A dictionary of passive clients. /// - private IDictionary PassiveClients { get; set; } = null!; + internal IDictionary PassiveClients { get; set; } = null!; /// public string[] ClientIds => [.. ActiveClients.Keys]; @@ -60,6 +61,16 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// public bool IsListening => _server?.IsListening ?? false; + /// + /// A logger to write internal log messages. + /// + internal ILogger? Logger { get; } + + /// + /// The options for the server. + /// + internal SimpleWebSocketServerOptions Options { get; } + /// /// A flag indicating whether the server is started. /// @@ -80,16 +91,6 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// private ITcpListener? _server; - /// - /// A logger to write internal log messages. - /// - private readonly ILogger? _logger; - - /// - /// The options for the server. - /// - private readonly SimpleWebSocketServerOptions _options; - /// /// Initializes a new instance of the class that listens /// for incoming connection attempts on the specified local IP address and port number. @@ -100,8 +101,8 @@ public SimpleWebSocketServer(SimpleWebSocketServerOptions options, ILogger? logg { LocalIpAddress = options.LocalIpAddress; Port = options.Port; - _logger = logger; - _options = options; + Logger = logger; + Options = options; InitializePassiveClientDictionary(options); } @@ -116,7 +117,7 @@ private void InitializePassiveClientDictionary(SimpleWebSocketServerOptions opti // Initialize the passive clients dictionary if (options.RemovePassiveClientsAfterClientExpirationTime) { - var passiveClients = new ExpiringDictionary(options.PassiveClientLifetime, _logger); + var passiveClients = new ExpiringDictionary(options.PassiveClientLifetime, Logger); passiveClients.ItemExpired += PassiveClients_ItemExpired; PassiveClients = passiveClients; } @@ -169,7 +170,7 @@ public void Start(CancellationToken? cancellationToken = null) _server.Start(); _ = Task.Run(async delegate { - _logger?.LogInformation("Server started at {LocalIpAddress}:{Port}", LocalIpAddress, Port); + Logger?.LogInformation("Server started at {LocalIpAddress}:{Port}", LocalIpAddress, Port); while (!linkedTokenSource.IsCancellationRequested) { try @@ -177,7 +178,7 @@ public void Start(CancellationToken? cancellationToken = null) // Accept the client var client = await _server.AcceptTcpClientAsync(linkedTokenSource.Token); - _logger?.LogDebug("Client connected from {endpoint}", client.ClientConnection!.RemoteEndPoint); + Logger?.LogDebug("Client connected from {endpoint}", client.ClientConnection!.RemoteEndPoint); _ = HandleClientAsync(client, linkedTokenSource.Token); } @@ -187,7 +188,7 @@ public void Start(CancellationToken? cancellationToken = null) } catch (Exception exception) { - _logger?.LogError(exception, "Error while accepting client."); + Logger?.LogError(exception, "Error while accepting Client."); } } }, linkedTokenSource.Token); @@ -203,7 +204,7 @@ public async Task ShutdownServer(CancellationToken? cancellationToken = null) cancellationToken ??= CancellationToken.None; var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value, _cancellationTokenSource.Token); - _logger?.LogInformation("Stopping server..."); + Logger?.LogInformation("Stopping server..."); // copying the active clients to avoid a collection modified exception var activeClients = ActiveClients.Values.ToArray(); @@ -220,7 +221,7 @@ public async Task ShutdownServer(CancellationToken? cancellationToken = null) _cancellationTokenSource?.Cancel(); _server?.Dispose(); _server = null; - _logger?.LogInformation("Server stopped"); + Logger?.LogInformation("Server stopped"); } /// @@ -238,11 +239,11 @@ 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); - _logger?.LogDebug("Message sent: {message}.", message); + Logger?.LogDebug("Message sent: {message}.", message); } catch (Exception exception) { - _logger?.LogError(exception, "Error while sending a message."); + Logger?.LogError(exception, "Error while sending a message."); throw new WebSocketServerException(message: "An Error occurred sending a message.", innerException: exception); } } @@ -263,126 +264,63 @@ public WebSocketServerClient GetClientById(string clientId) /// A asynchronous task private async Task HandleClientAsync(WebSocketServerClient client, CancellationToken cancellationToken) { - bool clientAdded = false; + var flow = new ClientHandlingFlow(this, client, cancellationToken); + var clientHandled = false; try { - // Upgrade the connection to a WebSocket - using var stream = client.ClientConnection!.GetStream(); - var upgradeHandler = new WebSocketUpgradeHandler(stream); - var request = await upgradeHandler.AwaitContextAsync(cancellationToken); + // Load the request context + await flow.LoadRequestContext(); - // Check if disconnected clients are remembered and can be reactivated - if (_options.RememberDisconnectedClients) - { - // Check if the request contains a user id - if (request.ContainsUserId) - { - _logger?.LogDebug("User id found in request: {userId}", request.UserId); - // Check if the client is an existing passive client - var clientExists = PassiveClients.ContainsKey(request.UserId); - if (clientExists) - { - _logger?.LogDebug("Passive client found for user id {userId} - reactivating user.", request.UserId); - - // Use the existing client - // Update the client with the new connection - // Remove the client from the passive clients - var passiveClient = PassiveClients[request.UserId]; - passiveClient.UpdateClient(client.ClientConnection); - client = passiveClient; - PassiveClients.Remove(request.UserId); - } - else - { - // No passive client found, checking for active clients with the same id - if (ActiveClients.ContainsKey(request.UserId)) - { - _logger?.LogDebug("Active client found for user id {userId} - rejecting connection.", request.UserId); - // Reject the connection - - var responseContext = new WebContext - { - StatusCode = HttpStatusCode.Conflict, - BodyContent = "User id already in use" - }; - await upgradeHandler.RejectWebSocketAsync(responseContext, cancellationToken); - return; - } - else - { - // Update the client with the new id - client.UpdateId(request.UserId); - } - } - } - } + // Handle the client user identification if activated + flow.HandleClientIdentification(); // raise async client upgrade request received event - var eventArgs = new ClientUpgradeRequestReceivedArgs(client, request, _logger); + var eventArgs = new ClientUpgradeRequestReceivedArgs(flow.Client, flow.Request, Logger); await AsyncEventRaiser.RaiseAsync(ClientUpgradeRequestReceivedAsync, this, eventArgs, cancellationToken); if (eventArgs.Handle) { - // The client is accepted - await upgradeHandler.AcceptWebSocketAsync(request, eventArgs.ResponseContext, client.Id, null, cancellationToken); + // The event consumer accepted the client + await flow.AcceptWebSocketAsync(eventArgs.ResponseContext); - // Use the websocket for the client - client.UseWebSocket(upgradeHandler.CreateWebSocket(isServer: true)); - - clientAdded = ActiveClients.TryAdd(client.Id, client); - if (clientAdded) + if (ActiveClients.TryAdd(flow.Client.Id, flow.Client)) { - _ = Task.Run(() => ClientConnected?.Invoke(this, new ClientConnectedArgs(client.Id)), cancellationToken); + _ = Task.Run(() => ClientConnected?.Invoke(this, new ClientConnectedArgs(flow.Client.Id)), cancellationToken); // Start listening for messages - _logger?.LogDebug("Connection upgraded, now listening on client {clientId}", client.Id); - await ProcessWebSocketMessagesAsync(client, cancellationToken); + Logger?.LogDebug("Connection upgraded, now listening on Client {clientId}", flow.Client.Id); + await ProcessWebSocketMessagesAsync(flow.Client, cancellationToken); + clientHandled = true; } } else { // The client is rejected - _logger?.LogDebug("Client upgrade request rejected by ClientUpgradeRequestReceivedAsync event."); - await upgradeHandler.RejectWebSocketAsync(eventArgs.ResponseContext, cancellationToken); + Logger?.LogDebug("Client upgrade request rejected by ClientUpgradeRequestReceivedAsync event."); + await flow.RejectWebSocketAsync(eventArgs.ResponseContext); } } catch (OperationCanceledException) { // Ignore the exception, because it is thrown when cancellation is requested } + catch (UserNotHandledException userNotHandledException) + { + await flow.RejectWebSocketAsync(userNotHandledException.ResponseContext); + } catch (Exception exception) { - _logger?.LogError(exception, "Error while handling the client {clientId}", client.Id); + Logger?.LogError(exception, "Error while handling the Client {clientId}", flow.Client.Id); } finally { // 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 (clientAdded && !_serverShuttingDown) + if (clientHandled && !_serverShuttingDown) { - HandleDisconnectedClient(client); + flow.HandleDisconnectedClient(); } } } - /// - /// Handles the disconnected client. - /// - /// - private void HandleDisconnectedClient(WebSocketServerClient client) - { - ActiveClients.TryRemove(client.Id, out _); - client.Dispose(); - - if (_options.RememberDisconnectedClients) - { - _logger?.LogDebug("Client {clientId} is now a passive user.", client.Id); - PassiveClients.Add(client.Id, client); - } - else - { - _logger?.LogDebug("Client {clientId} is removed.", client.Id); - } - } - /// /// Processes the WebSocket messages. /// @@ -410,20 +348,20 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C { // Handle the text message string receivedMessage = Encoding.UTF8.GetString(buffer, 0, result.Count); - _logger?.LogDebug("Message received: {message}", receivedMessage); + Logger?.LogDebug("Message received: {message}", receivedMessage); _ = Task.Run(() => MessageReceived?.Invoke(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); + Logger?.LogDebug("Binary message received, length: {length} bytes", result.Count); _ = Task.Run(() => BinaryMessageReceived?.Invoke(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 && !_serverShuttingDown) { - _logger?.LogInformation("Received close message from client"); + Logger?.LogInformation("Received close message from Client"); _ = Task.Run(() => ClientDisconnected?.Invoke(this, new ClientDisconnectedArgs(result.CloseStatusDescription ?? string.Empty, client.Id)), cancellationToken); await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None); break; @@ -441,7 +379,7 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C /// The arguments of the event private void PassiveClients_ItemExpired(object? sender, ItemExpiredArgs e) { - _logger?.LogDebug("Passive client expired: {clientId}", e.Item.Id); + Logger?.LogDebug("Passive Client expired: {clientId}", e.Item.Id); // Raise the event asynchronously // We don't want to block the cleanup process diff --git a/Jung.SimpleWebSocket/UserNotHandledException.cs b/Jung.SimpleWebSocket/UserNotHandledException.cs new file mode 100644 index 0000000..2e510a2 --- /dev/null +++ b/Jung.SimpleWebSocket/UserNotHandledException.cs @@ -0,0 +1,13 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Jung.SimpleWebSocket.Models; + +namespace Jung.SimpleWebSocket +{ + [Serializable] + internal class UserNotHandledException(WebContext responseContext) : Exception + { + public WebContext ResponseContext { get; set; } = responseContext; + } +} \ No newline at end of file diff --git a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs index 3c064ef..416a4aa 100644 --- a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs +++ b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs @@ -141,7 +141,7 @@ private static void CompleteHeaderSection(StringBuilder sb) sb.Append("\r\n"); } - private void AddBody(WebContext context, StringBuilder sb) + private static void AddBody(WebContext context, StringBuilder sb) { sb.Append(context.BodyContent); } @@ -201,7 +201,7 @@ internal static bool ProcessWebSocketProtocolHeader(string? clientSecWebSocketPr return true; } } - throw new WebSocketUpgradeException($"The WebSocket _client requested the following protocols: '{clientSecWebSocketProtocol}', but the server accepted '{subProtocol}' protocol(s)."); + throw new WebSocketUpgradeException($"The WebSocket Client requested the following protocols: '{clientSecWebSocketProtocol}', but the server accepted '{subProtocol}' protocol(s)."); } internal async Task SendUpgradeRequestAsync(WebContext requestContext, CancellationToken token) From aab5c8ab81a35f7341ed01718e6cb0a2e54438a4 Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Tue, 1 Oct 2024 23:10:04 +0200 Subject: [PATCH 08/27] Improve ClientHandlingFlow Add helper method for raising events in a new task Change some event signatures --- Jung.SimpleWebSocket/ClientHandlingFlow.cs | 82 +++++++++++++++---- .../Contracts/IWebSocketServer.cs | 10 +-- Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 44 +++++----- .../Utility/AsyncEventRaiser.cs | 27 +++++- 4 files changed, 121 insertions(+), 42 deletions(-) diff --git a/Jung.SimpleWebSocket/ClientHandlingFlow.cs b/Jung.SimpleWebSocket/ClientHandlingFlow.cs index 221e78a..11cde12 100644 --- a/Jung.SimpleWebSocket/ClientHandlingFlow.cs +++ b/Jung.SimpleWebSocket/ClientHandlingFlow.cs @@ -1,8 +1,12 @@ // This file is part of the Jung SimpleWebSocket project. // The project is licensed under the MIT license. +using Jung.SimpleWebSocket.Delegates; using Jung.SimpleWebSocket.Models; +using Jung.SimpleWebSocket.Models.EventArguments; +using Jung.SimpleWebSocket.Utility; using Microsoft.Extensions.Logging; +using System; using System.Collections.Concurrent; using System.Net; @@ -34,6 +38,21 @@ internal class ClientHandlingFlow(SimpleWebSocketServer server, WebSocketServerC /// private WebSocketUpgradeHandler _upgradeHandler = null!; + /// + /// Gets the response context that is being use to response to the client. + /// + private WebContext _responseContext = null!; + + /// + /// Gets a value indicating whether the client was accepted. + /// + private bool _clientAccepted; + + /// + /// Gets a value indicating whether the client was a passive client. + /// + private bool _clientWasPassiveClient; + /// /// Gets the options of the server. /// @@ -103,7 +122,12 @@ internal void HandleClientIdentification() var passiveClient = _passiveClients[Request.UserId]; passiveClient.UpdateClient(Client.ClientConnection!); Client = passiveClient; - _passiveClients.Remove(Request.UserId); + var clientRemoved = _passiveClients.Remove(Request.UserId); + + // Set the flag that the client was a passive client + // This should only be set if the client was removed in this specific flow + // Otherwise its possible that the client is handled twice + _clientWasPassiveClient = clientRemoved; } else { @@ -140,14 +164,17 @@ private void ThrowForUserAlreadyConnected() /// /// Accepts the websocket connection. /// - /// The response context to send to the client. - internal async Task AcceptWebSocketAsync(WebContext responseContext) + internal async Task AcceptWebSocketAsync() { // The client is accepted - await _upgradeHandler.AcceptWebSocketAsync(Request, responseContext, Client.Id, null, _cancellationToken); + await _upgradeHandler.AcceptWebSocketAsync(Request, _responseContext, Client.Id, null, _cancellationToken); // Use the websocket for the client Client.UseWebSocket(_upgradeHandler.CreateWebSocket(isServer: true)); + + // Set the flag that the client was accepted + // This is used to determine if the client should be added to the passive clients after disconnect + _clientAccepted = true; } /// @@ -164,21 +191,46 @@ internal async Task RejectWebSocketAsync(WebContext responseContext) /// internal void HandleDisconnectedClient() { - lock (_clientLock) + if (_clientWasPassiveClient || _clientAccepted) { - _activeClients.TryRemove(Client.Id, out _); - Client.Dispose(); - - if (_options.RememberDisconnectedClients) - { - _logger?.LogDebug("Client {clientId} is now a passive user.", Client.Id); - _passiveClients.Add(Client.Id, Client); - } - else + lock (_clientLock) { - _logger?.LogDebug("Client {clientId} is removed.", Client.Id); + _activeClients.TryRemove(Client.Id, out _); + Client.Dispose(); + + if (_options.RememberDisconnectedClients) + { + _logger?.LogDebug("Client {clientId} is now a passive user.", Client.Id); + _passiveClients.Add(Client.Id, Client); + } + else + { + _logger?.LogDebug("Client {clientId} is removed.", Client.Id); + } } } } + + /// + /// Raises the upgrade event. + /// + /// The event handler for the upgrade request. + /// The event arguments of the upgrade request. + internal async Task RaiseUpgradeEventAsync(AsyncEventHandler? clientUpgradeRequestReceivedAsync) + { + var eventArgs = new ClientUpgradeRequestReceivedArgs(Client, Request, _logger); + await AsyncEventRaiser.RaiseAsync(clientUpgradeRequestReceivedAsync, server, eventArgs, _cancellationToken); + _responseContext = eventArgs.ResponseContext; + return eventArgs; + } + + /// + /// Tries to add the client to the active user list. + /// + /// True if the client was added to the active user list. False if the client is already connected. + internal bool TryAddClientToActiveUserList() + { + return _activeClients.TryAdd(Client.Id, Client); + } } } \ No newline at end of file diff --git a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs b/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs index 464198f..5add6a8 100644 --- a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs +++ b/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs @@ -42,27 +42,27 @@ public interface IWebSocketServer : IDisposable /// /// Event that is raised when a client is connected. /// - event ClientConnectedEventHandler? ClientConnected; + event EventHandler? ClientConnected; /// /// Event that is raised when a client is disconnected. /// - event ClientDisconnectedEventHandler ClientDisconnected; + event EventHandler? ClientDisconnected; /// /// Event that is raised when a message is received from a client. /// - event ClientMessageReceivedEventHandler? MessageReceived; + event EventHandler? MessageReceived; /// /// Event that is raised when a binary message is received from a client. /// - event ClientBinaryMessageReceivedEventHandler? BinaryMessageReceived; + event EventHandler? BinaryMessageReceived; /// /// Occurs when an passive user expired. /// - event PassiveUserExpiredEventHandler? PassiveUserExpiredEvent; + event EventHandler? PassiveUserExpiredEvent; /// /// Async Event that is raised when a client upgrade request is received. diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index 2634e70..7e1d652 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -29,15 +29,15 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable public int Port { get; } /// - public event ClientConnectedEventHandler? ClientConnected; + public event EventHandler? ClientConnected; /// - public event ClientDisconnectedEventHandler? ClientDisconnected; + public event EventHandler? ClientDisconnected; /// - public event ClientMessageReceivedEventHandler? MessageReceived; + public event EventHandler? MessageReceived; /// - public event ClientBinaryMessageReceivedEventHandler? BinaryMessageReceived; + public event EventHandler? BinaryMessageReceived; /// - public event PassiveUserExpiredEventHandler? PassiveUserExpiredEvent; + public event EventHandler? PassiveUserExpiredEvent; /// public event AsyncEventHandler? ClientUpgradeRequestReceivedAsync; @@ -265,7 +265,6 @@ public WebSocketServerClient GetClientById(string clientId) private async Task HandleClientAsync(WebSocketServerClient client, CancellationToken cancellationToken) { var flow = new ClientHandlingFlow(this, client, cancellationToken); - var clientHandled = false; try { // Load the request context @@ -275,25 +274,28 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT flow.HandleClientIdentification(); // raise async client upgrade request received event - var eventArgs = new ClientUpgradeRequestReceivedArgs(flow.Client, flow.Request, Logger); - await AsyncEventRaiser.RaiseAsync(ClientUpgradeRequestReceivedAsync, this, eventArgs, cancellationToken); + var eventArgs = await flow.RaiseUpgradeEventAsync(ClientUpgradeRequestReceivedAsync); + + // Respond to the upgrade request if (eventArgs.Handle) { - // The event consumer accepted the client - await flow.AcceptWebSocketAsync(eventArgs.ResponseContext); - - if (ActiveClients.TryAdd(flow.Client.Id, flow.Client)) + // Accept the WebSocket connection + await flow.AcceptWebSocketAsync(); + if (flow.TryAddClientToActiveUserList()) { - _ = Task.Run(() => ClientConnected?.Invoke(this, new ClientConnectedArgs(flow.Client.Id)), cancellationToken); - // Start listening for messages 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); - clientHandled = true; + } + else + { + Logger?.LogDebug("Connection upgraded, now listening on Client {clientId}", flow.Client.Id); } } else { - // The client is rejected + // Reject the WebSocket connection Logger?.LogDebug("Client upgrade request rejected by ClientUpgradeRequestReceivedAsync event."); await flow.RejectWebSocketAsync(eventArgs.ResponseContext); } @@ -314,7 +316,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 (clientHandled && !_serverShuttingDown) + if (!_serverShuttingDown) { flow.HandleDisconnectedClient(); } @@ -349,20 +351,20 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C // 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 ClientMessageReceivedArgs(receivedMessage, client.Id)), cancellationToken); + 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); - _ = Task.Run(() => BinaryMessageReceived?.Invoke(this, new ClientBinaryMessageReceivedArgs(buffer[..result.Count], client.Id)), cancellationToken); + 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 && !_serverShuttingDown) { Logger?.LogInformation("Received close message from Client"); - _ = Task.Run(() => ClientDisconnected?.Invoke(this, new ClientDisconnectedArgs(result.CloseStatusDescription ?? string.Empty, client.Id)), cancellationToken); + AsyncEventRaiser.RaiseAsyncInNewTask(ClientDisconnected, this, new ClientDisconnectedArgs(result.CloseStatusDescription ?? string.Empty, client.Id), cancellationToken); await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None); break; } @@ -383,7 +385,7 @@ private void PassiveClients_ItemExpired(object? sender, ItemExpiredArgs PassiveUserExpiredEvent?.Invoke(this, new PassiveUserExpiredArgs(e.Item.Id))); + AsyncEventRaiser.RaiseAsyncInNewTask(PassiveUserExpiredEvent, this, new PassiveUserExpiredArgs(e.Item.Id), _cancellationTokenSource.Token); } /// diff --git a/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs b/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs index b81a851..91e76ae 100644 --- a/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs +++ b/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs @@ -47,5 +47,30 @@ internal static async Task RaiseAsync(AsyncEventHandler? } } } + + /// + /// Helper method to raise an async event. + /// + /// The type of the event arguments. + /// The event handler + /// The sender of the event. + /// The event arguments. + /// The cancellation token. + /// A task that represents the asynchronous operation. + internal static void RaiseAsyncInNewTask(EventHandler? @event, object sender, TEventArgs e, CancellationToken cancellationToken) where TEventArgs : class + { + if (@event != null) + { + var invocationList = @event.GetInvocationList(); + + foreach (var handler in invocationList) + { + var asyncHandler = (EventHandler)handler; + + // Execute directly if there's no synchronization context + _ = Task.Run(() => asyncHandler(sender, e), cancellationToken); + } + } + } } -} +} \ No newline at end of file From 3fa7413ca4f16347eddbe883b6244679ac11a858 Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Sun, 6 Oct 2024 20:01:18 +0200 Subject: [PATCH 09/27] Change the RaiseAsyncInNewTask method to call the handlers from the invoker list one after the other and not in parallel --- Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs b/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs index 91e76ae..a084e00 100644 --- a/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs +++ b/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs @@ -49,7 +49,7 @@ internal static async Task RaiseAsync(AsyncEventHandler? } /// - /// Helper method to raise an async event. + /// Helper method to raise an Event in a new Task. /// /// The type of the event arguments. /// The event handler @@ -62,14 +62,14 @@ internal static void RaiseAsyncInNewTask(EventHandler? @ if (@event != null) { var invocationList = @event.GetInvocationList(); - - foreach (var handler in invocationList) + Task.Run(() => { - var asyncHandler = (EventHandler)handler; - - // Execute directly if there's no synchronization context - _ = Task.Run(() => asyncHandler(sender, e), cancellationToken); - } + foreach (var handler in invocationList) + { + var handle = (EventHandler)handler; + handle(sender, e); + } + }, cancellationToken); } } } From e9c065e2def7c6ad23bbbb4e51925544200286ce Mon Sep 17 00:00:00 2001 From: Christoph Date: Tue, 28 Jan 2025 21:44:41 +0100 Subject: [PATCH 10/27] Add client handling flow tests --- .../UserNotHandledException.cs | 2 +- .../{ => Flows}/ClientHandlingFlow.cs | 1 + Jung.SimpleWebSocket/Models/WebContext.cs | 1 - .../Models/WebSocketServerClient.cs | 9 ++ Jung.SimpleWebSocket/docs/README.md | 9 +- .../ClientHandlingFlowTest.cs | 103 ++++++++++++++++++ .../Mock/ILoggerMockHelper.cs | 39 +++++++ .../Mock/LoggerMessages.cs | 25 +++++ .../SimpleWebSocketTest.cs | 59 +++------- 9 files changed, 200 insertions(+), 48 deletions(-) rename Jung.SimpleWebSocket/{ => Exceptions}/UserNotHandledException.cs (89%) rename Jung.SimpleWebSocket/{ => Flows}/ClientHandlingFlow.cs (99%) create mode 100644 Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs create mode 100644 Jung.SimpleWebSocketTest/Mock/ILoggerMockHelper.cs create mode 100644 Jung.SimpleWebSocketTest/Mock/LoggerMessages.cs diff --git a/Jung.SimpleWebSocket/UserNotHandledException.cs b/Jung.SimpleWebSocket/Exceptions/UserNotHandledException.cs similarity index 89% rename from Jung.SimpleWebSocket/UserNotHandledException.cs rename to Jung.SimpleWebSocket/Exceptions/UserNotHandledException.cs index 2e510a2..148c6f7 100644 --- a/Jung.SimpleWebSocket/UserNotHandledException.cs +++ b/Jung.SimpleWebSocket/Exceptions/UserNotHandledException.cs @@ -3,7 +3,7 @@ using Jung.SimpleWebSocket.Models; -namespace Jung.SimpleWebSocket +namespace Jung.SimpleWebSocket.Exceptions { [Serializable] internal class UserNotHandledException(WebContext responseContext) : Exception diff --git a/Jung.SimpleWebSocket/ClientHandlingFlow.cs b/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs similarity index 99% rename from Jung.SimpleWebSocket/ClientHandlingFlow.cs rename to Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs index 11cde12..c688226 100644 --- a/Jung.SimpleWebSocket/ClientHandlingFlow.cs +++ b/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs @@ -2,6 +2,7 @@ // The project is licensed under the MIT license. using Jung.SimpleWebSocket.Delegates; +using Jung.SimpleWebSocket.Exceptions; using Jung.SimpleWebSocket.Models; using Jung.SimpleWebSocket.Models.EventArguments; using Jung.SimpleWebSocket.Utility; diff --git a/Jung.SimpleWebSocket/Models/WebContext.cs b/Jung.SimpleWebSocket/Models/WebContext.cs index 04173cf..3b7626c 100644 --- a/Jung.SimpleWebSocket/Models/WebContext.cs +++ b/Jung.SimpleWebSocket/Models/WebContext.cs @@ -331,7 +331,6 @@ internal IEnumerable GetAllHeaderValues(string headerName) } } - /// /// Gets the concatenated headers of the web request. /// diff --git a/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs b/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs index 438a62f..e3a40d9 100644 --- a/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs +++ b/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs @@ -60,6 +60,15 @@ internal WebSocketServerClient(ITcpClient clientConnection) ClientConnection = clientConnection; } + /// + /// Constructor for unit tests + /// + /// The time the user was first seen. + internal WebSocketServerClient(DateTime firstSeen) + { + FirstSeen = LastConnectionTimestamp = firstSeen; + } + /// /// Updates the WebSocket client with a new connection. /// diff --git a/Jung.SimpleWebSocket/docs/README.md b/Jung.SimpleWebSocket/docs/README.md index 7d712e8..a0c5979 100644 --- a/Jung.SimpleWebSocket/docs/README.md +++ b/Jung.SimpleWebSocket/docs/README.md @@ -1,18 +1,19 @@ # Jung.SimpleWebSocket -Jung.SimpleWebSocket is a lightweight and easy-to-use library for working with WebSocket connections in .NET. +Jung.SimpleWebSocket is library for working with WebSocket connections in .NET. It is built on top of the `System.Net.WebSockets` namespace and provides a simple API for creating WebSocket clients and servers. -By using a TcpListener and TcpClient, Jung.SimpleWebSocket is able to handle WebSocket connections without the need for a full-fledged HTTP server. -You also don't need admin rights to run the server. +The library is designed to give you full access to the WebSocket connection process. +You also don't need admin rights to bind the server to a port, as the library uses the `HttpListener` class to listen for incoming WebSocket connections. ## Installation -You can install Jung.SimpleWebSocket via NuGet package manager or by manually downloading the library. +You can install Jung.SimpleWebSocket via NuGet package manager or by manually downloading and building the library. ### NuGet Package Manager 1. Open the NuGet Package Manager Console in Visual Studio. 2. Run the following command to install the package: `Install-Package Jung.SimpleWebSocket`. + ### Manual Download 1. Go to the [Jung.SimpleWebSocket GitHub repository](https://github.com/cjung95/SimpleWebSocket). diff --git a/Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs b/Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs new file mode 100644 index 0000000..e70360a --- /dev/null +++ b/Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs @@ -0,0 +1,103 @@ +using Jung.SimpleWebSocket; +using Jung.SimpleWebSocket.Contracts; +using Jung.SimpleWebSocket.Exceptions; +using Jung.SimpleWebSocket.Models; +using Jung.SimpleWebSocketTest.Mock; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using System.Text; + +namespace Jung.SimpleWebSocketTest +{ + [TestFixture] + internal class ClientHandlingFlowTest + { + private ILogger _logger; + + [SetUp] + public void SetUp() + { + var loggerHelper = new ILoggerMockHelper("Server"); + _logger = loggerHelper.Logger; + } + + private ClientHandlingFlow SetupClientHandlingFlow(object serverOptions, List? activeClients = null) + { + var tcpListener = new Mock(); + var serverMoq = new Mock(serverOptions, tcpListener.Object, _logger); + if (activeClients != null) + { + foreach (var client in activeClients) + { + serverMoq.Object.ActiveClients.TryAdd(client.Id, client); + } + } + + var tcpClientMoq = new Mock(); + var serverClientMoq = new WebSocketServerClient(tcpClientMoq.Object); + + return new ClientHandlingFlow(serverMoq.Object, serverClientMoq, CancellationToken.None); + } + + private string CreateUpgradeRequest(string? userId) + { + var sb = new StringBuilder(); + sb.Append("GET /chat HTTP/1.1\r\n" + + "Host: localhost:8080\r\n" + + "Upgrade: websocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n" + + "Sec-WebSocket-Version: 13\r\n"); + + if (!string.IsNullOrEmpty(userId)) + { + sb.Append("x-user-id: 6C8D0844-D84F-4AD9-B28D-23B3940887B7"); + } + + sb.Append("\r\n\r\n"); + return sb.ToString(); + } + + + [Test] + public void HandleClientIdentification_NoNewUser_UserIdIsUpdated() + { + var userId = "6C8D0844-D84F-4AD9-B28D-23B3940887B7"; + var requestText = CreateUpgradeRequest(userId); + var serverOptions = new SimpleWebSocketServerOptions + { + RememberDisconnectedClients = true, + }; + + var clientHandlingFlow = SetupClientHandlingFlow(serverOptions); + + clientHandlingFlow.Request = new WebContext(requestText); + clientHandlingFlow.HandleClientIdentification(); + + Assert.That(clientHandlingFlow.Client.Id, Is.EqualTo(userId)); + } + + [Test] + public void HandleClientIdentification_NoNewUser_UserAlreadyConnected() + { + // setup + var userId = "6C8D0844-D84F-4AD9-B28D-23B3940887B7"; + var activeUsers = new List(); + var client = new WebSocketServerClient(DateTime.Now); + client.UpdateId(userId); + activeUsers.Add(client); + var requestText = CreateUpgradeRequest(userId); + var serverOptions = new SimpleWebSocketServerOptions + { + RememberDisconnectedClients = true, + }; + + // act and assert + var clientHandlingFlow = SetupClientHandlingFlow(serverOptions, activeUsers); + + clientHandlingFlow.Request = new WebContext(requestText); + Assert.That(() => clientHandlingFlow.HandleClientIdentification(), Throws.Exception.TypeOf()); + } + } +} diff --git a/Jung.SimpleWebSocketTest/Mock/ILoggerMockHelper.cs b/Jung.SimpleWebSocketTest/Mock/ILoggerMockHelper.cs new file mode 100644 index 0000000..e72d2fa --- /dev/null +++ b/Jung.SimpleWebSocketTest/Mock/ILoggerMockHelper.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.Logging; +using Moq; + +namespace Jung.SimpleWebSocketTest.Mock +{ + internal class ILoggerMockHelper where T : class + { + internal Mock> LoggerMock { get; } + internal ILogger Logger => LoggerMock.Object; + + public ILoggerMockHelper(string name) + { + LoggerMock = new Mock>(); + ILoggerMockHelper.SetUpLogger(LoggerMock, name); + } + + private static void SetUpLogger(Mock> mock, string loggerName) + { + mock.Setup(m => m.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>()! + )).Callback(new InvocationAction(invocation => + { + var logLevel = (LogLevel)invocation.Arguments[0]; + var eventId = (EventId)invocation.Arguments[1]; + var state = invocation.Arguments[2]; + var exception = (Exception)invocation.Arguments[3]; + var formatter = invocation.Arguments[4]; + + var invokeMethod = formatter.GetType().GetMethod("Invoke"); + var logMessage = invokeMethod!.Invoke(formatter, [state, exception]); + LoggerMessages.AddMessage($"[{DateTime.Now:HH:mm:ss:fff}] {loggerName} ({logLevel}): {logMessage}"); + })); + } + } +} diff --git a/Jung.SimpleWebSocketTest/Mock/LoggerMessages.cs b/Jung.SimpleWebSocketTest/Mock/LoggerMessages.cs new file mode 100644 index 0000000..b5cc9b3 --- /dev/null +++ b/Jung.SimpleWebSocketTest/Mock/LoggerMessages.cs @@ -0,0 +1,25 @@ + +namespace Jung.SimpleWebSocketTest.Mock +{ + internal static class LoggerMessages + { + private static readonly object _lock = new object(); + internal static List Messages { get; } = []; + + internal static void AddMessage(string message) + { + lock (_lock) + { + Messages.Add(message); + } + } + + internal static string[] GetMessages() + { + lock (_lock) + { + return [.. Messages]; + } + } + } +} diff --git a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs index a4209dc..e86696c 100644 --- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs +++ b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs @@ -3,8 +3,7 @@ using Jung.SimpleWebSocket; using Jung.SimpleWebSocket.Models; -using Microsoft.Extensions.Logging; -using Moq; +using Jung.SimpleWebSocketTest.Mock; using NUnit.Framework; using System.Diagnostics; using System.Net; @@ -18,9 +17,8 @@ namespace Jung.SimpleWebSocketTest [TestFixture] public class SimpleWebSocketTest { - private readonly List _logMessages = []; - private Mock> _serverLogger; - private Mock> _clientLogger; + private ILoggerMockHelper _serverLoggerMockHelper; + private ILoggerMockHelper _clientLoggerMockHelper; [OneTimeSetUp] public void SetUpOnce() @@ -31,11 +29,8 @@ public void SetUpOnce() [SetUp] public void SetUp() { - _logMessages.Clear(); - _serverLogger = new Mock>(); - _clientLogger = new Mock>(); - SetUpLogger(_serverLogger, "Server"); - SetUpLogger(_clientLogger, "Client"); + _serverLoggerMockHelper = new("Server"); + _clientLoggerMockHelper = new("Client"); } [OneTimeTearDown] @@ -44,27 +39,7 @@ public void EndTest() Trace.Flush(); } - private void SetUpLogger(Mock> mock, string loggerName) - { - mock.Setup(m => m.Log( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>()! - )).Callback(new InvocationAction(invocation => - { - var logLevel = (LogLevel)invocation.Arguments[0]; - var eventId = (EventId)invocation.Arguments[1]; - var state = invocation.Arguments[2]; - var exception = (Exception)invocation.Arguments[3]; - var formatter = invocation.Arguments[4]; - - var invokeMethod = formatter.GetType().GetMethod("Invoke"); - var logMessage = invokeMethod!.Invoke(formatter, [state, exception]); - _logMessages.Add($"[{DateTime.Now:HH:mm:ss:fff}] {loggerName} ({logLevel}): {logMessage}"); - })); - } + [Test] [Platform("Windows7,Windows8,Windows8.1,Windows10", Reason = "This test establishes a TCP client-server connection using SimpleWebSocket, which relies on specific networking features and behaviors that are only available and consistent on Windows platforms. Running this test on non-Windows platforms could lead to inconsistent results or failures due to differences in networking stack implementations.")] @@ -78,8 +53,8 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() RememberDisconnectedClients = true, }; - using var server = new SimpleWebSocketServer(serverOptions, _serverLogger.Object); - using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", logger: _clientLogger.Object); + using var server = new SimpleWebSocketServer(serverOptions, _serverLoggerMockHelper.Logger); + using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", logger: _clientLoggerMockHelper.Logger); const string Message = "Hello World"; @@ -148,14 +123,14 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() WaitForManualResetEventOrThrow(disconnectResetEvent); // test if the server accepts the client again - var client2 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", client.UserId, logger: _clientLogger.Object); + var client2 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", client.UserId, logger: _clientLoggerMockHelper.Logger); await client2.ConnectAsync(); await Task.Delay(100); try { // test if two clients with the same user id can connect - var client3 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", client.UserId, logger: _clientLogger.Object); + var client3 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", client.UserId, logger: _clientLoggerMockHelper.Logger); await client3.ConnectAsync(); } catch (Exception exception) @@ -166,7 +141,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() await client2.SendMessageAsync("Hello World"); await server.ShutdownServer(CancellationToken.None); - _logMessages.ForEach(m => Trace.WriteLine(m)); + Array.ForEach(LoggerMessages.GetMessages(), m => Trace.WriteLine(m)); // Assert Assert.Multiple(() => @@ -206,8 +181,8 @@ public async Task TestClientServerConnection_ShouldRemoveClientFromPassiveClient PassiveClientLifetime = TimeSpan.FromSeconds(1) }; - using var server = new SimpleWebSocketServer(serverOptions, _serverLogger.Object); - using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", userId, _clientLogger.Object); + using var server = new SimpleWebSocketServer(serverOptions, _serverLoggerMockHelper.Logger); + using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", userId, _clientLoggerMockHelper.Logger); var expiredClientId = string.Empty; var expiredClientResetEvent = new ManualResetEvent(false); @@ -227,7 +202,7 @@ public async Task TestClientServerConnection_ShouldRemoveClientFromPassiveClient WaitForManualResetEventOrThrow(expiredClientResetEvent, 2000); await server.ShutdownServer(CancellationToken.None); - _logMessages.ForEach(m => Trace.WriteLine(m)); + Array.ForEach(LoggerMessages.GetMessages(), m => Trace.WriteLine(m)); // Assert Assert.That(expiredClientId, Is.EqualTo(userId)); @@ -244,8 +219,8 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld2() Port = 8010 }; - using var server = new SimpleWebSocketServer(serverOptions, _serverLogger.Object); - using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", logger: _clientLogger.Object); + using var server = new SimpleWebSocketServer(serverOptions, _serverLoggerMockHelper.Logger); + using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", logger: _clientLoggerMockHelper.Logger); const string Message = "Hello World"; @@ -292,7 +267,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld2() await server.ShutdownServer(CancellationToken.None); WaitForManualResetEventOrThrow(disconnectResetEvent, 100); - _logMessages.ForEach(m => Trace.WriteLine(m)); + Array.ForEach(LoggerMessages.GetMessages(), m => Trace.WriteLine(m)); // Assert Assert.Multiple(() => From 4d97a19cb41c7504e5f4482f4bd19065915f750d Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 29 Jan 2025 17:38:27 +0100 Subject: [PATCH 11/27] Rename tcp listener field from _server to _tcpListener for better unterstading of its purpose Remove unnecessary parameter from WebSocketServerClient consturctor Improve client handling flow tests --- .../Flows/ClientHandlingFlow.cs | 8 +++---- .../Models/WebSocketServerClient.cs | 13 ++++++------ Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 21 ++++++++++--------- .../ClientHandlingFlowTest.cs | 15 ++++++++----- 4 files changed, 31 insertions(+), 26 deletions(-) diff --git a/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs b/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs index c688226..ad5e3ab 100644 --- a/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs +++ b/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs @@ -11,7 +11,7 @@ using System.Collections.Concurrent; using System.Net; -namespace Jung.SimpleWebSocket +namespace Jung.SimpleWebSocket.Flows { /// /// A flow that handles the client connection. @@ -163,14 +163,14 @@ private void ThrowForUserAlreadyConnected() } /// - /// Accepts the websocket connection. + /// Accepts the web socket connection. /// internal async Task AcceptWebSocketAsync() { // The client is accepted await _upgradeHandler.AcceptWebSocketAsync(Request, _responseContext, Client.Id, null, _cancellationToken); - // Use the websocket for the client + // Use the web socket for the client Client.UseWebSocket(_upgradeHandler.CreateWebSocket(isServer: true)); // Set the flag that the client was accepted @@ -179,7 +179,7 @@ internal async Task AcceptWebSocketAsync() } /// - /// Rejects the websocket connection. + /// Rejects the web socket connection. /// /// The response context to send to the client. internal async Task RejectWebSocketAsync(WebContext responseContext) diff --git a/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs b/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs index e3a40d9..f48c7c3 100644 --- a/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs +++ b/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs @@ -54,19 +54,18 @@ public class WebSocketServerClient : IDisposable /// /// The connection of the client. internal WebSocketServerClient(ITcpClient clientConnection) + : base() { - FirstSeen = DateTime.UtcNow; - LastConnectionTimestamp = FirstSeen; ClientConnection = clientConnection; } /// - /// Constructor for unit tests + /// Initializes a new instance of the class without a client connection. /// - /// The time the user was first seen. - internal WebSocketServerClient(DateTime firstSeen) + internal WebSocketServerClient() { - FirstSeen = LastConnectionTimestamp = firstSeen; + FirstSeen = DateTime.UtcNow; + LastConnectionTimestamp = FirstSeen; } /// @@ -82,7 +81,7 @@ internal void UpdateClient(ITcpClient client) /// /// Updates the client with a new WebSocket. /// - /// The websocket that the client should use. + /// The web socket that the client should use. internal void UseWebSocket(IWebSocket? webSocket) { ArgumentNullException.ThrowIfNull(webSocket); diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index 7e1d652..13d28b9 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -4,6 +4,7 @@ using Jung.SimpleWebSocket.Contracts; using Jung.SimpleWebSocket.Delegates; using Jung.SimpleWebSocket.Exceptions; +using Jung.SimpleWebSocket.Flows; using Jung.SimpleWebSocket.Models; using Jung.SimpleWebSocket.Models.EventArguments; using Jung.SimpleWebSocket.Utility; @@ -59,7 +60,7 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable public int ClientCount => ActiveClients.Count; /// - public bool IsListening => _server?.IsListening ?? false; + public bool IsListening => _tcpListener?.IsListening ?? false; /// /// A logger to write internal log messages. @@ -89,7 +90,7 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// /// The server that listens for incoming connection attempts. /// - private ITcpListener? _server; + private ITcpListener? _tcpListener; /// /// Initializes a new instance of the class that listens @@ -153,7 +154,7 @@ public SimpleWebSocketServer(IOptions options, ILo internal SimpleWebSocketServer(SimpleWebSocketServerOptions options, ITcpListener tcpListener, ILogger? logger = null) : this(options, logger) { - _server = tcpListener; + _tcpListener = tcpListener; } /// @@ -166,8 +167,8 @@ public void Start(CancellationToken? cancellationToken = null) _cancellationTokenSource = new CancellationTokenSource(); var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value, _cancellationTokenSource.Token); - _server ??= new TcpListenerWrapper(LocalIpAddress, Port); - _server.Start(); + _tcpListener ??= new TcpListenerWrapper(LocalIpAddress, Port); + _tcpListener.Start(); _ = Task.Run(async delegate { Logger?.LogInformation("Server started at {LocalIpAddress}:{Port}", LocalIpAddress, Port); @@ -176,7 +177,7 @@ public void Start(CancellationToken? cancellationToken = null) try { // Accept the client - var client = await _server.AcceptTcpClientAsync(linkedTokenSource.Token); + var client = await _tcpListener.AcceptTcpClientAsync(linkedTokenSource.Token); Logger?.LogDebug("Client connected from {endpoint}", client.ClientConnection!.RemoteEndPoint); @@ -219,8 +220,8 @@ public async Task ShutdownServer(CancellationToken? cancellationToken = null) } _cancellationTokenSource?.Cancel(); - _server?.Dispose(); - _server = null; + _tcpListener?.Dispose(); + _tcpListener = null; Logger?.LogInformation("Server stopped"); } @@ -392,8 +393,8 @@ private void PassiveClients_ItemExpired(object? sender, ItemExpiredArgs(); - var client = new WebSocketServerClient(DateTime.Now); + + var client = new WebSocketServerClient(); client.UpdateId(userId); activeUsers.Add(client); + var requestText = CreateUpgradeRequest(userId); var serverOptions = new SimpleWebSocketServerOptions { From 8902f123587030da8467bfac884a2f73852e16aa Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 1 Feb 2025 21:25:28 +0100 Subject: [PATCH 12/27] Remove code that handles user remembering --- .../Contracts/IWebSocketClient.cs | 5 - .../Contracts/IWebSocketServer.cs | 6 - .../PassiveUserExpiredEventHandler.cs | 13 - .../Flows/ClientHandlingFlow.cs | 118 +------- .../PassiveUserExpiredEventArgs.cs | 10 - .../Models/SimpleWebSocketServerOptions.cs | 31 +- Jung.SimpleWebSocket/Models/WebContext.cs | 28 +- Jung.SimpleWebSocket/SimpleWebSocketClient.cs | 13 +- Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 86 +----- .../Utility/ExpiringDictionary.cs | 281 ------------------ .../Wrappers/WebSocketUpgradeHandler.cs | 3 +- .../ClientHandlingFlowTest.cs | 53 +--- .../SimpleWebSocketTest.cs | 60 +--- .../WebSocketUpgradeHandlerTests.cs | 4 +- 14 files changed, 32 insertions(+), 679 deletions(-) delete mode 100644 Jung.SimpleWebSocket/Delegates/PassiveUserExpiredEventHandler.cs delete mode 100644 Jung.SimpleWebSocket/Models/EventArguments/PassiveUserExpiredEventArgs.cs delete mode 100644 Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs diff --git a/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs b/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs index 5bad1b4..1a73240 100644 --- a/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs +++ b/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs @@ -30,11 +30,6 @@ public interface IWebSocketClient : IDisposable /// bool IsConnected { get; } - /// - /// The user id of the client. If not set, the server did not sent a user id at websocket upgrade. - /// - string? UserId { get; } - /// /// Event that is raised when a message is received from a client. /// diff --git a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs b/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs index 5add6a8..f4973a2 100644 --- a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs +++ b/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs @@ -4,7 +4,6 @@ using Jung.SimpleWebSocket.Delegates; using Jung.SimpleWebSocket.Models; using Jung.SimpleWebSocket.Models.EventArguments; -using Jung.SimpleWebSocket.Utility; using System.Net; namespace Jung.SimpleWebSocket.Contracts; @@ -59,11 +58,6 @@ public interface IWebSocketServer : IDisposable /// event EventHandler? BinaryMessageReceived; - /// - /// Occurs when an passive user expired. - /// - event EventHandler? PassiveUserExpiredEvent; - /// /// Async Event that is raised when a client upgrade request is received. /// diff --git a/Jung.SimpleWebSocket/Delegates/PassiveUserExpiredEventHandler.cs b/Jung.SimpleWebSocket/Delegates/PassiveUserExpiredEventHandler.cs deleted file mode 100644 index de46577..0000000 --- a/Jung.SimpleWebSocket/Delegates/PassiveUserExpiredEventHandler.cs +++ /dev/null @@ -1,13 +0,0 @@ -// This file is part of the Jung SimpleWebSocket project. -// The project is licensed under the MIT license. - -using Jung.SimpleWebSocket.Models.EventArguments; - -namespace Jung.SimpleWebSocket.Delegates; - -/// -/// The event handler for the passive user expired event. -/// -/// The sender of the event. -/// The arguments of the event. -public delegate void PassiveUserExpiredEventHandler(object sender, PassiveUserExpiredArgs e); \ No newline at end of file diff --git a/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs b/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs index ad5e3ab..e41e93e 100644 --- a/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs +++ b/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs @@ -2,14 +2,11 @@ // The project is licensed under the MIT license. using Jung.SimpleWebSocket.Delegates; -using Jung.SimpleWebSocket.Exceptions; using Jung.SimpleWebSocket.Models; using Jung.SimpleWebSocket.Models.EventArguments; using Jung.SimpleWebSocket.Utility; using Microsoft.Extensions.Logging; -using System; using System.Collections.Concurrent; -using System.Net; namespace Jung.SimpleWebSocket.Flows { @@ -44,31 +41,11 @@ internal class ClientHandlingFlow(SimpleWebSocketServer server, WebSocketServerC /// private WebContext _responseContext = null!; - /// - /// Gets a value indicating whether the client was accepted. - /// - private bool _clientAccepted; - - /// - /// Gets a value indicating whether the client was a passive client. - /// - private bool _clientWasPassiveClient; - - /// - /// Gets the options of the server. - /// - private readonly SimpleWebSocketServerOptions _options = server.Options; - /// /// Gets the active clients of the server. /// private readonly ConcurrentDictionary _activeClients = server.ActiveClients; - /// - /// Gets the passive clients of the server. - /// - private readonly IDictionary _passiveClients = server.PassiveClients; - /// /// Gets the logger of the server. /// @@ -94,88 +71,16 @@ internal async Task LoadRequestContext() Request = await _upgradeHandler.AwaitContextAsync(_cancellationToken); } - /// - /// Handles the client identification. - /// - internal void HandleClientIdentification() - { - // Check if disconnected clients are remembered and can be reactivated - if (_options.RememberDisconnectedClients) - { - // Check if the request contains a user id - if (Request.ContainsUserId) - { - _logger?.LogDebug("User id found in request: {userId}", Request.UserId); - - lock (_clientLock) - { - ThrowForUserAlreadyConnected(); - - // Check if the client is an existing passive client - var clientExists = _passiveClients.ContainsKey(Request.UserId); - if (clientExists) - { - _logger?.LogDebug("Passive Client found for user id {userId} - reactivating user.", Request.UserId); - - // Use the existing client - // Update the client with the new connection - // Remove the client from the passive clients - var passiveClient = _passiveClients[Request.UserId]; - passiveClient.UpdateClient(Client.ClientConnection!); - Client = passiveClient; - var clientRemoved = _passiveClients.Remove(Request.UserId); - - // Set the flag that the client was a passive client - // This should only be set if the client was removed in this specific flow - // Otherwise its possible that the client is handled twice - _clientWasPassiveClient = clientRemoved; - } - else - { - // Client is not a passive client - // Update the clients user id - Client.UpdateId(Request.UserId); - } - } - } - } - } - - /// - /// Throws an exception if the user is already connected. - /// - /// - private void ThrowForUserAlreadyConnected() - { - // No passive client found, checking for active clients with the same id - if (_activeClients.ContainsKey(Request.UserId)) - { - _logger?.LogDebug("Active Client found for user id {userId} - rejecting connection.", Request.UserId); - // Reject the connection - - var responseContext = new WebContext - { - StatusCode = HttpStatusCode.Conflict, - BodyContent = "User id already in use" - }; - throw new UserNotHandledException(responseContext); - } - } - /// /// Accepts the web socket connection. /// internal async Task AcceptWebSocketAsync() { // The client is accepted - await _upgradeHandler.AcceptWebSocketAsync(Request, _responseContext, Client.Id, null, _cancellationToken); + await _upgradeHandler.AcceptWebSocketAsync(Request, _responseContext, null, _cancellationToken); // Use the web socket for the client Client.UseWebSocket(_upgradeHandler.CreateWebSocket(isServer: true)); - - // Set the flag that the client was accepted - // This is used to determine if the client should be added to the passive clients after disconnect - _clientAccepted = true; } /// @@ -192,23 +97,12 @@ internal async Task RejectWebSocketAsync(WebContext responseContext) /// internal void HandleDisconnectedClient() { - if (_clientWasPassiveClient || _clientAccepted) + lock (_clientLock) { - lock (_clientLock) - { - _activeClients.TryRemove(Client.Id, out _); - Client.Dispose(); - - if (_options.RememberDisconnectedClients) - { - _logger?.LogDebug("Client {clientId} is now a passive user.", Client.Id); - _passiveClients.Add(Client.Id, Client); - } - else - { - _logger?.LogDebug("Client {clientId} is removed.", Client.Id); - } - } + _activeClients.TryRemove(Client.Id, out _); + Client.Dispose(); + + _logger?.LogDebug("Client {clientId} is removed.", Client.Id); } } diff --git a/Jung.SimpleWebSocket/Models/EventArguments/PassiveUserExpiredEventArgs.cs b/Jung.SimpleWebSocket/Models/EventArguments/PassiveUserExpiredEventArgs.cs deleted file mode 100644 index 19cd99a..0000000 --- a/Jung.SimpleWebSocket/Models/EventArguments/PassiveUserExpiredEventArgs.cs +++ /dev/null @@ -1,10 +0,0 @@ -// This file is part of the Jung SimpleWebSocket project. -// The project is licensed under the MIT license. - -namespace Jung.SimpleWebSocket.Models.EventArguments; - -/// -/// Represents the arguments of the event when a passive user expired. -/// -/// The identifier of the user that expired. -public record PassiveUserExpiredArgs(string ClientId); \ No newline at end of file diff --git a/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs b/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs index 7e5a486..ea58fde 100644 --- a/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs +++ b/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs @@ -1,4 +1,7 @@ -using System.Net; +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using System.Net; namespace Jung.SimpleWebSocket.Models { @@ -16,29 +19,5 @@ public class SimpleWebSocketServerOptions /// Gets or sets the port of the server. /// public int Port { get; set; } - - /// - /// Switch for remembering disconnected clients. - /// - /// - /// If true the server will put disconnected clients into a passive client list. - /// This clients can reidentify themselves with their user id. - /// - public bool RememberDisconnectedClients { get; set; } = false; - - /// - /// Switch for removing passive clients after the end of the . - /// - public bool RemovePassiveClientsAfterClientExpirationTime { get; set; } = false; - - /// - /// Switch for sending the user id to the client. - /// - public bool SendUserIdToClient { get; set; } = false; - - /// - /// The time after which a passive client is removed from the passive client list. - /// - public TimeSpan PassiveClientLifetime { get; set; } = TimeSpan.FromMinutes(1); } -} \ No newline at end of file +} diff --git a/Jung.SimpleWebSocket/Models/WebContext.cs b/Jung.SimpleWebSocket/Models/WebContext.cs index 3b7626c..589ab61 100644 --- a/Jung.SimpleWebSocket/Models/WebContext.cs +++ b/Jung.SimpleWebSocket/Models/WebContext.cs @@ -238,9 +238,8 @@ private NameValueCollection ParseHeaders() /// The host name of the web request. /// The port of the web request. /// The request path of the web request. - /// The user id of the web request. /// The created web request context. - internal static WebContext CreateRequest(string hostName, int port, string requestPath, string? userId = null) + internal static WebContext CreateRequest(string hostName, int port, string requestPath) { var context = new WebContext() { @@ -249,12 +248,7 @@ internal static WebContext CreateRequest(string hostName, int port, string reque RequestPath = requestPath, }; - if (userId != null) - { - context.Headers.Add("x-user-id", userId); - } - - return context; + return context; } /// @@ -397,24 +391,6 @@ public static string GetStatusDescription(HttpStatusCode statusCode) return string.Join(" ", _splitByUppercaseRegex.Split(enumName)); } - - /// - /// Gets the user id of the web request. - /// - public string UserId - { - get - { - var userId = Headers["x-user-id"] ?? throw new WebSocketUpgradeException("UserId header is missing"); - return userId; - } - } - - /// - /// Gets a value indicating whether the web request contains a user id. - /// - public bool ContainsUserId => Headers["x-user-id"] != null; - /// /// Gets the content lines of the web request. /// diff --git a/Jung.SimpleWebSocket/SimpleWebSocketClient.cs b/Jung.SimpleWebSocket/SimpleWebSocketClient.cs index 2138cd6..6793e3d 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketClient.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketClient.cs @@ -22,9 +22,8 @@ namespace Jung.SimpleWebSocket /// The host name to connect to /// The port to connect to /// The web socket request path - /// The user id of the client. This is normally created by the server and sent back to the client /// A logger to write internal log messages - public class SimpleWebSocketClient(string hostName, int port, string requestPath, string? userId = null, ILogger? logger = null) : IWebSocketClient, IDisposable + public class SimpleWebSocketClient(string hostName, int port, string requestPath, ILogger? logger = null) : IWebSocketClient, IDisposable { /// public string HostName { get; } = hostName; @@ -33,9 +32,6 @@ public class SimpleWebSocketClient(string hostName, int port, string requestPath /// public string RequestPath { get; } = requestPath; - /// - public string? UserId { get; private set; } - /// public bool IsConnected => _client?.Connected ?? false; @@ -154,16 +150,11 @@ private async Task HandleWebSocketInitiation(TcpClientWrapper client, Cancellati _stream = client.GetStream(); var socketWrapper = new WebSocketUpgradeHandler(_stream); - var requestContext = WebContext.CreateRequest(HostName, Port, RequestPath, userId); + var requestContext = WebContext.CreateRequest(HostName, Port, RequestPath); await socketWrapper.SendUpgradeRequestAsync(requestContext, cancellationToken); var response = await socketWrapper.AwaitContextAsync(cancellationToken); WebSocketUpgradeHandler.ValidateUpgradeResponse(response, requestContext); - if (response.ContainsUserId) - { - UserId = response.UserId; - } - _webSocket = socketWrapper.CreateWebSocket(isServer: false); } diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index 13d28b9..04f6d1a 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -15,19 +15,24 @@ using System.Net; using System.Net.WebSockets; using System.Text; -using System.Threading; namespace Jung.SimpleWebSocket { /// /// A simple WebSocket server. /// - public class SimpleWebSocketServer : IWebSocketServer, IDisposable + /// + /// Initializes a new instance of the class that listens + /// for incoming connection attempts on the specified local IP address and port number. + /// + /// The options for the server + /// A logger to write internal log messages + public class SimpleWebSocketServer(SimpleWebSocketServerOptions options, ILogger? logger = null) : IWebSocketServer, IDisposable { /// - public IPAddress LocalIpAddress { get; } + public IPAddress LocalIpAddress { get; } = options.LocalIpAddress; /// - public int Port { get; } + public int Port { get; } = options.Port; /// public event EventHandler? ClientConnected; @@ -37,8 +42,6 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable public event EventHandler? MessageReceived; /// public event EventHandler? BinaryMessageReceived; - /// - public event EventHandler? PassiveUserExpiredEvent; /// public event AsyncEventHandler? ClientUpgradeRequestReceivedAsync; @@ -48,11 +51,6 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// internal ConcurrentDictionary ActiveClients { get; } = []; - /// - /// A dictionary of passive clients. - /// - internal IDictionary PassiveClients { get; set; } = null!; - /// public string[] ClientIds => [.. ActiveClients.Keys]; @@ -65,12 +63,12 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// /// A logger to write internal log messages. /// - internal ILogger? Logger { get; } + internal ILogger? Logger { get; } = logger; /// /// The options for the server. /// - internal SimpleWebSocketServerOptions Options { get; } + internal SimpleWebSocketServerOptions Options { get; } = options; /// /// A flag indicating whether the server is started. @@ -92,48 +90,6 @@ public class SimpleWebSocketServer : IWebSocketServer, IDisposable /// private ITcpListener? _tcpListener; - /// - /// Initializes a new instance of the class that listens - /// for incoming connection attempts on the specified local IP address and port number. - /// - /// The options for the server - /// A logger to write internal log messages - public SimpleWebSocketServer(SimpleWebSocketServerOptions options, ILogger? logger = null) - { - LocalIpAddress = options.LocalIpAddress; - Port = options.Port; - Logger = logger; - Options = options; - InitializePassiveClientDictionary(options); - } - - /// - /// Initializes the passive clients dictionary. - /// - /// - private void InitializePassiveClientDictionary(SimpleWebSocketServerOptions options) - { - if (options.RememberDisconnectedClients) - { - // Initialize the passive clients dictionary - if (options.RemovePassiveClientsAfterClientExpirationTime) - { - var passiveClients = new ExpiringDictionary(options.PassiveClientLifetime, Logger); - passiveClients.ItemExpired += PassiveClients_ItemExpired; - PassiveClients = passiveClients; - } - else - { - PassiveClients = new Dictionary(); - } - } - else - { - // If user handling is not activated, the passive clients are not needed - PassiveClients = null!; - } - } - /// /// Initializes a new instance of the class that listens /// for incoming connection attempts on the specified local IP address and port number. @@ -271,9 +227,6 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT // Load the request context await flow.LoadRequestContext(); - // Handle the client user identification if activated - flow.HandleClientIdentification(); - // raise async client upgrade request received event var eventArgs = await flow.RaiseUpgradeEventAsync(ClientUpgradeRequestReceivedAsync); @@ -372,23 +325,6 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C } } - /// - /// Handles the event when a passive user expired. - /// - /// - /// Condition: is set to true. - /// - /// The sender of the event () - /// The arguments of the event - private void PassiveClients_ItemExpired(object? sender, ItemExpiredArgs e) - { - Logger?.LogDebug("Passive Client expired: {clientId}", e.Item.Id); - - // Raise the event asynchronously - // We don't want to block the cleanup process - AsyncEventRaiser.RaiseAsyncInNewTask(PassiveUserExpiredEvent, this, new PassiveUserExpiredArgs(e.Item.Id), _cancellationTokenSource.Token); - } - /// public void Dispose() { diff --git a/Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs b/Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs deleted file mode 100644 index 52d3131..0000000 --- a/Jung.SimpleWebSocket/Utility/ExpiringDictionary.cs +++ /dev/null @@ -1,281 +0,0 @@ -// 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; -using System.Collections; -using System.Diagnostics.CodeAnalysis; - -namespace Jung.SimpleWebSocket.Utility; - -/// -/// Creates a dictionary with expiration time for each item. -/// -/// The type of the keys in the dictionary. -/// The type of the values in the dictionary. -/// The expiration time for each item. -/// The logger to log exceptions. -public class ExpiringDictionary(TimeSpan expiration, ILogger? logger = null): IDictionary where TKey : class -{ - - /// - /// Occurs when an item is expired. - /// - public event EventHandler>? ItemExpired; - - private readonly SortedList _expirationQueue = []; - private readonly Dictionary _dictionary = []; - - private bool _cleanupInProgress = false; - - /// - /// Add the specified key and value to the dictionary. - /// - /// - /// - public void Add(TKey key, TValue value) - { - lock (_dictionary) - { - // Add the item to the dictionary - _dictionary[key] = value; - - // Add item with expiration time to the queue - var expirationTime = DateTime.Now.Add(expiration); - - lock (_expirationQueue) - { - _expirationQueue.Add(expirationTime, key); - } - } - - // Trigger cleanup after the Add operation is done - lock (_expirationQueue) - { - if (!_cleanupInProgress) - { - _cleanupInProgress = true; - - // Run cleanup asynchronously - CleanupExpiredItems().ContinueWith(t => - { - if (t.IsFaulted) - { - // Handle exceptions here - logger?.LogError(t.Exception, "An Exception occurred during cleanup expired items."); - } - }); - } - } - } - - /// - /// Determines whether the dictionary contains the specified key. - /// - /// - /// - public bool ContainsKey(TKey key) - { - lock (_dictionary) - { - return _dictionary.ContainsKey(key); - } - } - /// - /// Removes the value with the specified key from the dictionary. - /// - /// The key of the value to remove. - /// Returns true if the element is successfully found and removed; otherwise, false. - public bool Remove(TKey key) - { - lock (_dictionary) - { - if (_dictionary.Remove(key)) - { - lock (_expirationQueue) - { - // Find and remove the expiration time entry for this key - var expirationTime = _expirationQueue.FirstOrDefault(x => x.Value.Equals(key)).Key; - if (expirationTime != default) - { - _expirationQueue.Remove(expirationTime); - } - } - return true; - } - } - return false; - } - - /// - /// Get or set the value associated with the specified key. - /// - /// The key of the value to get or set. - /// The value associated with the specified key. - public TValue this[TKey key] - { - get - { - lock (_dictionary) - { - return _dictionary[key]; - } - } - set - { - lock (_dictionary) - { - _dictionary[key] = value; - var expirationTime = DateTime.Now.Add(expiration); - lock (_expirationQueue) - { - _expirationQueue[expirationTime] = key; - } - } - } - } - - /// - /// Cleans up expired items. - /// - /// A task that represents the asynchronous cleanup operation. - private async Task CleanupExpiredItems() - { - while (true) - { - DateTime nearestExpiration; - TKey expiredKey; - - // Safely lock and retrieve the first item to expire - lock (_expirationQueue) - { - // If there are no items, stop the cleanup process - if (_expirationQueue.Count == 0) - { - _cleanupInProgress = false; - return; - } - - // Get the first key in expiration queue (FIFO order) - var firstItem = _expirationQueue.First(); - nearestExpiration = firstItem.Key; - expiredKey = firstItem.Value; - } - - // Calculate the delay based on the expiration time - TimeSpan delay = nearestExpiration - DateTime.Now; - - if (delay > TimeSpan.Zero) - { - await Task.Delay(delay); - } - - // Remove the expired item from the dictionary - lock (_dictionary) - { - if (_dictionary.TryGetValue(expiredKey, out var expiredItem)) - { - ItemExpired?.Invoke(this, new ItemExpiredArgs(expiredItem)); - _dictionary.Remove(expiredKey); - } - } - - // Remove from expiration queue, but only if it's still the correct key - lock (_expirationQueue) - { - if (_expirationQueue.Count > 0 && _expirationQueue.First().Value.Equals(expiredKey)) - { - _expirationQueue.RemoveAt(0); // Safely remove the correct item - } - } - } - } - - #region NotImplemented - - /// - /// Not implemented. - /// - public ICollection Keys => throw new NotImplementedException(); - - /// - /// Not implemented. - /// - public ICollection Values => throw new NotImplementedException(); - - /// - /// Not implemented. - /// - public int Count => throw new NotImplementedException(); - - /// - /// Not implemented. - /// - public bool IsReadOnly => throw new NotImplementedException(); - - /// - /// Not implemented. - /// - public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) - { - throw new NotImplementedException(); - } - - /// - /// Not implemented. - /// - public void Add(KeyValuePair item) - { - throw new NotImplementedException(); - } - - /// - /// Not implemented. - /// - public void Clear() - { - throw new NotImplementedException(); - } - - /// - /// Not implemented. - /// - public bool Contains(KeyValuePair item) - { - throw new NotImplementedException(); - } - - /// - /// Not implemented. - /// - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - throw new NotImplementedException(); - } - - /// - /// Not implemented. - /// - public bool Remove(KeyValuePair item) - { - throw new NotImplementedException(); - } - - /// - /// Not implemented. - /// - public IEnumerator> GetEnumerator() - { - throw new NotImplementedException(); - } - - /// - /// Not implemented. - /// - IEnumerator IEnumerable.GetEnumerator() - { - throw new NotImplementedException(); - } - - #endregion -} \ No newline at end of file diff --git a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs index 416a4aa..8a8e122 100644 --- a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs +++ b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs @@ -63,7 +63,7 @@ public async Task AwaitContextAsync(CancellationToken cancellationTo return context; } - public async Task AcceptWebSocketAsync(WebContext request, WebContext response, string userId, string? subProtocol, CancellationToken cancellationToken) + public async Task AcceptWebSocketAsync(WebContext request, WebContext response, string? subProtocol, CancellationToken cancellationToken) { try { @@ -79,7 +79,6 @@ public async Task AcceptWebSocketAsync(WebContext request, WebContext response, response.Headers.Add("Upgrade", "websocket"); response.Headers.Add("Sec-WebSocket-Accept", secWebSocketAcceptString); response.StatusCode = HttpStatusCode.SwitchingProtocols; - response.Headers.Add(_userIdHeaderName, userId); await SendWebSocketResponseHeaders(response, cancellationToken); _acceptedProtocol = subProtocol; } diff --git a/Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs b/Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs index 0e8916e..c9ba4eb 100644 --- a/Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs +++ b/Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs @@ -1,6 +1,5 @@ using Jung.SimpleWebSocket; using Jung.SimpleWebSocket.Contracts; -using Jung.SimpleWebSocket.Exceptions; using Jung.SimpleWebSocket.Flows; using Jung.SimpleWebSocket.Models; using Jung.SimpleWebSocketTest.Mock; @@ -41,7 +40,7 @@ private ClientHandlingFlow SetupClientHandlingFlow(object serverOptions, List(); - - var client = new WebSocketServerClient(); - client.UpdateId(userId); - activeUsers.Add(client); - - var requestText = CreateUpgradeRequest(userId); - var serverOptions = new SimpleWebSocketServerOptions - { - RememberDisconnectedClients = true, - }; - - // act and assert - var clientHandlingFlow = SetupClientHandlingFlow(serverOptions, activeUsers); - - clientHandlingFlow.Request = new WebContext(requestText); - Assert.That(() => clientHandlingFlow.HandleClientIdentification(), Throws.Exception.TypeOf()); - } } } diff --git a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs index e86696c..dc061e7 100644 --- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs +++ b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs @@ -50,7 +50,6 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() { LocalIpAddress = IPAddress.Any, Port = 8010, - RememberDisconnectedClients = true, }; using var server = new SimpleWebSocketServer(serverOptions, _serverLoggerMockHelper.Logger); @@ -61,7 +60,6 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() const string ClosingStatusDescription = "closing status test description"; string receivedMessage = string.Empty; string receivedClosingDescription = string.Empty; - string exceptionMessage = string.Empty; var messageResetEvent = new ManualResetEvent(false); var disconnectResetEvent = new ManualResetEvent(false); @@ -123,21 +121,11 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() WaitForManualResetEventOrThrow(disconnectResetEvent); // test if the server accepts the client again - var client2 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", client.UserId, logger: _clientLoggerMockHelper.Logger); + var client2 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", logger: _clientLoggerMockHelper.Logger); await client2.ConnectAsync(); await Task.Delay(100); - try - { - // test if two clients with the same user id can connect - var client3 = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", client.UserId, logger: _clientLoggerMockHelper.Logger); - await client3.ConnectAsync(); - } - catch (Exception exception) - { - exceptionMessage = exception.InnerException!.Message; - } - + await client2.SendMessageAsync("Hello World"); await server.ShutdownServer(CancellationToken.None); @@ -148,7 +136,6 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() { Assert.That(receivedMessage, Is.EqualTo(Message)); Assert.That(receivedClosingDescription, Is.EqualTo(ClosingStatusDescription)); - Assert.That(exceptionMessage, Does.Contain("User id already in use")); }); } @@ -165,49 +152,6 @@ private static async Task DbContext_IpAddresses_Contains(IPAddress ipAddre return ipAddress.Equals(IPAddress.Loopback); } - - [Test] - [Platform("Windows7,Windows8,Windows8.1,Windows10", Reason = "This test establishes a TCP client-server connection using SimpleWebSocket, which relies on specific networking features and behaviors that are only available and consistent on Windows platforms. Running this test on non-Windows platforms could lead to inconsistent results or failures due to differences in networking stack implementations.")] - public async Task TestClientServerConnection_ShouldRemoveClientFromPassiveClients() - { - // Arrange - string userId = Guid.NewGuid().ToString(); - var serverOptions = new SimpleWebSocketServerOptions - { - LocalIpAddress = IPAddress.Any, - Port = 8010, - RememberDisconnectedClients = true, - RemovePassiveClientsAfterClientExpirationTime = true, - PassiveClientLifetime = TimeSpan.FromSeconds(1) - }; - - using var server = new SimpleWebSocketServer(serverOptions, _serverLoggerMockHelper.Logger); - using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", userId, _clientLoggerMockHelper.Logger); - - var expiredClientId = string.Empty; - var expiredClientResetEvent = new ManualResetEvent(false); - - server.PassiveUserExpiredEvent += (sender, args) => - { - expiredClientId = args.ClientId; - expiredClientResetEvent.Set(); - }; - - // Act - server.Start(); - await client.ConnectAsync(); - await Task.Delay(100); - await client.DisconnectAsync(); - - WaitForManualResetEventOrThrow(expiredClientResetEvent, 2000); - - await server.ShutdownServer(CancellationToken.None); - Array.ForEach(LoggerMessages.GetMessages(), m => Trace.WriteLine(m)); - - // Assert - Assert.That(expiredClientId, Is.EqualTo(userId)); - } - [Test] [Platform("Windows7,Windows8,Windows8.1,Windows10", Reason = "This test establishes a TCP client-server connection using SimpleWebSocket, which relies on specific networking features and behaviors that are only available and consistent on Windows platforms. Running this test on non-Windows platforms could lead to inconsistent results or failures due to differences in networking stack implementations.")] public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld2() diff --git a/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs b/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs index aa69078..158ce22 100644 --- a/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs +++ b/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs @@ -64,7 +64,7 @@ public async Task AcceptWebSocketAsync_ShouldSendUpgradeResponse(string hostname _mockNetworkStream.Setup(ns => ns.WriteAsync(It.IsAny(), It.IsAny())).Callback((buffer, ct) => { response = Encoding.UTF8.GetString(buffer); }); // Act - await _socketWrapper.AcceptWebSocketAsync(request,new WebContext(), Guid.NewGuid().ToString(), null, cancellationToken); + await _socketWrapper.AcceptWebSocketAsync(request, new WebContext(), null, cancellationToken); // Assert Assert.That(response, Does.Contain("HTTP/1.1 101 Switching Protocols")); @@ -90,7 +90,7 @@ public async Task AcceptWebSocketAsync_ShouldSendUpgradeResponseWithCorrectProto _mockNetworkStream.Setup(ns => ns.WriteAsync(It.IsAny(), It.IsAny())).Callback((buffer, ct) => { response = Encoding.UTF8.GetString(buffer); }); // Act - await _socketWrapper.AcceptWebSocketAsync(request, new WebContext(), Guid.NewGuid().ToString(), serverSubprotocol, cancellationToken); + await _socketWrapper.AcceptWebSocketAsync(request, new WebContext(), serverSubprotocol, cancellationToken); // Assert Assert.Multiple(() => From 57c4fb80fd9fb911b40c3c3cb9c0b7da0f73e7c6 Mon Sep 17 00:00:00 2001 From: Christoph Date: Sun, 2 Feb 2025 09:47:59 +0100 Subject: [PATCH 13/27] Fix some spelling issues --- .../Wrappers/WebSocketUpgradeHandler.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs index 8a8e122..5d9f87b 100644 --- a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs +++ b/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs @@ -22,11 +22,10 @@ internal partial class WebSocketUpgradeHandler { private const string _supportedVersion = "13"; private const string _webSocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - private const string _userIdHeaderName = "x-user-id"; private string? _acceptedProtocol; private readonly INetworkStream _networkStream; - private readonly WebSocketHelper _websocketHelper; + private readonly WebSocketHelper _webSocketHelper; // Regex for a valid request path: must start with a `/` and can include valid path characters. [GeneratedRegex(@"^\/[a-zA-Z0-9\-._~\/]*$", RegexOptions.Compiled)] @@ -36,13 +35,13 @@ internal partial class WebSocketUpgradeHandler public WebSocketUpgradeHandler(INetworkStream networkStream) { _networkStream = networkStream; - _websocketHelper = new WebSocketHelper(); + _webSocketHelper = new WebSocketHelper(); } - internal WebSocketUpgradeHandler(INetworkStream networkStream, WebSocketHelper websocketHelper) + internal WebSocketUpgradeHandler(INetworkStream networkStream, WebSocketHelper webSocketHelper) { _networkStream = networkStream; - _websocketHelper = websocketHelper; + _webSocketHelper = webSocketHelper; } public async Task AwaitContextAsync(CancellationToken cancellationToken) @@ -88,7 +87,7 @@ public async Task AcceptWebSocketAsync(WebContext request, WebContext response, } catch (Exception message) { - throw new WebSocketException("Error while accepting the websocket", message); + throw new WebSocketException("Error while accepting the web socket", message); } } @@ -295,7 +294,7 @@ private static string ComputeWebSocketAccept(string secWebSocketKey) internal IWebSocket CreateWebSocket(bool isServer, TimeSpan? keepAliveInterval = null) { keepAliveInterval ??= TimeSpan.FromSeconds(30); - return _websocketHelper.CreateFromStream(_networkStream.Stream, isServer, _acceptedProtocol, keepAliveInterval.Value); + return _webSocketHelper.CreateFromStream(_networkStream.Stream, isServer, _acceptedProtocol, keepAliveInterval.Value); } internal async Task RejectWebSocketAsync(WebContext response, CancellationToken cancellationToken) From 8998849e144d58fc9f8785e301abed8192a35b42 Mon Sep 17 00:00:00 2001 From: Christoph Date: Sun, 2 Feb 2025 09:54:49 +0100 Subject: [PATCH 14/27] Add method to change the id of a user to the web socket server --- .../Contracts/IWebSocketServer.cs | 10 +++ .../ClientIdAlreadyExistsException.cs | 13 ++++ .../Exceptions/ClientNotFoundException.cs | 13 ++++ Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 14 ++++ .../SimpleWebSocketTest.cs | 76 ++++++++++++++++++- 5 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 Jung.SimpleWebSocket/Exceptions/ClientIdAlreadyExistsException.cs create mode 100644 Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs diff --git a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs b/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs index f4973a2..a12093e 100644 --- a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs +++ b/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs @@ -2,6 +2,7 @@ // The project is licensed under the MIT license. using Jung.SimpleWebSocket.Delegates; +using Jung.SimpleWebSocket.Exceptions; using Jung.SimpleWebSocket.Models; using Jung.SimpleWebSocket.Models.EventArguments; using System.Net; @@ -92,4 +93,13 @@ public interface IWebSocketServer : IDisposable /// The cancellation token. /// A task representing the asynchronous operation. void Start(CancellationToken? cancellationToken = null); + + /// + /// Changes the id of a client. + /// + /// The client to update + /// The new id of the client + /// Throws when the client is not found + /// Throws when the new id is already in use + void ChangeClientId(WebSocketServerClient client, string newId); } diff --git a/Jung.SimpleWebSocket/Exceptions/ClientIdAlreadyExistsException.cs b/Jung.SimpleWebSocket/Exceptions/ClientIdAlreadyExistsException.cs new file mode 100644 index 0000000..91635d6 --- /dev/null +++ b/Jung.SimpleWebSocket/Exceptions/ClientIdAlreadyExistsException.cs @@ -0,0 +1,13 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +namespace Jung.SimpleWebSocket.Exceptions +{ + /// + /// Exception thrown when a client with the same id already exists in the client list. + /// + /// The message to display when the exception is thrown. + public class ClientIdAlreadyExistsException(string message) : Exception(message) + { + } +} diff --git a/Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs b/Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs new file mode 100644 index 0000000..58d4a95 --- /dev/null +++ b/Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs @@ -0,0 +1,13 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +namespace Jung.SimpleWebSocket.Exceptions +{ + /// + /// Exception thrown when a client with the given id is not found in the client list. + /// + /// The message to display when the exception is thrown. + public class ClientNotFoundException(string message) : Exception(message) + { + } +} diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index 04f6d1a..c2cd853 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -213,6 +213,20 @@ public WebSocketServerClient GetClientById(string clientId) return client; } + /// + public void ChangeClientId(WebSocketServerClient client, string newId) + { + // 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"); + + // because the id is used as a key in the dictionary, + // we have to remove the client and add it again with the new id + ActiveClients.TryRemove(client.Id, out _); + client.UpdateId(newId); + ActiveClients.TryAdd(newId, client); + } + /// /// Handles the client connection. /// diff --git a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs index dc061e7..8f9bcb3 100644 --- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs +++ b/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs @@ -2,6 +2,7 @@ // The project is licensed under the MIT license. using Jung.SimpleWebSocket; +using Jung.SimpleWebSocket.Exceptions; using Jung.SimpleWebSocket.Models; using Jung.SimpleWebSocketTest.Mock; using NUnit.Framework; @@ -39,7 +40,80 @@ public void EndTest() Trace.Flush(); } + [Test] + public void ChangeClientId_UserIdUnique_ShouldUpdateId() + { + // Arrange + var serverOptions = new SimpleWebSocketServerOptions + { + LocalIpAddress = IPAddress.Any, + Port = 8010, + }; + + var connectedClient1 = new WebSocketServerClient(); + var connectedClient2 = new WebSocketServerClient(); + var oldId = connectedClient1.Id; + + using var server = new SimpleWebSocketServer(serverOptions, _serverLoggerMockHelper.Logger); + if (!server.ActiveClients.TryAdd(connectedClient1.Id, connectedClient1) || + !server.ActiveClients.TryAdd(connectedClient2.Id, connectedClient2)) + { + throw new Exception("Could not add clients to the server."); + } + // Act + var newId = Guid.NewGuid().ToString(); + server.ChangeClientId(connectedClient1, newId); + + // Assert + Assert.Multiple(() => + { + Assert.That(connectedClient1.Id, Is.EqualTo(newId)); + Assert.That(server.ActiveClients.ContainsKey(oldId), Is.False); + Assert.That(server.ActiveClients.ContainsKey(newId), Is.True); + }); + } + + [Test] + public void ChangeClientId_UserIdDuplicated_ShouldThrowException() + { + // Arrange + var serverOptions = new SimpleWebSocketServerOptions + { + LocalIpAddress = IPAddress.Any, + Port = 8010, + }; + + var connectedClient1 = new WebSocketServerClient(); + var connectedClient2 = new WebSocketServerClient(); + + + using var server = new SimpleWebSocketServer(serverOptions, _serverLoggerMockHelper.Logger); + if (!server.ActiveClients.TryAdd(connectedClient1.Id, connectedClient1) || + !server.ActiveClients.TryAdd(connectedClient2.Id, connectedClient2)) + { + throw new Exception("Could not add clients to the server."); + } + + // Act & Assert + Assert.That(() => server.ChangeClientId(connectedClient1, connectedClient2.Id), Throws.Exception.TypeOf()); + } + + [Test] + public void ChangeClientId_TargetUserNotExisting_ShouldThrowException() + { + // Arrange + var serverOptions = new SimpleWebSocketServerOptions + { + LocalIpAddress = IPAddress.Any, + Port = 8010, + }; + + using var server = new SimpleWebSocketServer(serverOptions, _serverLoggerMockHelper.Logger); + + // Act & Assert + Assert.That(() => server.ChangeClientId(new WebSocketServerClient(), Guid.NewGuid().ToString()), Throws.Exception.TypeOf()); + } [Test] [Platform("Windows7,Windows8,Windows8.1,Windows10", Reason = "This test establishes a TCP client-server connection using SimpleWebSocket, which relies on specific networking features and behaviors that are only available and consistent on Windows platforms. Running this test on non-Windows platforms could lead to inconsistent results or failures due to differences in networking stack implementations.")] @@ -125,7 +199,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() await client2.ConnectAsync(); await Task.Delay(100); - + await client2.SendMessageAsync("Hello World"); await server.ShutdownServer(CancellationToken.None); From e30b32ab5275863d03cf7388ffecc04048ab4df5 Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Sat, 8 Feb 2025 19:00:10 +0100 Subject: [PATCH 15/27] Improve handling flow --- .../Flows/ClientHandlingFlow.cs | 58 +++++++++++++------ Jung.SimpleWebSocket/SimpleWebSocketServer.cs | 5 +- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs b/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs index e41e93e..5519676 100644 --- a/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs +++ b/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs @@ -7,6 +7,7 @@ using Jung.SimpleWebSocket.Utility; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; namespace Jung.SimpleWebSocket.Flows { @@ -29,17 +30,17 @@ internal class ClientHandlingFlow(SimpleWebSocketServer server, WebSocketServerC /// /// Gets the request context of the client. /// - internal WebContext Request { get; set; } = null!; + internal WebContext? Request { get; set; } = null!; /// /// Gets the upgrade handler for the client. /// - private WebSocketUpgradeHandler _upgradeHandler = null!; + private WebSocketUpgradeHandler? _upgradeHandler = null; /// /// Gets the response context that is being use to response to the client. /// - private WebContext _responseContext = null!; + private WebContext? _responseContext = null; /// /// Gets the active clients of the server. @@ -56,11 +57,6 @@ internal class ClientHandlingFlow(SimpleWebSocketServer server, WebSocketServerC /// private readonly CancellationToken _cancellationToken = cancellationToken; - /// - /// The lock object for the client dictionaries. - /// - private static readonly object _clientLock = new(); - /// /// Loads the request context. /// @@ -76,11 +72,15 @@ internal async Task LoadRequestContext() /// internal async Task AcceptWebSocketAsync() { + // Check if the response context are initialized + ThrowForResponseContextNotInitialized(_responseContext); + // The client is accepted - await _upgradeHandler.AcceptWebSocketAsync(Request, _responseContext, null, _cancellationToken); + await _upgradeHandler!.AcceptWebSocketAsync(Request!, _responseContext, null, _cancellationToken); // Use the web socket for the client Client.UseWebSocket(_upgradeHandler.CreateWebSocket(isServer: true)); + Cleanup(); } /// @@ -89,7 +89,9 @@ internal async Task AcceptWebSocketAsync() /// The response context to send to the client. internal async Task RejectWebSocketAsync(WebContext responseContext) { - await _upgradeHandler.RejectWebSocketAsync(responseContext, _cancellationToken); + // The client is rejected + await _upgradeHandler!.RejectWebSocketAsync(responseContext, _cancellationToken); + Cleanup(); } /// @@ -97,13 +99,10 @@ internal async Task RejectWebSocketAsync(WebContext responseContext) /// internal void HandleDisconnectedClient() { - lock (_clientLock) - { - _activeClients.TryRemove(Client.Id, out _); - Client.Dispose(); + _activeClients.TryRemove(Client.Id, out _); + Client.Dispose(); - _logger?.LogDebug("Client {clientId} is removed.", Client.Id); - } + _logger?.LogDebug("Client {clientId} is removed.", Client.Id); } /// @@ -113,7 +112,7 @@ internal void HandleDisconnectedClient() /// The event arguments of the upgrade request. internal async Task RaiseUpgradeEventAsync(AsyncEventHandler? clientUpgradeRequestReceivedAsync) { - var eventArgs = new ClientUpgradeRequestReceivedArgs(Client, Request, _logger); + var eventArgs = new ClientUpgradeRequestReceivedArgs(Client, Request!, _logger); await AsyncEventRaiser.RaiseAsync(clientUpgradeRequestReceivedAsync, server, eventArgs, _cancellationToken); _responseContext = eventArgs.ResponseContext; return eventArgs; @@ -127,5 +126,28 @@ internal bool TryAddClientToActiveUserList() { return _activeClients.TryAdd(Client.Id, Client); } + + /// + /// Throws an exception if the response context is not initialized. + /// + /// The response context to check. + /// + private static void ThrowForResponseContextNotInitialized([NotNull] WebContext? responseContext) + { + if (responseContext is null) + { + throw new InvalidOperationException("The response context is not initialized."); + } + } + + /// + /// Disposes the upgrade handler. + /// + private void Cleanup() + { + _upgradeHandler = null; + _responseContext = null; + Request = null; + } } -} \ No newline at end of file +} diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index c2cd853..4b298cf 100644 --- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -241,7 +241,7 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT // Load the request context await flow.LoadRequestContext(); - // raise async client upgrade request received event + // Raise async client upgrade request received event var eventArgs = await flow.RaiseUpgradeEventAsync(ClientUpgradeRequestReceivedAsync); // Respond to the upgrade request @@ -249,6 +249,7 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT { // Accept the WebSocket connection await flow.AcceptWebSocketAsync(); + if (flow.TryAddClientToActiveUserList()) { Logger?.LogDebug("Connection upgraded, now listening on Client {clientId}", flow.Client.Id); @@ -258,7 +259,7 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT } else { - Logger?.LogDebug("Connection upgraded, now listening on Client {clientId}", flow.Client.Id); + Logger?.LogDebug("Error while adding Client {clientId} to active clients", flow.Client.Id); } } else From d26cbca23d62b38f5620d59c01d642562dd8d6dd Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Tue, 11 Feb 2025 20:05:53 +0100 Subject: [PATCH 16/27] Move projects in src and tests directories --- Jung.SimpleWebSocket.sln | 10 +++- .../ClientHandlingFlowTest.cs | 57 ------------------- .../Jung.SimpleWebSocket}/AssemblyInfo.cs | 2 +- .../Contracts/INetworkStream.cs | 0 .../Contracts/ITcpClient.cs | 0 .../Contracts/ITcpListener.cs | 0 .../Contracts/IWebSocket.cs | 0 .../Contracts/IWebSocketClient.cs | 0 .../Contracts/IWebSocketServer.cs | 0 .../Delegates/AsyncEventHandler.cs | 0 .../BinaryMessageReceivedEventHandler.cs | 0 ...ClientBinaryMessageReceivedEventHandler.cs | 0 .../Delegates/ClientConnectedEventHandler.cs | 0 .../ClientDisconnectedEventHandler.cs | 0 .../ClientMessageReceivedEventHandler.cs | 0 .../Delegates/DisconnectedEventHandler.cs | 0 .../Delegates/MessageReceivedEventHandler.cs | 0 .../ClientIdAlreadyExistsException.cs | 0 .../Exceptions/ClientNotFoundException.cs | 0 .../Exceptions/SimpleWebSocketException.cs | 0 .../Exceptions/UserNotHandledException.cs | 0 .../Exceptions/WebContextException.cs | 0 .../Exceptions/WebSocketClientException.cs | 0 .../Exceptions/WebSocketServerException.cs | 0 .../Exceptions/WebSocketUpgradeException.cs | 0 .../Flows/ClientHandlingFlow.cs | 0 .../Helpers/WebSocketHelper.cs | 0 .../Jung.SimpleWebSocket.csproj | 0 .../BinaryMessageReceivedArgs.cs | 0 .../ClientBinaryMessageReceivedArgs.cs | 0 .../EventArguments/ClientConnectedArgs.cs | 0 .../EventArguments/ClientDisconnectedArgs.cs | 0 .../ClientMessageReceivedArgs.cs | 0 .../ClientUpgradeRequestReceivedArgs.cs | 0 .../Models/EventArguments/DisconnectedArgs.cs | 0 .../Models/EventArguments/ItemExpiredArgs.cs | 0 .../EventArguments/MessageReceivedArgs.cs | 0 .../Models/SimpleWebSocketServerOptions.cs | 0 .../Models/WebContext.cs | 0 .../Models/WebSocketServerClient.cs | 0 .../SimpleWebSocketClient.cs | 0 .../SimpleWebSocketServer.cs | 0 .../Utility/AsyncEventRaiser.cs | 0 .../Wrappers/NetworkStreamWrapper.cs | 0 .../Wrappers/TcpClientWrapper.cs | 0 .../Wrappers/TcpListenerWrapper.cs | 0 .../Wrappers/WebSocketUpgradeHandler.cs | 0 .../Wrappers/WebSocketWrapper.cs | 0 .../Jung.SimpleWebSocket}/docs/README.md | 0 .../Jung.SimpleWebSocket.UnitTests.csproj | 2 +- .../Mock/ILoggerMockHelper.cs | 7 ++- .../Mock/LoggerMessages.cs | 6 +- .../SimpleWebSocketTest.cs | 7 +-- .../WebContextTest.cs | 5 +- .../WebSocketUpgradeHandlerTests.cs | 6 +- 55 files changed, 30 insertions(+), 72 deletions(-) delete mode 100644 Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/AssemblyInfo.cs (94%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Contracts/INetworkStream.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Contracts/ITcpClient.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Contracts/ITcpListener.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Contracts/IWebSocket.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Contracts/IWebSocketClient.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Contracts/IWebSocketServer.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Delegates/AsyncEventHandler.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Delegates/BinaryMessageReceivedEventHandler.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Delegates/ClientBinaryMessageReceivedEventHandler.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Delegates/ClientConnectedEventHandler.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Delegates/ClientDisconnectedEventHandler.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Delegates/ClientMessageReceivedEventHandler.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Delegates/DisconnectedEventHandler.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Delegates/MessageReceivedEventHandler.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Exceptions/ClientIdAlreadyExistsException.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Exceptions/ClientNotFoundException.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Exceptions/SimpleWebSocketException.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Exceptions/UserNotHandledException.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Exceptions/WebContextException.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Exceptions/WebSocketClientException.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Exceptions/WebSocketServerException.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Exceptions/WebSocketUpgradeException.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Flows/ClientHandlingFlow.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Helpers/WebSocketHelper.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Jung.SimpleWebSocket.csproj (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/EventArguments/BinaryMessageReceivedArgs.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/EventArguments/ClientBinaryMessageReceivedArgs.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/EventArguments/ClientConnectedArgs.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/EventArguments/ClientDisconnectedArgs.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/EventArguments/ClientMessageReceivedArgs.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/EventArguments/DisconnectedArgs.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/EventArguments/ItemExpiredArgs.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/EventArguments/MessageReceivedArgs.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/SimpleWebSocketServerOptions.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/WebContext.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Models/WebSocketServerClient.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/SimpleWebSocketClient.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/SimpleWebSocketServer.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Utility/AsyncEventRaiser.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Wrappers/NetworkStreamWrapper.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Wrappers/TcpClientWrapper.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Wrappers/TcpListenerWrapper.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Wrappers/WebSocketUpgradeHandler.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/Wrappers/WebSocketWrapper.cs (100%) rename {Jung.SimpleWebSocket => src/Jung.SimpleWebSocket}/docs/README.md (100%) rename Jung.SimpleWebSocketTest/Jung.SimpleWebSocketTest.csproj => tests/Jung.SimpleWebSocket.UnitTests/Jung.SimpleWebSocket.UnitTests.csproj (88%) rename {Jung.SimpleWebSocketTest => tests/Jung.SimpleWebSocket.UnitTests}/Mock/ILoggerMockHelper.cs (87%) rename {Jung.SimpleWebSocketTest => tests/Jung.SimpleWebSocket.UnitTests}/Mock/LoggerMessages.cs (76%) rename {Jung.SimpleWebSocketTest => tests/Jung.SimpleWebSocket.UnitTests}/SimpleWebSocketTest.cs (98%) rename {Jung.SimpleWebSocketTest => tests/Jung.SimpleWebSocket.UnitTests}/WebContextTest.cs (97%) rename {Jung.SimpleWebSocketTest => tests/Jung.SimpleWebSocket.UnitTests}/WebSocketUpgradeHandlerTests.cs (98%) diff --git a/Jung.SimpleWebSocket.sln b/Jung.SimpleWebSocket.sln index ed5da37..b5af771 100644 --- a/Jung.SimpleWebSocket.sln +++ b/Jung.SimpleWebSocket.sln @@ -3,9 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.11.35222.181 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jung.SimpleWebSocket", "Jung.SimpleWebSocket\Jung.SimpleWebSocket.csproj", "{793B04E9-6326-425A-A29C-A736CFD1E0C0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jung.SimpleWebSocket", "src\Jung.SimpleWebSocket\Jung.SimpleWebSocket.csproj", "{793B04E9-6326-425A-A29C-A736CFD1E0C0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jung.SimpleWebSocketTest", "Jung.SimpleWebSocketTest\Jung.SimpleWebSocketTest.csproj", "{26725C3C-8E90-49AC-9EE4-2A77ADB2229D}" +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", "{144CF5BC-D92F-4BC4-80E9-A2FABA93C8A2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -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 + {144CF5BC-D92F-4BC4-80E9-A2FABA93C8A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {144CF5BC-D92F-4BC4-80E9-A2FABA93C8A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {144CF5BC-D92F-4BC4-80E9-A2FABA93C8A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {144CF5BC-D92F-4BC4-80E9-A2FABA93C8A2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs b/Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs deleted file mode 100644 index c9ba4eb..0000000 --- a/Jung.SimpleWebSocketTest/ClientHandlingFlowTest.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Jung.SimpleWebSocket; -using Jung.SimpleWebSocket.Contracts; -using Jung.SimpleWebSocket.Flows; -using Jung.SimpleWebSocket.Models; -using Jung.SimpleWebSocketTest.Mock; -using Microsoft.Extensions.Logging; -using Moq; -using NUnit.Framework; -using System.Text; - -namespace Jung.SimpleWebSocketTest -{ - [TestFixture] - internal class ClientHandlingFlowTest - { - private ILogger _logger; - - [SetUp] - public void SetUp() - { - var loggerHelper = new ILoggerMockHelper("Server"); - _logger = loggerHelper.Logger; - } - - private ClientHandlingFlow SetupClientHandlingFlow(object serverOptions, List? activeClients = null) - { - var tcpListener = new Mock(); - var serverMoq = new Mock(serverOptions, tcpListener.Object, _logger); - if (activeClients != null) - { - foreach (var client in activeClients) - { - serverMoq.Object.ActiveClients.TryAdd(client.Id, client); - } - } - - var tcpClientMoq = new Mock(); - var serverClientMoq = new WebSocketServerClient(tcpClientMoq.Object); - - return new ClientHandlingFlow(serverMoq.Object, serverClientMoq, CancellationToken.None); - } - - private string CreateUpgradeRequest() - { - var sb = new StringBuilder(); - sb.Append("GET /chat HTTP/1.1\r\n" + - "Host: localhost:8080\r\n" + - "Upgrade: websocket\r\n" + - "Connection: Upgrade\r\n" + - "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n" + - "Sec-WebSocket-Version: 13\r\n"); - - sb.Append("\r\n\r\n"); - return sb.ToString(); - } - } -} diff --git a/Jung.SimpleWebSocket/AssemblyInfo.cs b/src/Jung.SimpleWebSocket/AssemblyInfo.cs similarity index 94% rename from Jung.SimpleWebSocket/AssemblyInfo.cs rename to src/Jung.SimpleWebSocket/AssemblyInfo.cs index dddbe37..44a7198 100644 --- a/Jung.SimpleWebSocket/AssemblyInfo.cs +++ b/src/Jung.SimpleWebSocket/AssemblyInfo.cs @@ -21,5 +21,5 @@ [assembly: Guid("ca34219d-7a2e-4993-ad9d-f27fda1bb9dc")] // Make internals visible to the test project and the dynamic proxy assembly (moq) -[assembly: InternalsVisibleTo("Jung.SimpleWebSocketTest")] +[assembly: InternalsVisibleTo("Jung.SimpleWebSocket.UnitTests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/Jung.SimpleWebSocket/Contracts/INetworkStream.cs b/src/Jung.SimpleWebSocket/Contracts/INetworkStream.cs similarity index 100% rename from Jung.SimpleWebSocket/Contracts/INetworkStream.cs rename to src/Jung.SimpleWebSocket/Contracts/INetworkStream.cs diff --git a/Jung.SimpleWebSocket/Contracts/ITcpClient.cs b/src/Jung.SimpleWebSocket/Contracts/ITcpClient.cs similarity index 100% rename from Jung.SimpleWebSocket/Contracts/ITcpClient.cs rename to src/Jung.SimpleWebSocket/Contracts/ITcpClient.cs diff --git a/Jung.SimpleWebSocket/Contracts/ITcpListener.cs b/src/Jung.SimpleWebSocket/Contracts/ITcpListener.cs similarity index 100% rename from Jung.SimpleWebSocket/Contracts/ITcpListener.cs rename to src/Jung.SimpleWebSocket/Contracts/ITcpListener.cs diff --git a/Jung.SimpleWebSocket/Contracts/IWebSocket.cs b/src/Jung.SimpleWebSocket/Contracts/IWebSocket.cs similarity index 100% rename from Jung.SimpleWebSocket/Contracts/IWebSocket.cs rename to src/Jung.SimpleWebSocket/Contracts/IWebSocket.cs diff --git a/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs b/src/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs similarity index 100% rename from Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs rename to src/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs diff --git a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs b/src/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs similarity index 100% rename from Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs rename to src/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs diff --git a/Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs b/src/Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs similarity index 100% rename from Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs rename to src/Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs diff --git a/Jung.SimpleWebSocket/Delegates/BinaryMessageReceivedEventHandler.cs b/src/Jung.SimpleWebSocket/Delegates/BinaryMessageReceivedEventHandler.cs similarity index 100% rename from Jung.SimpleWebSocket/Delegates/BinaryMessageReceivedEventHandler.cs rename to src/Jung.SimpleWebSocket/Delegates/BinaryMessageReceivedEventHandler.cs diff --git a/Jung.SimpleWebSocket/Delegates/ClientBinaryMessageReceivedEventHandler.cs b/src/Jung.SimpleWebSocket/Delegates/ClientBinaryMessageReceivedEventHandler.cs similarity index 100% rename from Jung.SimpleWebSocket/Delegates/ClientBinaryMessageReceivedEventHandler.cs rename to src/Jung.SimpleWebSocket/Delegates/ClientBinaryMessageReceivedEventHandler.cs diff --git a/Jung.SimpleWebSocket/Delegates/ClientConnectedEventHandler.cs b/src/Jung.SimpleWebSocket/Delegates/ClientConnectedEventHandler.cs similarity index 100% rename from Jung.SimpleWebSocket/Delegates/ClientConnectedEventHandler.cs rename to src/Jung.SimpleWebSocket/Delegates/ClientConnectedEventHandler.cs diff --git a/Jung.SimpleWebSocket/Delegates/ClientDisconnectedEventHandler.cs b/src/Jung.SimpleWebSocket/Delegates/ClientDisconnectedEventHandler.cs similarity index 100% rename from Jung.SimpleWebSocket/Delegates/ClientDisconnectedEventHandler.cs rename to src/Jung.SimpleWebSocket/Delegates/ClientDisconnectedEventHandler.cs diff --git a/Jung.SimpleWebSocket/Delegates/ClientMessageReceivedEventHandler.cs b/src/Jung.SimpleWebSocket/Delegates/ClientMessageReceivedEventHandler.cs similarity index 100% rename from Jung.SimpleWebSocket/Delegates/ClientMessageReceivedEventHandler.cs rename to src/Jung.SimpleWebSocket/Delegates/ClientMessageReceivedEventHandler.cs diff --git a/Jung.SimpleWebSocket/Delegates/DisconnectedEventHandler.cs b/src/Jung.SimpleWebSocket/Delegates/DisconnectedEventHandler.cs similarity index 100% rename from Jung.SimpleWebSocket/Delegates/DisconnectedEventHandler.cs rename to src/Jung.SimpleWebSocket/Delegates/DisconnectedEventHandler.cs diff --git a/Jung.SimpleWebSocket/Delegates/MessageReceivedEventHandler.cs b/src/Jung.SimpleWebSocket/Delegates/MessageReceivedEventHandler.cs similarity index 100% rename from Jung.SimpleWebSocket/Delegates/MessageReceivedEventHandler.cs rename to src/Jung.SimpleWebSocket/Delegates/MessageReceivedEventHandler.cs diff --git a/Jung.SimpleWebSocket/Exceptions/ClientIdAlreadyExistsException.cs b/src/Jung.SimpleWebSocket/Exceptions/ClientIdAlreadyExistsException.cs similarity index 100% rename from Jung.SimpleWebSocket/Exceptions/ClientIdAlreadyExistsException.cs rename to src/Jung.SimpleWebSocket/Exceptions/ClientIdAlreadyExistsException.cs diff --git a/Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs b/src/Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs similarity index 100% rename from Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs rename to src/Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs diff --git a/Jung.SimpleWebSocket/Exceptions/SimpleWebSocketException.cs b/src/Jung.SimpleWebSocket/Exceptions/SimpleWebSocketException.cs similarity index 100% rename from Jung.SimpleWebSocket/Exceptions/SimpleWebSocketException.cs rename to src/Jung.SimpleWebSocket/Exceptions/SimpleWebSocketException.cs diff --git a/Jung.SimpleWebSocket/Exceptions/UserNotHandledException.cs b/src/Jung.SimpleWebSocket/Exceptions/UserNotHandledException.cs similarity index 100% rename from Jung.SimpleWebSocket/Exceptions/UserNotHandledException.cs rename to src/Jung.SimpleWebSocket/Exceptions/UserNotHandledException.cs diff --git a/Jung.SimpleWebSocket/Exceptions/WebContextException.cs b/src/Jung.SimpleWebSocket/Exceptions/WebContextException.cs similarity index 100% rename from Jung.SimpleWebSocket/Exceptions/WebContextException.cs rename to src/Jung.SimpleWebSocket/Exceptions/WebContextException.cs diff --git a/Jung.SimpleWebSocket/Exceptions/WebSocketClientException.cs b/src/Jung.SimpleWebSocket/Exceptions/WebSocketClientException.cs similarity index 100% rename from Jung.SimpleWebSocket/Exceptions/WebSocketClientException.cs rename to src/Jung.SimpleWebSocket/Exceptions/WebSocketClientException.cs diff --git a/Jung.SimpleWebSocket/Exceptions/WebSocketServerException.cs b/src/Jung.SimpleWebSocket/Exceptions/WebSocketServerException.cs similarity index 100% rename from Jung.SimpleWebSocket/Exceptions/WebSocketServerException.cs rename to src/Jung.SimpleWebSocket/Exceptions/WebSocketServerException.cs diff --git a/Jung.SimpleWebSocket/Exceptions/WebSocketUpgradeException.cs b/src/Jung.SimpleWebSocket/Exceptions/WebSocketUpgradeException.cs similarity index 100% rename from Jung.SimpleWebSocket/Exceptions/WebSocketUpgradeException.cs rename to src/Jung.SimpleWebSocket/Exceptions/WebSocketUpgradeException.cs diff --git a/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs b/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs similarity index 100% rename from Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs rename to src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs diff --git a/Jung.SimpleWebSocket/Helpers/WebSocketHelper.cs b/src/Jung.SimpleWebSocket/Helpers/WebSocketHelper.cs similarity index 100% rename from Jung.SimpleWebSocket/Helpers/WebSocketHelper.cs rename to src/Jung.SimpleWebSocket/Helpers/WebSocketHelper.cs diff --git a/Jung.SimpleWebSocket/Jung.SimpleWebSocket.csproj b/src/Jung.SimpleWebSocket/Jung.SimpleWebSocket.csproj similarity index 100% rename from Jung.SimpleWebSocket/Jung.SimpleWebSocket.csproj rename to src/Jung.SimpleWebSocket/Jung.SimpleWebSocket.csproj diff --git a/Jung.SimpleWebSocket/Models/EventArguments/BinaryMessageReceivedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/BinaryMessageReceivedArgs.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/EventArguments/BinaryMessageReceivedArgs.cs rename to src/Jung.SimpleWebSocket/Models/EventArguments/BinaryMessageReceivedArgs.cs diff --git a/Jung.SimpleWebSocket/Models/EventArguments/ClientBinaryMessageReceivedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientBinaryMessageReceivedArgs.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/EventArguments/ClientBinaryMessageReceivedArgs.cs rename to src/Jung.SimpleWebSocket/Models/EventArguments/ClientBinaryMessageReceivedArgs.cs diff --git a/Jung.SimpleWebSocket/Models/EventArguments/ClientConnectedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientConnectedArgs.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/EventArguments/ClientConnectedArgs.cs rename to src/Jung.SimpleWebSocket/Models/EventArguments/ClientConnectedArgs.cs diff --git a/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs rename to src/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs diff --git a/Jung.SimpleWebSocket/Models/EventArguments/ClientMessageReceivedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientMessageReceivedArgs.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/EventArguments/ClientMessageReceivedArgs.cs rename to src/Jung.SimpleWebSocket/Models/EventArguments/ClientMessageReceivedArgs.cs diff --git a/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs rename to src/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs diff --git a/Jung.SimpleWebSocket/Models/EventArguments/DisconnectedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/DisconnectedArgs.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/EventArguments/DisconnectedArgs.cs rename to src/Jung.SimpleWebSocket/Models/EventArguments/DisconnectedArgs.cs diff --git a/Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs rename to src/Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs diff --git a/Jung.SimpleWebSocket/Models/EventArguments/MessageReceivedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/MessageReceivedArgs.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/EventArguments/MessageReceivedArgs.cs rename to src/Jung.SimpleWebSocket/Models/EventArguments/MessageReceivedArgs.cs diff --git a/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs b/src/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs rename to src/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs diff --git a/Jung.SimpleWebSocket/Models/WebContext.cs b/src/Jung.SimpleWebSocket/Models/WebContext.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/WebContext.cs rename to src/Jung.SimpleWebSocket/Models/WebContext.cs diff --git a/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs b/src/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs similarity index 100% rename from Jung.SimpleWebSocket/Models/WebSocketServerClient.cs rename to src/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs diff --git a/Jung.SimpleWebSocket/SimpleWebSocketClient.cs b/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs similarity index 100% rename from Jung.SimpleWebSocket/SimpleWebSocketClient.cs rename to src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs similarity index 100% rename from Jung.SimpleWebSocket/SimpleWebSocketServer.cs rename to src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs diff --git a/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs b/src/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs similarity index 100% rename from Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs rename to src/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs diff --git a/Jung.SimpleWebSocket/Wrappers/NetworkStreamWrapper.cs b/src/Jung.SimpleWebSocket/Wrappers/NetworkStreamWrapper.cs similarity index 100% rename from Jung.SimpleWebSocket/Wrappers/NetworkStreamWrapper.cs rename to src/Jung.SimpleWebSocket/Wrappers/NetworkStreamWrapper.cs diff --git a/Jung.SimpleWebSocket/Wrappers/TcpClientWrapper.cs b/src/Jung.SimpleWebSocket/Wrappers/TcpClientWrapper.cs similarity index 100% rename from Jung.SimpleWebSocket/Wrappers/TcpClientWrapper.cs rename to src/Jung.SimpleWebSocket/Wrappers/TcpClientWrapper.cs diff --git a/Jung.SimpleWebSocket/Wrappers/TcpListenerWrapper.cs b/src/Jung.SimpleWebSocket/Wrappers/TcpListenerWrapper.cs similarity index 100% rename from Jung.SimpleWebSocket/Wrappers/TcpListenerWrapper.cs rename to src/Jung.SimpleWebSocket/Wrappers/TcpListenerWrapper.cs diff --git a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs b/src/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs similarity index 100% rename from Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs rename to src/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs diff --git a/Jung.SimpleWebSocket/Wrappers/WebSocketWrapper.cs b/src/Jung.SimpleWebSocket/Wrappers/WebSocketWrapper.cs similarity index 100% rename from Jung.SimpleWebSocket/Wrappers/WebSocketWrapper.cs rename to src/Jung.SimpleWebSocket/Wrappers/WebSocketWrapper.cs diff --git a/Jung.SimpleWebSocket/docs/README.md b/src/Jung.SimpleWebSocket/docs/README.md similarity index 100% rename from Jung.SimpleWebSocket/docs/README.md rename to src/Jung.SimpleWebSocket/docs/README.md diff --git a/Jung.SimpleWebSocketTest/Jung.SimpleWebSocketTest.csproj b/tests/Jung.SimpleWebSocket.UnitTests/Jung.SimpleWebSocket.UnitTests.csproj similarity index 88% rename from Jung.SimpleWebSocketTest/Jung.SimpleWebSocketTest.csproj rename to tests/Jung.SimpleWebSocket.UnitTests/Jung.SimpleWebSocket.UnitTests.csproj index 90bd5a1..cfa0430 100644 --- a/Jung.SimpleWebSocketTest/Jung.SimpleWebSocketTest.csproj +++ b/tests/Jung.SimpleWebSocket.UnitTests/Jung.SimpleWebSocket.UnitTests.csproj @@ -18,7 +18,7 @@ - + diff --git a/Jung.SimpleWebSocketTest/Mock/ILoggerMockHelper.cs b/tests/Jung.SimpleWebSocket.UnitTests/Mock/ILoggerMockHelper.cs similarity index 87% rename from Jung.SimpleWebSocketTest/Mock/ILoggerMockHelper.cs rename to tests/Jung.SimpleWebSocket.UnitTests/Mock/ILoggerMockHelper.cs index e72d2fa..b784ee1 100644 --- a/Jung.SimpleWebSocketTest/Mock/ILoggerMockHelper.cs +++ b/tests/Jung.SimpleWebSocket.UnitTests/Mock/ILoggerMockHelper.cs @@ -1,7 +1,10 @@ -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; using Moq; -namespace Jung.SimpleWebSocketTest.Mock +namespace Jung.SimpleWebSocket.UnitTests.Mock { internal class ILoggerMockHelper where T : class { diff --git a/Jung.SimpleWebSocketTest/Mock/LoggerMessages.cs b/tests/Jung.SimpleWebSocket.UnitTests/Mock/LoggerMessages.cs similarity index 76% rename from Jung.SimpleWebSocketTest/Mock/LoggerMessages.cs rename to tests/Jung.SimpleWebSocket.UnitTests/Mock/LoggerMessages.cs index b5cc9b3..86af14e 100644 --- a/Jung.SimpleWebSocketTest/Mock/LoggerMessages.cs +++ b/tests/Jung.SimpleWebSocket.UnitTests/Mock/LoggerMessages.cs @@ -1,5 +1,7 @@ - -namespace Jung.SimpleWebSocketTest.Mock +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +namespace Jung.SimpleWebSocket.UnitTests.Mock { internal static class LoggerMessages { diff --git a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs b/tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs similarity index 98% rename from Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs rename to tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs index 8f9bcb3..2621a9f 100644 --- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs +++ b/tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs @@ -1,19 +1,18 @@ // This file is part of the Jung SimpleWebSocket project. // The project is licensed under the MIT license. -using Jung.SimpleWebSocket; using Jung.SimpleWebSocket.Exceptions; using Jung.SimpleWebSocket.Models; -using Jung.SimpleWebSocketTest.Mock; +using Jung.SimpleWebSocket.UnitTests.Mock; using NUnit.Framework; using System.Diagnostics; using System.Net; using System.Runtime.CompilerServices; -// internals of the simple web socket are visible to the test project +// internals of the simple web socket project are visible to the test project // because of the InternalsVisibleTo attribute in the AssemblyInfo.cs -namespace Jung.SimpleWebSocketTest +namespace Jung.SimpleWebSocket.UnitTests { [TestFixture] public class SimpleWebSocketTest diff --git a/Jung.SimpleWebSocketTest/WebContextTest.cs b/tests/Jung.SimpleWebSocket.UnitTests/WebContextTest.cs similarity index 97% rename from Jung.SimpleWebSocketTest/WebContextTest.cs rename to tests/Jung.SimpleWebSocket.UnitTests/WebContextTest.cs index 7999dd7..cc3ad0e 100644 --- a/Jung.SimpleWebSocketTest/WebContextTest.cs +++ b/tests/Jung.SimpleWebSocket.UnitTests/WebContextTest.cs @@ -5,7 +5,10 @@ using Jung.SimpleWebSocket.Models; using NUnit.Framework; -namespace Jung.SimpleWebSocketTest +// internals of the simple web socket project are visible to the test project +// because of the InternalsVisibleTo attribute in the AssemblyInfo.cs + +namespace Jung.SimpleWebSocket.UnitTests { [TestFixture] internal class WebContextTest diff --git a/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs b/tests/Jung.SimpleWebSocket.UnitTests/WebSocketUpgradeHandlerTests.cs similarity index 98% rename from Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs rename to tests/Jung.SimpleWebSocket.UnitTests/WebSocketUpgradeHandlerTests.cs index 158ce22..503df3e 100644 --- a/Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs +++ b/tests/Jung.SimpleWebSocket.UnitTests/WebSocketUpgradeHandlerTests.cs @@ -1,7 +1,6 @@ // This file is part of the Jung SimpleWebSocket project. // The project is licensed under the MIT license. -using Jung.SimpleWebSocket; using Jung.SimpleWebSocket.Contracts; using Jung.SimpleWebSocket.Exceptions; using Jung.SimpleWebSocket.Helpers; @@ -10,7 +9,10 @@ using NUnit.Framework; using System.Text; -namespace Jung.SimpleWebSocketTest +// internals of the simple web socket project are visible to the test project +// because of the InternalsVisibleTo attribute in the AssemblyInfo.cs + +namespace Jung.SimpleWebSocket.UnitTests { public class WebSocketUpgradeHandlerTests { From 1e74d9db36cfea2a26657e090ff17a54dc2250f0 Mon Sep 17 00:00:00 2001 From: Christoph Jung Date: Tue, 11 Feb 2025 20:20:41 +0100 Subject: [PATCH 17/27] Improve method summary --- src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs b/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs index 5519676..58772f3 100644 --- a/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs +++ b/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs @@ -141,7 +141,7 @@ private static void ThrowForResponseContextNotInitialized([NotNull] WebContext? } /// - /// Disposes the upgrade handler. + /// Releases resources that are no longer required. /// private void Cleanup() { From 26614bf5827e0da90b998d15e4cc7d4cb4a46297 Mon Sep 17 00:00:00 2001 From: Christoph Date: Tue, 11 Feb 2025 22:24:14 +0100 Subject: [PATCH 18/27] Remove obsolete project and file --- Jung.SimpleWebSocket.sln | 6 ------ .../Models/EventArguments/ItemExpiredArgs.cs | 10 ---------- 2 files changed, 16 deletions(-) delete mode 100644 src/Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs diff --git a/Jung.SimpleWebSocket.sln b/Jung.SimpleWebSocket.sln index b5af771..d6ee15b 100644 --- a/Jung.SimpleWebSocket.sln +++ b/Jung.SimpleWebSocket.sln @@ -7,8 +7,6 @@ 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", "{144CF5BC-D92F-4BC4-80E9-A2FABA93C8A2}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -23,10 +21,6 @@ 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 - {144CF5BC-D92F-4BC4-80E9-A2FABA93C8A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {144CF5BC-D92F-4BC4-80E9-A2FABA93C8A2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {144CF5BC-D92F-4BC4-80E9-A2FABA93C8A2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {144CF5BC-D92F-4BC4-80E9-A2FABA93C8A2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs deleted file mode 100644 index f2b78be..0000000 --- a/src/Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs +++ /dev/null @@ -1,10 +0,0 @@ -// This file is part of the Jung SimpleWebSocket project. -// The project is licensed under the MIT license. - -namespace Jung.SimpleWebSocket.Models.EventArguments; - -/// -/// Represents the arguments of the event when an item is expired. -/// -/// The item that is expired. -public record ItemExpiredArgs(TValue Item); \ No newline at end of file From 08ff994475795a2b2586d1dcec0709c06f0919b8 Mon Sep 17 00:00:00 2001 From: Christoph Date: Sat, 5 Apr 2025 21:58:31 +0200 Subject: [PATCH 19/27] 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 20/27] 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 21/27] 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 22/27] 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 23/27] 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"); } From 17351283ee501d037b5fc5d30d7cc338411b5ee8 Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 15 Oct 2025 15:41:57 +0200 Subject: [PATCH 24/27] Add example projects Add SendingUpgradeRequestAsync event to client Change argument of client disconnected event from client id to the client object --- Jung.SimpleWebSocket.sln | 39 +++++ .../BasicClientExample.csproj | 14 ++ examples/BasicClientExample/Program.cs | 59 ++++++++ .../BasicServerExample.csproj | 14 ++ examples/BasicServerExample/Program.cs | 93 ++++++++++++ .../BasicUserHandlingClientExample.csproj | 14 ++ .../BasicUserHandlingClientExample/Program.cs | 70 +++++++++ .../BasicUserHandlingServerExample.csproj | 14 ++ .../BasicUserHandlingServerExample/Program.cs | 135 ++++++++++++++++++ .../Contracts/IWebSocketClient.cs | 10 ++ .../Contracts/IWebSocketServer.cs | 9 ++ .../Flows/ClientHandlingFlow.cs | 11 +- .../EventArguments/ClientDisconnectedArgs.cs | 8 +- .../ClientUpgradeRequestReceivedArgs.cs | 4 +- .../SendingUpdateRequestArgs.cs | 13 ++ .../SimpleWebSocketClient.cs | 28 +++- .../SimpleWebSocketServer.cs | 40 ++++-- .../Wrappers/WebSocketUpgradeHandler.cs | 19 ++- .../Tests/DisplayEventsServerTest.cs | 2 +- .../Tests/SendMessagesLoopTest.cs | 2 +- .../SimpleWebSocketTest.cs | 4 +- 21 files changed, 581 insertions(+), 21 deletions(-) create mode 100644 examples/BasicClientExample/BasicClientExample.csproj create mode 100644 examples/BasicClientExample/Program.cs create mode 100644 examples/BasicServerExample/BasicServerExample.csproj create mode 100644 examples/BasicServerExample/Program.cs create mode 100644 examples/BasicUserHandlingClientExample/BasicUserHandlingClientExample.csproj create mode 100644 examples/BasicUserHandlingClientExample/Program.cs create mode 100644 examples/BasicUserHandlingServerExample/BasicUserHandlingServerExample.csproj create mode 100644 examples/BasicUserHandlingServerExample/Program.cs create mode 100644 src/Jung.SimpleWebSocket/Models/EventArguments/SendingUpdateRequestArgs.cs diff --git a/Jung.SimpleWebSocket.sln b/Jung.SimpleWebSocket.sln index 6e0dc3c..fec2fcb 100644 --- a/Jung.SimpleWebSocket.sln +++ b/Jung.SimpleWebSocket.sln @@ -3,12 +3,26 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.11.35222.181 MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{0487DC39-481D-4828-81A5-58CF9BCA2E98}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{68AB7986-ED88-4C74-A447-934ED6D1B657}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jung.SimpleWebSocket", "src\Jung.SimpleWebSocket\Jung.SimpleWebSocket.csproj", "{793B04E9-6326-425A-A29C-A736CFD1E0C0}" 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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicServerExample", "examples\BasicServerExample\BasicServerExample.csproj", "{0C73E461-DE3D-4D14-B81B-732B7C6971A1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicClientExample", "examples\BasicClientExample\BasicClientExample.csproj", "{9D4AD09E-B6FF-4E2A-894E-49B97729E190}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicUserHandlingServerExample", "examples\BasicUserHandlingServerExample\BasicUserHandlingServerExample.csproj", "{A538895A-481B-44A5-8E6F-6D617C3F5378}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicUserHandlingClientExample", "examples\BasicUserHandlingClientExample\BasicUserHandlingClientExample.csproj", "{C79EBA14-EFA6-424D-9C6E-609C98994473}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,10 +41,35 @@ Global {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 + {0C73E461-DE3D-4D14-B81B-732B7C6971A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C73E461-DE3D-4D14-B81B-732B7C6971A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C73E461-DE3D-4D14-B81B-732B7C6971A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C73E461-DE3D-4D14-B81B-732B7C6971A1}.Release|Any CPU.Build.0 = Release|Any CPU + {9D4AD09E-B6FF-4E2A-894E-49B97729E190}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9D4AD09E-B6FF-4E2A-894E-49B97729E190}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9D4AD09E-B6FF-4E2A-894E-49B97729E190}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9D4AD09E-B6FF-4E2A-894E-49B97729E190}.Release|Any CPU.Build.0 = Release|Any CPU + {A538895A-481B-44A5-8E6F-6D617C3F5378}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A538895A-481B-44A5-8E6F-6D617C3F5378}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A538895A-481B-44A5-8E6F-6D617C3F5378}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A538895A-481B-44A5-8E6F-6D617C3F5378}.Release|Any CPU.Build.0 = Release|Any CPU + {C79EBA14-EFA6-424D-9C6E-609C98994473}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C79EBA14-EFA6-424D-9C6E-609C98994473}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C79EBA14-EFA6-424D-9C6E-609C98994473}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C79EBA14-EFA6-424D-9C6E-609C98994473}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {793B04E9-6326-425A-A29C-A736CFD1E0C0} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {26725C3C-8E90-49AC-9EE4-2A77ADB2229D} = {68AB7986-ED88-4C74-A447-934ED6D1B657} + {D052400A-9F1E-4F2E-98B9-AF74A7A16A2F} = {68AB7986-ED88-4C74-A447-934ED6D1B657} + {0C73E461-DE3D-4D14-B81B-732B7C6971A1} = {0487DC39-481D-4828-81A5-58CF9BCA2E98} + {9D4AD09E-B6FF-4E2A-894E-49B97729E190} = {0487DC39-481D-4828-81A5-58CF9BCA2E98} + {A538895A-481B-44A5-8E6F-6D617C3F5378} = {0487DC39-481D-4828-81A5-58CF9BCA2E98} + {C79EBA14-EFA6-424D-9C6E-609C98994473} = {0487DC39-481D-4828-81A5-58CF9BCA2E98} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5F0E3FEC-7DDE-4E02-941B-CEF2DE33DB1C} EndGlobalSection diff --git a/examples/BasicClientExample/BasicClientExample.csproj b/examples/BasicClientExample/BasicClientExample.csproj new file mode 100644 index 0000000..6467dee --- /dev/null +++ b/examples/BasicClientExample/BasicClientExample.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/examples/BasicClientExample/Program.cs b/examples/BasicClientExample/Program.cs new file mode 100644 index 0000000..fec5318 --- /dev/null +++ b/examples/BasicClientExample/Program.cs @@ -0,0 +1,59 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Jung.SimpleWebSocket; + +namespace BasicClientExample +{ + internal class Program + { + /// + /// An example of a basic WebSocket client using the Jung.SimpleWebSocket library. + /// + /// + static void Main(string[] args) + { + // Create the WebSocket client and connect to the server at ws://127.0.0.1:8085/chat + using var simpleWebSocketClient = new SimpleWebSocketClient("127.0.0.1", 8085, "/chat"); + + // Subscribe to client events + simpleWebSocketClient.Disconnected += (s, e) => Console.WriteLine($"Disconnected from the server. Reason: {e.ClosingStatusDescription}"); + simpleWebSocketClient.MessageReceived += (s, e) => Console.WriteLine($"Message received from server: {e.Message}"); + simpleWebSocketClient.BinaryMessageReceived += (s, e) => Console.WriteLine($"Binary message received from server: {BitConverter.ToString(e.Message)}"); + + try + { + // Connect to the server + simpleWebSocketClient.ConnectAsync().GetAwaiter().GetResult(); + + // Simulate any delay + Thread.Sleep(1000); + + // Send a message to the server + Console.WriteLine("Sending message to the server: Hello, Server!"); + simpleWebSocketClient.SendMessageAsync("Hello, Server!").GetAwaiter().GetResult(); + + // Keep the server running until a key is pressed + Console.WriteLine("Press Enter to stop the server..."); + Console.ReadKey(); + + // You do not have to explicitly disconnect the client because of the using statement + // simpleWebSocketClient.DisconnectAsync("Client is shutting down").Wait(); + } + catch (Exception exception) + { + + var exceptionMessage = exception.Message; + if (exception.InnerException != null) + { + exceptionMessage += $" Inner exception: {exception.InnerException.Message}"; + } + Console.WriteLine($"An error occurred: {exceptionMessage}"); + + // Keep the console application running until a key is pressed + Console.WriteLine("Press Enter close this window..."); + Console.ReadKey(); + } + } + } +} diff --git a/examples/BasicServerExample/BasicServerExample.csproj b/examples/BasicServerExample/BasicServerExample.csproj new file mode 100644 index 0000000..6467dee --- /dev/null +++ b/examples/BasicServerExample/BasicServerExample.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/examples/BasicServerExample/Program.cs b/examples/BasicServerExample/Program.cs new file mode 100644 index 0000000..fcd20e2 --- /dev/null +++ b/examples/BasicServerExample/Program.cs @@ -0,0 +1,93 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Jung.SimpleWebSocket; +using Jung.SimpleWebSocket.Models; +using Jung.SimpleWebSocket.Models.EventArguments; + +namespace BasicServerExample +{ + internal class Program + { + /// + /// An example of a basic WebSocket server using the Jung.SimpleWebSocket library. + /// + /// + static void Main(string[] args) + { + // Create server options + var serverOptions = new SimpleWebSocketServerOptions() + { + // Set the server to listen on port 8080 and localhost + Port = 8085, + LocalIpAddress = new System.Net.IPAddress([127, 0, 0, 1]) + }; + + // Create the WebSocket server + using var simpleWebSocketServer = new SimpleWebSocketServer(serverOptions); + + // Subscribe to the server events + simpleWebSocketServer.ClientConnected += (s, e) => Console.WriteLine($"Client connected: {e.ClientId}"); + simpleWebSocketServer.ClientDisconnected += (s, e) => Console.WriteLine($"Client disconnected: {e.Client.Id}, Reason: {e.ClosingStatusDescription}"); + simpleWebSocketServer.MessageReceived += (s, e) => Console.WriteLine($"Message received from {e.ClientId}: {e.Message}"); + simpleWebSocketServer.BinaryMessageReceived += SimpleWebSocketServer_BinaryMessageReceived; + simpleWebSocketServer.ClientUpgradeRequestReceivedAsync += SimpleWebSocketServer_ClientUpgradeRequestReceivedAsync; + + // Start the server + simpleWebSocketServer.Start(); + Console.WriteLine("Server started on ws://127.0.0.1:8085"); + + // Keep the server running until a key is pressed + Console.WriteLine("Press Enter to stop the server..."); + Console.ReadKey(); + + // You do not have to explicitly shutdown the server because of the using statement + // simpleWebSocketServer.ShutdownServer().Wait(); + } + + /// + /// This event is triggered when a client sends a binary message to the server. + /// + /// The server that received the binary message. + /// The event arguments containing the client ID and the binary message. + private static void SimpleWebSocketServer_BinaryMessageReceived(object? sender, ClientBinaryMessageReceivedArgs e) + { + // Convert the binary message to a hex string + string hex = BitConverter.ToString(e.Message).Replace("-", " "); + Console.WriteLine($"Binary message received from {e.ClientId}: {hex}"); + } + + /// + /// This event is triggered when a client sends an upgrade request to the server. + /// + /// The server that received the upgrade request. + /// The event arguments containing the client and the request details. + /// The cancellation token of the server. + /// A task that represents the asynchronous operation. + private static Task SimpleWebSocketServer_ClientUpgradeRequestReceivedAsync(object sender, ClientUpgradeRequestReceivedArgs e, CancellationToken cancellationToken) + { + Console.WriteLine($"Upgrade request received from {e.Client.Id} for {e.WebContext.RequestPath}"); + + // Do something with the upgrade request + // For example save request path to client properties + e.Client.Properties["RequestPath"] = e.WebContext.RequestPath; + + // Or reject the upgrade request if the request path is not /chat + if (e.WebContext.RequestPath != "/chat") + { + // It is recommended to set a status code higher than 400 to reject the upgrade request. + e.ResponseContext.StatusCode = System.Net.HttpStatusCode.Forbidden; // 403 Forbidden + // Handle the request and reject it + e.AcceptRequest = false; + } + else + { + // Handle the request and accept it + e.AcceptRequest = true; + } + + // Because this is an async event, we need to return a completed task. + return Task.CompletedTask; + } + } +} diff --git a/examples/BasicUserHandlingClientExample/BasicUserHandlingClientExample.csproj b/examples/BasicUserHandlingClientExample/BasicUserHandlingClientExample.csproj new file mode 100644 index 0000000..6467dee --- /dev/null +++ b/examples/BasicUserHandlingClientExample/BasicUserHandlingClientExample.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/examples/BasicUserHandlingClientExample/Program.cs b/examples/BasicUserHandlingClientExample/Program.cs new file mode 100644 index 0000000..67eff18 --- /dev/null +++ b/examples/BasicUserHandlingClientExample/Program.cs @@ -0,0 +1,70 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Jung.SimpleWebSocket; +using Jung.SimpleWebSocket.Models.EventArguments; + +namespace BasicUserHandlingClientExample +{ + internal class Program + { + /// + /// An example of a basic WebSocket client using the Jung.SimpleWebSocket library. + /// + /// + static void Main(string[] args) + { + // Create the WebSocket client and connect to the server at ws://127.0.0.1:8085/chat + using var simpleWebSocketClient = new SimpleWebSocketClient("127.0.0.1", 8085, "/chat"); + + // Subscribe to client events + simpleWebSocketClient.Disconnected += (s, e) => Console.WriteLine($"Disconnected from the server. Reason: {e.ClosingStatusDescription}"); + simpleWebSocketClient.MessageReceived += (s, e) => Console.WriteLine($"Message received from server: {e.Message}"); + simpleWebSocketClient.BinaryMessageReceived += (s, e) => Console.WriteLine($"Binary message received from server: {BitConverter.ToString(e.Message)}"); + simpleWebSocketClient.SendingUpgradeRequestAsync += SimpleWebSocketClient_SendingUpgradeRequestAsync; + + try + { + // Connect to the server + simpleWebSocketClient.ConnectAsync().GetAwaiter().GetResult(); + + // Simulate any delay + Thread.Sleep(1000); + + // Send a message to the server + Console.WriteLine("Sending message to the server: Hello, Server!"); + simpleWebSocketClient.SendMessageAsync("Hello, Server!").GetAwaiter().GetResult(); + + // Keep the server running until a key is pressed + Console.WriteLine("Press Enter to stop the server..."); + Console.ReadKey(); + + // You do not have to explicitly disconnect the client because of the using statement + // simpleWebSocketClient.DisconnectAsync("Client is shutting down").GetAwaiter().GetResult(); + } + catch (Exception exception) + { + var exceptionMessage = exception.Message; + if (exception.InnerException != null) + { + exceptionMessage += $" Inner exception: {exception.InnerException.Message}"; + } + Console.WriteLine($"An error occurred: {exceptionMessage}"); + + // Keep the console application running until a key is pressed + Console.WriteLine("Press Enter close this window..."); + Console.ReadKey(); + } + } + + private static Task SimpleWebSocketClient_SendingUpgradeRequestAsync(object sender, SendingUpgradeRequestArgs e, CancellationToken cancellationToken) + { + // Add a custom header to the upgrade request + e.WebContext.Headers["User-Name"] = "Alice"; + + // Because this is a synchronous method, we return a completed task. + return Task.CompletedTask; + + } + } +} diff --git a/examples/BasicUserHandlingServerExample/BasicUserHandlingServerExample.csproj b/examples/BasicUserHandlingServerExample/BasicUserHandlingServerExample.csproj new file mode 100644 index 0000000..6467dee --- /dev/null +++ b/examples/BasicUserHandlingServerExample/BasicUserHandlingServerExample.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/examples/BasicUserHandlingServerExample/Program.cs b/examples/BasicUserHandlingServerExample/Program.cs new file mode 100644 index 0000000..437f1ab --- /dev/null +++ b/examples/BasicUserHandlingServerExample/Program.cs @@ -0,0 +1,135 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Jung.SimpleWebSocket; +using Jung.SimpleWebSocket.Models; +using Jung.SimpleWebSocket.Models.EventArguments; +using System.Collections.Concurrent; + +namespace BasicUserHandlingServerExample +{ + internal class Program + { + // A thread-safe dictionary to store connected users + private static readonly ConcurrentDictionary _connectedUsers = []; + + /// + /// An example of a basic WebSocket server using the Jung.SimpleWebSocket library. + /// + /// + static void Main(string[] args) + { + // Create server options + var serverOptions = new SimpleWebSocketServerOptions() + { + // Set the server to listen on port 8080 and localhost + Port = 8085, + LocalIpAddress = new System.Net.IPAddress([127, 0, 0, 1]) + }; + + // Create the WebSocket server + using var simpleWebSocketServer = new SimpleWebSocketServer(serverOptions); + + // Subscribe to the server events + simpleWebSocketServer.ClientConnected += SimpleWebSocketServer_ClientConnected; + simpleWebSocketServer.ClientDisconnected += SimpleWebSocketServer_ClientDisconnected; + simpleWebSocketServer.MessageReceived += SimpleWebSocketServer_MessageReceived; + simpleWebSocketServer.BinaryMessageReceived += SimpleWebSocketServer_BinaryMessageReceived; + simpleWebSocketServer.ClientUpgradeRequestReceivedAsync += SimpleWebSocketServer_ClientUpgradeRequestReceivedAsync; + + // Start the server + simpleWebSocketServer.Start(); + Console.WriteLine($"Server started on ws://{serverOptions.LocalIpAddress}:{serverOptions.Port}"); + + // Keep the server running until a key is pressed + Console.WriteLine("Press Enter to stop the server..."); + Console.ReadKey(); + + // You do not have to explicitly shutdown the server because of the using statement + // simpleWebSocketServer.ShutdownServer().Wait(); + } + + + private static void SimpleWebSocketServer_ClientConnected(object? sender, ClientConnectedArgs e) + { + if (((SimpleWebSocketServer)sender!).GetClientById(e.ClientId) is WebSocketServerClient client) + { + Console.WriteLine($"User name {client.Properties["UserName"]} connected successfully."); + } + } + + private static void SimpleWebSocketServer_ClientDisconnected(object? sender, ClientDisconnectedArgs e) + { + // Remove the user from the connected users list + var userName = e.Client.Properties["UserName"]?.ToString() ?? "Unknown"; + _connectedUsers.TryRemove(userName, out _); + Console.WriteLine($"User name {userName} disconnected."); + } + + private static void SimpleWebSocketServer_MessageReceived(object? sender, ClientMessageReceivedArgs e) + { + if (((SimpleWebSocketServer)sender!).GetClientById(e.ClientId) is WebSocketServerClient client) + { + Console.WriteLine($"Message received from {client.Properties["UserName"]}: {e.Message}"); + } + } + + /// + /// This event is triggered when a client sends a binary message to the server. + /// + /// The server that received the binary message. + /// The event arguments containing the client ID and the binary message. + private static void SimpleWebSocketServer_BinaryMessageReceived(object? sender, ClientBinaryMessageReceivedArgs e) + { + // Convert the binary message to a hex string + string hex = BitConverter.ToString(e.Message).Replace("-", " "); + Console.WriteLine($"Binary message received from {e.ClientId}: {hex}"); + } + + /// + /// This event is triggered when a client sends an upgrade request to the server. + /// + /// The server that received the upgrade request. + /// The event arguments containing the client and the request details. + /// The cancellation token of the server. + /// A task that represents the asynchronous operation. + private static Task SimpleWebSocketServer_ClientUpgradeRequestReceivedAsync(object sender, ClientUpgradeRequestReceivedArgs e, CancellationToken cancellationToken) + { + Console.WriteLine($"Upgrade request received from {e.Client.Id} for {e.WebContext.RequestPath}"); + + // Get the user name from the request headers + var userName = e.WebContext.Headers["User-Name"]; + if (userName != null) + { + Console.WriteLine($"Request with user name: {userName}"); + + // Check if the user name is already connected + if (_connectedUsers.ContainsKey(userName)) + { + Console.WriteLine($"User name {userName} is already connected. Rejecting the upgrade request."); + e.ResponseContext.StatusCode = System.Net.HttpStatusCode.Conflict; // 409 Conflict + e.ResponseContext.BodyContent = "User name is already connected"; + e.AcceptRequest = false; + } + else + { + // Add the user name to the connected users list + _connectedUsers.TryAdd(userName, e.Client.Id); + // Store the user name in the client properties for future reference + e.Client.Properties["UserName"] = userName; + } + } + else + { + // It is recommended to set a status code higher than 400 to reject the upgrade request. + e.ResponseContext.StatusCode = System.Net.HttpStatusCode.BadRequest; // 400 Bad Request + e.ResponseContext.BodyContent = "Missing User-Name header"; + // Handle the request and reject it + e.AcceptRequest = false; + } + + // Because this is an async event, we need to return a completed task. + return Task.CompletedTask; + } + } +} diff --git a/src/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs b/src/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs index 1a73240..9c00f62 100644 --- a/src/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs +++ b/src/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs @@ -2,6 +2,7 @@ // The project is licensed under the MIT license. using Jung.SimpleWebSocket.Delegates; +using Jung.SimpleWebSocket.Models.EventArguments; namespace Jung.SimpleWebSocket.Contracts; @@ -45,6 +46,15 @@ public interface IWebSocketClient : IDisposable /// event DisconnectedEventHandler? Disconnected; + /// + /// Occurs before an upgrade request is sent, allowing the request to be inspected or modified asynchronously. + /// + /// This event is triggered when an upgrade request is about to be sent. Subscribers can use this + /// event to inspect or modify the request by handling the parameter. The + /// event handler is asynchronous, so any modifications or operations should be performed within the provided + /// asynchronous context. + event AsyncEventHandler? SendingUpgradeRequestAsync; + /// /// Sends a message to all connected clients asynchronously. /// diff --git a/src/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs b/src/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs index a12093e..03d8b27 100644 --- a/src/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs +++ b/src/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs @@ -5,6 +5,7 @@ using Jung.SimpleWebSocket.Exceptions; using Jung.SimpleWebSocket.Models; using Jung.SimpleWebSocket.Models.EventArguments; +using System.Diagnostics.CodeAnalysis; using System.Net; namespace Jung.SimpleWebSocket.Contracts; @@ -71,6 +72,14 @@ public interface IWebSocketServer : IDisposable /// The client WebSocketServerClient GetClientById(string clientId); + /// + /// Attempts to get a client by its id. + /// + /// The id of the client + /// The client if found, otherwise null + /// if the client was found, otherwise ." + bool TryGetClientById(string clientId, [NotNullWhen(true)] out WebSocketServerClient? client); + /// /// Sends a message to all connected clients asynchronously. /// diff --git a/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs b/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs index c5f68b8..fa1288d 100644 --- a/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs +++ b/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs @@ -89,7 +89,16 @@ internal async Task AcceptWebSocketAsync() /// The response context to send to the client. internal async Task RejectWebSocketAsync(WebContext responseContext) { - // The client is rejected + // If the status code is SwitchingProtocols, change it to BadRequest + if (responseContext.StatusCode == System.Net.HttpStatusCode.SwitchingProtocols) + { + // If we would send a SwitchingProtocols status code, the client would expect a WebSocket connection. + // We want to reject the connection, so we send a BadRequest status code. + // We could get here if the user sets the status code to SwitchingProtocols in the upgrade event. + responseContext.StatusCode = System.Net.HttpStatusCode.BadRequest; + } + + // Reject the client await _upgradeHandler!.RejectWebSocketAsync(responseContext, _cancellationToken).ConfigureAwait(false); Cleanup(); } diff --git a/src/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs index 51cf852..eda1d8e 100644 --- a/src/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs +++ b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs @@ -7,5 +7,9 @@ namespace Jung.SimpleWebSocket.Models.EventArguments; /// 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); +/// The Client that disconnected from the server. +public record ClientDisconnectedArgs( + string? ClosingStatusDescription, + // We use the client object here instead of just the client ID to give more context about the disconnected client. + // When the event is fired, the client is already removed from the active clients list, so we can't access it there. + WebSocketServerClient Client); diff --git a/src/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs index 0f238b0..e7b3150 100644 --- a/src/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs +++ b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs @@ -16,9 +16,9 @@ public record ClientUpgradeRequestReceivedArgs(WebSocketServerClient Client, Web private WebContext? _responseContext; /// - /// Gets or sets a value indicating whether the upgrade request should be handled. + /// Gets or sets a value indicating whether the upgrade request should be Accepted. Default is true. /// - public bool Handle { get; set; } = true; + public bool AcceptRequest { get; set; } = true; /// /// The context that is being use to response to the client. diff --git a/src/Jung.SimpleWebSocket/Models/EventArguments/SendingUpdateRequestArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/SendingUpdateRequestArgs.cs new file mode 100644 index 0000000..4d59437 --- /dev/null +++ b/src/Jung.SimpleWebSocket/Models/EventArguments/SendingUpdateRequestArgs.cs @@ -0,0 +1,13 @@ +// This file is part of the Jung SimpleWebSocket project. +// The project is licensed under the MIT license. + +using Microsoft.Extensions.Logging; + +namespace Jung.SimpleWebSocket.Models.EventArguments; + +/// +/// Represents the arguments of the event when a upgrade request is sent to a server. +/// +/// The context of the request. +/// The current Logger. +public record SendingUpgradeRequestArgs(WebContext WebContext, ILogger? Logger); diff --git a/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs b/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs index 89e5383..f77671e 100644 --- a/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs +++ b/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs @@ -6,6 +6,7 @@ using Jung.SimpleWebSocket.Exceptions; using Jung.SimpleWebSocket.Models; using Jung.SimpleWebSocket.Models.EventArguments; +using Jung.SimpleWebSocket.Utility; using Jung.SimpleWebSocket.Wrappers; using Microsoft.Extensions.Logging; using System.Net.Sockets; @@ -43,6 +44,9 @@ public class SimpleWebSocketClient(string hostName, int port, string requestPath /// public event BinaryMessageReceivedEventHandler? BinaryMessageReceived; + /// + public event AsyncEventHandler? SendingUpgradeRequestAsync; + /// /// The CancellationTokenSource for the client. /// @@ -114,7 +118,7 @@ public async Task ConnectAsync(CancellationToken? cancellationToken = null) { throw new WebSocketConnectionException(message: "Error connecting to Server", innerException: exception); } - else if (exception is WebSocketException) + else if (exception is WebSocketException || exception is WebSocketUpgradeException) { throw; } @@ -173,6 +177,7 @@ private async Task HandleWebSocketInitiation(TcpClientWrapper client, Cancellati var socketWrapper = new WebSocketUpgradeHandler(_stream); var requestContext = WebContext.CreateRequest(HostName, Port, RequestPath); + requestContext = await RaiseUpgradeEventAsync(requestContext, cancellationToken).ConfigureAwait(false); await socketWrapper.SendUpgradeRequestAsync(requestContext, cancellationToken).ConfigureAwait(false); var response = await socketWrapper.AwaitContextAsync(cancellationToken).ConfigureAwait(false); WebSocketUpgradeHandler.ValidateUpgradeResponse(response, requestContext); @@ -180,6 +185,19 @@ private async Task HandleWebSocketInitiation(TcpClientWrapper client, Cancellati _webSocket = socketWrapper.CreateWebSocket(isServer: false); } + /// + /// Raises the upgrade event. + /// + /// The request context to use for the upgrade event + /// The cancellation token + /// The event arguments of the upgrade request. + internal async Task RaiseUpgradeEventAsync(WebContext requestContext, CancellationToken cancellationToken) + { + var eventArgs = new SendingUpgradeRequestArgs(requestContext, _logger); + await AsyncEventRaiser.RaiseAsync(SendingUpgradeRequestAsync, this, eventArgs, cancellationToken).ConfigureAwait(false); + return requestContext; + } + /// public async Task SendMessageAsync(string message, CancellationToken? cancellationToken = null) { @@ -264,9 +282,17 @@ public void Dispose() { if (Interlocked.Exchange(ref _disposed, 1) == 0) { + // Unsubscribe all event handlers + Disconnected = null; + MessageReceived = null; + BinaryMessageReceived = null; + SendingUpgradeRequestAsync = null; + + // Dispose managed resources _cancellationTokenSource?.Cancel(); _stream?.Dispose(); _client?.Dispose(); + GC.SuppressFinalize(this); } } diff --git a/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs index fd296a6..6d02221 100644 --- a/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs +++ b/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.WebSockets; using System.Text; @@ -267,8 +268,26 @@ public WebSocketServerClient GetClientById(string clientId) { ThrowIfDisposed(); - if (!ActiveClients.TryGetValue(clientId, out var client)) throw new WebSocketServerException(message: "Client not found"); - return client; + if (TryGetClientById(clientId, out var client)) + { + return client; + } + throw new WebSocketServerException(message: "Client not found"); + } + + /// + public bool TryGetClientById(string clientId, [NotNullWhen(true)] out WebSocketServerClient? client) + { + ThrowIfDisposed(); + + if (ActiveClients.TryGetValue(clientId, out client)) + { + if (client != null) + { + return true; + } + } + return false; } /// @@ -305,7 +324,7 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT var eventArgs = await flow.RaiseUpgradeEventAsync(ClientUpgradeRequestReceivedAsync).ConfigureAwait(false); // Respond to the upgrade request - if (eventArgs.Handle) + if (eventArgs.AcceptRequest) { // Accept the WebSocket connection await flow.AcceptWebSocketAsync().ConfigureAwait(false); @@ -333,10 +352,6 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT { // Ignore the exception, because it is thrown when cancellation is requested } - catch (UserNotHandledException userNotHandledException) - { - await flow.RejectWebSocketAsync(userNotHandledException.ResponseContext).ConfigureAwait(false); - } catch (Exception exception) { Logger?.LogError(exception, "Error while handling the Client {clientId}", flow.Client.Id); @@ -407,7 +422,7 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C // if we leave the loop, the client disconnected if (!IsShuttingDown) { - AsyncEventRaiser.RaiseAsyncInNewTask(ClientDisconnected, this, new ClientDisconnectedArgs(closeStatusDescription, client.Id), cancellationToken); + AsyncEventRaiser.RaiseAsyncInNewTask(ClientDisconnected, this, new ClientDisconnectedArgs(closeStatusDescription, client), cancellationToken); } } } @@ -422,10 +437,19 @@ public void Dispose() try { + // unsubscribe all event handlers + ClientConnected = null; + ClientDisconnected = null; + MessageReceived = null; + BinaryMessageReceived = null; + ClientUpgradeRequestReceivedAsync = null; + + // shutdown server and free resources ShutdownServer().GetAwaiter().GetResult(); _cancellationTokenSource?.Cancel(); _tcpListener?.Dispose(); _tcpListener = null; + GC.SuppressFinalize(this); } finally diff --git a/src/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs b/src/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs index 57ded7b..e7ea051 100644 --- a/src/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs +++ b/src/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs @@ -105,7 +105,7 @@ private async Task SendWebSocketResponseHeaders(WebContext context, Cancellation private async Task SendWebSocketRejectResponse(WebContext context, CancellationToken cancellationToken) { var sb = new StringBuilder( - $"HTTP/1.1 409 Conflict\r\n"); + $"HTTP/1.1 {(int)context.StatusCode} {context.StatusDescription}\r\n"); AddHeaders(context, sb); CompleteHeaderSection(sb); AddBody(context, sb); @@ -299,9 +299,22 @@ internal IWebSocket CreateWebSocket(bool isServer, TimeSpan? keepAliveInterval = internal async Task RejectWebSocketAsync(WebContext response, CancellationToken cancellationToken) { + // This header is optional, but recommended to inform the client that the connection will be closed response.Headers.Add("Connection", "close"); - response.Headers.Add("Content-Type", "text/plain"); - response.Headers.Add("Content-Length", response.BodyContent.Length.ToString()); + + // If there is body content, ensure Content-Type and Content-Length headers are set + if (!string.IsNullOrEmpty(response.BodyContent)) + { + // Set default Content-Type if not already set + if (response.Headers["Content-Type"] == null) + { + response.Headers.Add("Content-Type", "text/plain"); + } + // Set Content-Length based on the body content length + response.Headers.Add("Content-Length", response.BodyContent.Length.ToString()); + } + + // Send the rejection response await SendWebSocketRejectResponse(response, cancellationToken).ConfigureAwait(false); } } \ No newline at end of file diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/DisplayEventsServerTest.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/DisplayEventsServerTest.cs index 129ae9e..2ad1875 100644 --- a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/DisplayEventsServerTest.cs +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/DisplayEventsServerTest.cs @@ -56,7 +56,7 @@ private void SimpleWebSocketServer_ClientConnected(object? sender, ClientConnect private void SimpleWebSocketServer_ClientDisconnected(object? sender, ClientDisconnectedArgs e) { - _logger.LogInformation("Client disconnected: {ClientId}", e.ClientId); + _logger.LogInformation("Client disconnected: {ClientId}", e.Client.Id); } private void SimpleWebSocketServer_MessageReceived(object? sender, ClientMessageReceivedArgs e) diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs index 9bd26ba..1638852 100644 --- a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs +++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs @@ -15,7 +15,7 @@ internal override async Task RunAsync() var cancellationTokenSource = new CancellationTokenSource(); var token = cancellationTokenSource.Token; - using var client = new SimpleWebSocketClient("localhost", 8085, "", clientLogger); + using var client = new SimpleWebSocketClient("localhost", 8085, string.Empty, clientLogger); InitializeClientEvents(client); diff --git a/tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs b/tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs index 762f70d..4699925 100644 --- a/tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs +++ b/tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs @@ -167,7 +167,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() var IpAddress = (args.Client.RemoteEndPoint as IPEndPoint)?.Address; if (IpAddress == null) { - args.Handle = false; + args.AcceptRequest = false; return; } @@ -177,7 +177,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld() { args.ResponseContext.StatusCode = HttpStatusCode.Forbidden; args.ResponseContext.BodyContent = "Connection only possible via local network."; - args.Handle = false; + args.AcceptRequest = false; } args.Client.Properties["test"] = "test"; }; From b79265ba7e6f6d5d840f0f2c7977d30b7695c631 Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 15 Oct 2025 17:38:54 +0200 Subject: [PATCH 25/27] Complete xml summaries of the examples --- .../BasicUserHandlingClientExample/Program.cs | 9 ++++++++- .../BasicUserHandlingServerExample/Program.cs | 17 ++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/examples/BasicUserHandlingClientExample/Program.cs b/examples/BasicUserHandlingClientExample/Program.cs index 67eff18..66e3490 100644 --- a/examples/BasicUserHandlingClientExample/Program.cs +++ b/examples/BasicUserHandlingClientExample/Program.cs @@ -57,6 +57,14 @@ static void Main(string[] args) } } + /// + /// Handles the event triggered before a WebSocket upgrade request is sent, allowing customization of the + /// request. + /// + /// The source of the event, the WebSocket client instance. + /// The event arguments containing details about the upgrade request, including headers and other context. + /// The cancellation token of the client. + /// A completed task, as this method performs its operation synchronously. private static Task SimpleWebSocketClient_SendingUpgradeRequestAsync(object sender, SendingUpgradeRequestArgs e, CancellationToken cancellationToken) { // Add a custom header to the upgrade request @@ -64,7 +72,6 @@ private static Task SimpleWebSocketClient_SendingUpgradeRequestAsync(object send // Because this is a synchronous method, we return a completed task. return Task.CompletedTask; - } } } diff --git a/examples/BasicUserHandlingServerExample/Program.cs b/examples/BasicUserHandlingServerExample/Program.cs index 437f1ab..b30f4f7 100644 --- a/examples/BasicUserHandlingServerExample/Program.cs +++ b/examples/BasicUserHandlingServerExample/Program.cs @@ -49,7 +49,11 @@ static void Main(string[] args) // simpleWebSocketServer.ShutdownServer().Wait(); } - + /// + /// This event is triggered when a client successfully connects to the server. + /// + /// The server that the client connected to. + /// The event arguments containing the client ID. private static void SimpleWebSocketServer_ClientConnected(object? sender, ClientConnectedArgs e) { if (((SimpleWebSocketServer)sender!).GetClientById(e.ClientId) is WebSocketServerClient client) @@ -58,6 +62,11 @@ private static void SimpleWebSocketServer_ClientConnected(object? sender, Client } } + /// + /// This event is triggered when a client disconnects from the server. + /// + /// The server that the client disconnected from. + /// The event arguments containing the client and the disconnection details. private static void SimpleWebSocketServer_ClientDisconnected(object? sender, ClientDisconnectedArgs e) { // Remove the user from the connected users list @@ -66,6 +75,12 @@ private static void SimpleWebSocketServer_ClientDisconnected(object? sender, Cli Console.WriteLine($"User name {userName} disconnected."); } + + /// + /// This event is triggered when a text message is received from a client. + /// + /// The server that received the message. + /// The event arguments containing the client ID and the message. private static void SimpleWebSocketServer_MessageReceived(object? sender, ClientMessageReceivedArgs e) { if (((SimpleWebSocketServer)sender!).GetClientById(e.ClientId) is WebSocketServerClient client) From 81a56175e59e76c240fca10c9de345da836c65a9 Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 15 Oct 2025 18:06:43 +0200 Subject: [PATCH 26/27] Standardize output of received binary messages --- examples/BasicServerExample/Program.cs | 2 +- examples/BasicUserHandlingServerExample/Program.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/BasicServerExample/Program.cs b/examples/BasicServerExample/Program.cs index fcd20e2..78d92c9 100644 --- a/examples/BasicServerExample/Program.cs +++ b/examples/BasicServerExample/Program.cs @@ -53,7 +53,7 @@ static void Main(string[] args) private static void SimpleWebSocketServer_BinaryMessageReceived(object? sender, ClientBinaryMessageReceivedArgs e) { // Convert the binary message to a hex string - string hex = BitConverter.ToString(e.Message).Replace("-", " "); + string hex = BitConverter.ToString(e.Message); Console.WriteLine($"Binary message received from {e.ClientId}: {hex}"); } diff --git a/examples/BasicUserHandlingServerExample/Program.cs b/examples/BasicUserHandlingServerExample/Program.cs index b30f4f7..7103c33 100644 --- a/examples/BasicUserHandlingServerExample/Program.cs +++ b/examples/BasicUserHandlingServerExample/Program.cs @@ -97,7 +97,7 @@ private static void SimpleWebSocketServer_MessageReceived(object? sender, Client private static void SimpleWebSocketServer_BinaryMessageReceived(object? sender, ClientBinaryMessageReceivedArgs e) { // Convert the binary message to a hex string - string hex = BitConverter.ToString(e.Message).Replace("-", " "); + string hex = BitConverter.ToString(e.Message); Console.WriteLine($"Binary message received from {e.ClientId}: {hex}"); } From 600f77d091f65d2894f4c46b7001087a6af11f6f Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 15 Oct 2025 18:36:41 +0200 Subject: [PATCH 27/27] Fix false comments --- examples/BasicServerExample/Program.cs | 2 +- examples/BasicUserHandlingServerExample/Program.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/BasicServerExample/Program.cs b/examples/BasicServerExample/Program.cs index 78d92c9..c4f7934 100644 --- a/examples/BasicServerExample/Program.cs +++ b/examples/BasicServerExample/Program.cs @@ -18,7 +18,7 @@ static void Main(string[] args) // Create server options var serverOptions = new SimpleWebSocketServerOptions() { - // Set the server to listen on port 8080 and localhost + // Set the server to listen on port 8085 and localhost Port = 8085, LocalIpAddress = new System.Net.IPAddress([127, 0, 0, 1]) }; diff --git a/examples/BasicUserHandlingServerExample/Program.cs b/examples/BasicUserHandlingServerExample/Program.cs index 7103c33..2850237 100644 --- a/examples/BasicUserHandlingServerExample/Program.cs +++ b/examples/BasicUserHandlingServerExample/Program.cs @@ -22,7 +22,7 @@ static void Main(string[] args) // Create server options var serverOptions = new SimpleWebSocketServerOptions() { - // Set the server to listen on port 8080 and localhost + // Set the server to listen on port 8085 and localhost Port = 8085, LocalIpAddress = new System.Net.IPAddress([127, 0, 0, 1]) };