diff --git a/Jung.SimpleWebSocket.sln b/Jung.SimpleWebSocket.sln
index ed5da37..fec2fcb 100644
--- a/Jung.SimpleWebSocket.sln
+++ b/Jung.SimpleWebSocket.sln
@@ -3,9 +3,25 @@ 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("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jung.SimpleWebSocketTest", "Jung.SimpleWebSocketTest\Jung.SimpleWebSocketTest.csproj", "{26725C3C-8E90-49AC-9EE4-2A77ADB2229D}"
+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
@@ -21,10 +37,39 @@ 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
+ {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/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs b/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs
deleted file mode 100644
index fe16b81..0000000
--- a/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs
+++ /dev/null
@@ -1,11 +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 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
diff --git a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/Jung.SimpleWebSocket/SimpleWebSocketServer.cs
deleted file mode 100644
index 943ca4a..0000000
--- a/Jung.SimpleWebSocket/SimpleWebSocketServer.cs
+++ /dev/null
@@ -1,314 +0,0 @@
-// This file is part of the Jung SimpleWebSocket project.
-// The project is licensed under the MIT license.
-
-using Jung.SimpleWebSocket.Contracts;
-using Jung.SimpleWebSocket.Delegates;
-using Jung.SimpleWebSocket.Exceptions;
-using Jung.SimpleWebSocket.Models;
-using Jung.SimpleWebSocket.Models.EventArguments;
-using Jung.SimpleWebSocket.Wrappers;
-using Microsoft.Extensions.Logging;
-using System.Collections.Concurrent;
-using System.Net;
-using System.Net.WebSockets;
-using System.Text;
-
-namespace Jung.SimpleWebSocket
-{
- ///
- /// A simple WebSocket server.
- ///
- public class SimpleWebSocketServer : IWebSocketServer, IDisposable
- {
- ///
- public IPAddress LocalIpAddress { get; }
- ///
- public int Port { get; }
-
- ///
- public event ClientConnectedEventHandler? ClientConnected;
- ///
- public event ClientDisconnectedEventHandler? ClientDisconnected;
- ///
- public event ClientMessageReceivedEventHandler? MessageReceived;
- ///
- public event ClientBinaryMessageReceivedEventHandler? BinaryMessageReceived;
-
- ///
- /// A dictionary of active clients.
- ///
- private ConcurrentDictionary ActiveClients { 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;
-
- ///
- /// A flag indicating whether the server is started.
- ///
- private bool _isStarted;
-
- ///
- /// A flag indicating whether the server is shutting down.
- ///
- private bool _serverShuttingDown;
-
- ///
- /// A cancellation token source to cancel the server.
- ///
- private CancellationTokenSource _cancellationTokenSource = new();
-
- ///
- /// The server that listens for incoming connection attempts.
- ///
- private ITcpListener? _server;
-
- ///
- /// A logger to write internal log messages.
- ///
- private readonly ILogger? _logger;
-
- ///
- /// 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
- /// A logger to write internal log messages
- public SimpleWebSocketServer(IPAddress localIpAddress, int port, ILogger? logger = null)
- {
- LocalIpAddress = localIpAddress;
- Port = port;
- _logger = logger;
- }
-
- ///
- /// Constructor for dependency injection (used in tests)
- ///
- /// A local ip address
- /// A port on which to listen for incoming connection attempts
- /// A wrapped tcp listener
- /// >A logger to write internal log messages
- internal SimpleWebSocketServer(IPAddress localIpAddress, int port, ITcpListener tcpListener, ILogger? logger = null)
- {
- LocalIpAddress = localIpAddress;
- Port = port;
- _server = tcpListener;
- _logger = logger;
- }
-
- ///
- public void Start(CancellationToken? cancellationToken = null)
- {
- if (_isStarted) throw new WebSocketServerException("Server is already started");
- _isStarted = true;
- cancellationToken ??= CancellationToken.None;
-
- _cancellationTokenSource = new CancellationTokenSource();
- var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value, _cancellationTokenSource.Token);
-
- _server ??= new TcpListenerWrapper(LocalIpAddress, Port);
- _server.Start();
- _ = Task.Run(async delegate
- {
- _logger?.LogInformation("Server started at {LocalIpAddress}:{Port}", LocalIpAddress, Port);
- while (!linkedTokenSource.IsCancellationRequested)
- {
- try
- {
- // 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;
- }
-
- _ = HandleClientAsync(client, linkedTokenSource.Token);
- }
- catch (OperationCanceledException)
- {
- // Ignore the exception, because it is thrown when cancellation is requested
- }
- catch (Exception exception)
- {
- _logger?.LogError(exception, "Error while accepting client.");
- }
- }
- }, linkedTokenSource.Token);
- }
-
- ///
- 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;
-
- 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)
- {
- await client.WebSocket.CloseAsync(WebSocketCloseStatus.EndpointUnavailable, "Server is shutting down", linkedTokenSource.Token);
- ActiveClients.TryRemove(client.Id, out _);
- client?.Dispose();
- }
- }
-
- _cancellationTokenSource?.Cancel();
- _server?.Dispose();
- _server = null;
- _logger?.LogInformation("Server stopped");
- }
-
- ///
- public async Task SendMessageAsync(string clientId, string message, CancellationToken? cancellationToken = null)
- {
- // 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");
-
- cancellationToken ??= CancellationToken.None;
- var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value, _cancellationTokenSource.Token);
-
- try
- {
- // 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);
- }
- catch (Exception exception)
- {
- _logger?.LogError(exception, "Error while sending a message.");
- throw new WebSocketServerException(message: "An Error occurred sending a message.", innerException: exception);
- }
- }
-
- ///
- ///
- public WebSocketServerClient GetClientById(string clientId)
- {
- if (!ActiveClients.TryGetValue(clientId, out var client)) throw new WebSocketServerException(message: "Client not found");
- return client;
- }
-
- ///
- /// Handles the client connection.
- ///
- /// The client to handle
- /// The cancellation token
- /// A asynchronous task
- private async Task HandleClientAsync(WebSocketServerClient client, CancellationToken cancellationToken)
- {
- try
- {
- // Upgrade the connection to a WebSocket
- 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);
-
- // Start listening for messages
- _logger?.LogDebug("Connection upgraded, now listening on client {clientId}", client.Id);
- await ProcessWebSocketMessagesAsync(client, cancellationToken);
- }
- catch (OperationCanceledException)
- {
- // Ignore the exception, because it is thrown when cancellation is requested
- }
- catch (Exception exception)
- {
- _logger?.LogError(exception, "Error while handling the client {clientId}", client.Id);
- }
- finally
- {
- if (!_serverShuttingDown)
- {
- ActiveClients.TryRemove(client.Id, out _);
- client?.Dispose();
- }
- }
- }
-
- ///
- /// Processes the WebSocket messages.
- ///
- /// The client whose messages to process
- /// The cancellation token
- /// A asynchronous task
- ///
- private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, CancellationToken cancellationToken)
- {
- if (client.WebSocket == null)
- {
- throw new InvalidOperationException("WebSocket is not initialized");
- }
-
- var webSocket = client.WebSocket;
-
- 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);
- _ = 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);
- _ = 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");
- _ = Task.Run(() => ClientDisconnected?.Invoke(this, new ClientDisconnectedArgs(result.CloseStatusDescription ?? string.Empty, client.Id)), cancellationToken);
- await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
- break;
- }
- }
- }
-
- ///
- public void Dispose()
- {
- _cancellationTokenSource?.Cancel();
- _server?.Dispose();
- _server = null;
- GC.SuppressFinalize(this);
- }
- }
-}
\ No newline at end of file
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..c4f7934
--- /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 8085 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);
+ 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..66e3490
--- /dev/null
+++ b/examples/BasicUserHandlingClientExample/Program.cs
@@ -0,0 +1,77 @@
+// 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();
+ }
+ }
+
+ ///
+ /// 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
+ 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..2850237
--- /dev/null
+++ b/examples/BasicUserHandlingServerExample/Program.cs
@@ -0,0 +1,150 @@
+// 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 8085 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();
+ }
+
+ ///
+ /// 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)
+ {
+ Console.WriteLine($"User name {client.Properties["UserName"]} connected successfully.");
+ }
+ }
+
+ ///
+ /// 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
+ var userName = e.Client.Properties["UserName"]?.ToString() ?? "Unknown";
+ _connectedUsers.TryRemove(userName, out _);
+ 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)
+ {
+ 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);
+ 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/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 78%
rename from Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs
rename to src/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs
index 1a73240..9c00f62 100644
--- a/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/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs b/src/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs
similarity index 65%
rename from Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs
rename to src/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs
index 4e9e79b..03d8b27 100644
--- a/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs
+++ b/src/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs
@@ -2,8 +2,10 @@
// 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.Diagnostics.CodeAnalysis;
using System.Net;
namespace Jung.SimpleWebSocket.Contracts;
@@ -41,22 +43,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;
+
+ ///
+ /// Async Event that is raised when a client upgrade request is received.
+ ///
+ event AsyncEventHandler? ClientUpgradeRequestReceivedAsync;
///
/// Gets a client by its id.
@@ -65,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.
///
@@ -87,4 +102,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/src/Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs b/src/Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs
new file mode 100644
index 0000000..ba551fe
--- /dev/null
+++ b/src/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/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/src/Jung.SimpleWebSocket/Exceptions/ClientIdAlreadyExistsException.cs b/src/Jung.SimpleWebSocket/Exceptions/ClientIdAlreadyExistsException.cs
new file mode 100644
index 0000000..91635d6
--- /dev/null
+++ b/src/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/src/Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs b/src/Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs
new file mode 100644
index 0000000..58d4a95
--- /dev/null
+++ b/src/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/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/src/Jung.SimpleWebSocket/Exceptions/UserNotHandledException.cs b/src/Jung.SimpleWebSocket/Exceptions/UserNotHandledException.cs
new file mode 100644
index 0000000..148c6f7
--- /dev/null
+++ b/src/Jung.SimpleWebSocket/Exceptions/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.Exceptions
+{
+ [Serializable]
+ internal class UserNotHandledException(WebContext responseContext) : Exception
+ {
+ public WebContext ResponseContext { get; set; } = responseContext;
+ }
+}
\ No newline at end of file
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/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/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/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs b/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs
new file mode 100644
index 0000000..fa1288d
--- /dev/null
+++ b/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs
@@ -0,0 +1,162 @@
+// 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.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Jung.SimpleWebSocket.Flows
+{
+ ///
+ /// 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 response context that is being use to response to the client.
+ ///
+ private WebContext? _responseContext = null;
+
+ ///
+ /// Gets the active clients of the server.
+ ///
+ private readonly ConcurrentDictionary _activeClients = server.ActiveClients;
+
+ ///
+ /// Gets the logger of the server.
+ ///
+ private readonly ILogger? _logger = server.Logger;
+
+ ///
+ /// Gets the cancellation token of the server.
+ ///
+ private readonly CancellationToken _cancellationToken = cancellationToken;
+
+ ///
+ /// Loads the request context.
+ ///
+ internal async Task LoadRequestContext()
+ {
+ var stream = Client.ClientConnection!.GetStream();
+ _upgradeHandler = new WebSocketUpgradeHandler(stream);
+ Request = await _upgradeHandler.AwaitContextAsync(_cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Accepts the web socket connection.
+ ///
+ internal async Task AcceptWebSocketAsync()
+ {
+ // Check if the response context are initialized
+ ThrowForResponseContextNotInitialized(_responseContext);
+
+ // The client is accepted
+ await _upgradeHandler!.AcceptWebSocketAsync(Request!, _responseContext, null, _cancellationToken).ConfigureAwait(false);
+
+ // Use the web socket for the client
+ Client.UseWebSocket(_upgradeHandler.CreateWebSocket(isServer: true));
+ Cleanup();
+ }
+
+ ///
+ /// Rejects the web socket connection.
+ ///
+ /// The response context to send to the client.
+ internal async Task RejectWebSocketAsync(WebContext responseContext)
+ {
+ // 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();
+ }
+
+ ///
+ /// Handles the disconnected client.
+ ///
+ internal void HandleDisconnectedClient()
+ {
+ _activeClients.TryRemove(Client.Id, out _);
+ Client.Dispose();
+
+ _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).ConfigureAwait(false);
+ _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);
+ }
+
+ ///
+ /// 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.");
+ }
+ }
+
+ ///
+ /// Releases resources that are no longer required.
+ ///
+ private void Cleanup()
+ {
+ _upgradeHandler = null;
+ _responseContext = null;
+ Request = null;
+ }
+ }
+}
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/src/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs
new file mode 100644
index 0000000..eda1d8e
--- /dev/null
+++ b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs
@@ -0,0 +1,15 @@
+// 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 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 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/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/src/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs
new file mode 100644
index 0000000..e7b3150
--- /dev/null
+++ b/src/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 Accepted. Default is true.
+ ///
+ public bool AcceptRequest { 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/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/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/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/Models/SimpleWebSocketServerOptions.cs b/src/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs
new file mode 100644
index 0000000..e832858
--- /dev/null
+++ b/src/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs
@@ -0,0 +1,28 @@
+// This file is part of the Jung SimpleWebSocket project.
+// The project is licensed under the MIT license.
+
+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 log level of the server.
+ ///
+ public string LogLevel { get; set; } = "Information";
+ }
+}
diff --git a/Jung.SimpleWebSocket/Models/WebContext.cs b/src/Jung.SimpleWebSocket/Models/WebContext.cs
similarity index 72%
rename from Jung.SimpleWebSocket/Models/WebContext.cs
rename to src/Jung.SimpleWebSocket/Models/WebContext.cs
index c2603f6..589ab61 100644
--- a/Jung.SimpleWebSocket/Models/WebContext.cs
+++ b/src/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)
{
///
@@ -44,7 +45,23 @@ internal class WebContext(string? content = null)
private string? _requestPath = null;
///
- /// Gets the headers of the web request.
+ /// The status code of the context.
+ ///
+ private HttpStatusCode? _statusCode;
+
+
+ // A Regular Expression to split a string by uppercase letters.
+ [GeneratedRegex(@"(?
+ /// The body content.
+ ///
+ private string? _bodyContent = null;
+
+ ///
+ /// Gets the headers.
///
public NameValueCollection Headers
{
@@ -55,6 +72,30 @@ public NameValueCollection Headers
}
}
+ ///
+ /// Gets or Sets the body content.
+ ///
+ 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.
///
@@ -140,6 +181,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.
///
@@ -173,24 +241,26 @@ private NameValueCollection ParseHeaders()
/// The created web request context.
internal static WebContext CreateRequest(string hostName, int port, string requestPath)
{
- return new WebContext()
+ var context = new WebContext()
{
HostName = hostName,
Port = port,
- RequestPath = requestPath
+ RequestPath = requestPath,
};
+
+ return context;
}
///
/// 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));
}
///
@@ -255,7 +325,6 @@ internal IEnumerable GetAllHeaderValues(string headerName)
}
}
-
///
/// Gets the concatenated headers of the web request.
///
@@ -300,6 +369,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/src/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs
similarity index 75%
rename from Jung.SimpleWebSocket/Models/WebSocketServerClient.cs
rename to src/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs
index d737b17..f48c7c3 100644
--- a/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs
+++ b/src/Jung.SimpleWebSocket/Models/WebSocketServerClient.cs
@@ -16,10 +16,18 @@ 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.
///
- internal ITcpClient ClientConnection { get; private set; }
+ internal ITcpClient? ClientConnection { get; private set; }
///
/// Gets the timestamp when the WebSocket client was last seen.
@@ -34,7 +42,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.
@@ -46,10 +54,18 @@ public class WebSocketServerClient : IDisposable
///
/// The connection of the client.
internal WebSocketServerClient(ITcpClient clientConnection)
+ : base()
+ {
+ ClientConnection = clientConnection;
+ }
+
+ ///
+ /// Initializes a new instance of the class without a client connection.
+ ///
+ internal WebSocketServerClient()
{
FirstSeen = DateTime.UtcNow;
LastConnectionTimestamp = FirstSeen;
- ClientConnection = clientConnection;
}
///
@@ -62,6 +78,16 @@ internal void UpdateClient(ITcpClient client)
ClientConnection = client;
}
+ ///
+ /// Updates the client with a new WebSocket.
+ ///
+ /// The web socket 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 +108,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/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs
similarity index 59%
rename from Jung.SimpleWebSocket/SimpleWebSocketClient.cs
rename to src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs
index b690ab7..f77671e 100644
--- a/Jung.SimpleWebSocket/SimpleWebSocketClient.cs
+++ b/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs
@@ -6,8 +6,10 @@
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;
using System.Net.WebSockets;
using System.Text;
@@ -23,7 +25,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;
@@ -42,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.
///
@@ -64,8 +69,21 @@ public class SimpleWebSocketClient(string hostName, int port, string requestPath
///
/// A value indicating whether the client is disconnecting.
+ /// 0=Not disconnecting, 1=Disconnecting
+ ///
+ private int _clientIsDisconnecting = 0;
+
+
+ ///
+ /// A value indicating whether the client is disposed.
+ /// 0=Not Disposed, 1=Disposed
///
- private bool _clientIsDisconnecting;
+ 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 +93,8 @@ public class SimpleWebSocketClient(string hostName, int port, string requestPath
///
public async Task ConnectAsync(CancellationToken? cancellationToken = null)
{
+ ThrowIfDisposed();
+
if (IsConnected) throw new WebSocketClientException(message: "Client is already connected");
cancellationToken ??= CancellationToken.None;
@@ -85,8 +105,8 @@ public async Task ConnectAsync(CancellationToken? cancellationToken = null)
try
{
_client = new TcpClientWrapper();
- await _client.ConnectAsync(HostName, Port);
- await HandleWebSocketInitiation(_client, linkedTokenSource.Token);
+ await _client.ConnectAsync(HostName, Port).ConfigureAwait(false);
+ await HandleWebSocketInitiation(_client, linkedTokenSource.Token).ConfigureAwait(false);
_logger?.LogDebug("Connection upgraded, now listening.");
_ = ProcessWebSocketMessagesAsync(_webSocket!, linkedTokenSource.Token);
@@ -94,7 +114,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 || exception is WebSocketUpgradeException)
{
throw;
}
@@ -108,19 +132,22 @@ public async Task ConnectAsync(CancellationToken? cancellationToken = null)
///
public async Task DisconnectAsync(string closingStatusDescription = "Closing", CancellationToken? cancellationToken = null)
{
- if (_clientIsDisconnecting) throw new WebSocketClientException("Client is already disconnecting");
- _clientIsDisconnecting = true;
+ // Make sure we only disconnect once
+ if (Interlocked.Exchange(ref _clientIsDisconnecting, 1) == 1)
+ {
+ return;
+ }
cancellationToken ??= CancellationToken.None;
var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value, _cancellationTokenSource.Token);
- _logger?.LogInformation("Disconnecting from Server");
if (_webSocket != null && (_webSocket.State == WebSocketState.Open || _webSocket.State == WebSocketState.CloseReceived))
{
try
{
- await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, closingStatusDescription, linkedTokenSource.Token);
+ _logger?.LogInformation("Disconnecting from Server");
+ await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, closingStatusDescription, linkedTokenSource.Token).ConfigureAwait(false);
}
catch (Exception exception)
{
@@ -135,7 +162,6 @@ public async Task DisconnectAsync(string closingStatusDescription = "Closing", C
}
}
}
- _client?.Dispose();
}
///
@@ -151,15 +177,32 @@ 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);
+ 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);
+
_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)
{
+ ThrowIfDisposed();
+
if (!IsConnected) throw new WebSocketClientException(message: "Client is not connected");
if (_webSocket == null) throw new WebSocketClientException(message: "WebSocket is not initialized");
@@ -170,11 +213,12 @@ public async Task SendMessageAsync(string message, CancellationToken? cancellati
{
// Send the message
var buffer = Encoding.UTF8.GetBytes(message);
- await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, linkedTokenSource.Token);
+ await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, linkedTokenSource.Token).ConfigureAwait(false);
_logger?.LogDebug("Message sent: {message}", message);
}
catch (Exception exception)
{
+ _logger?.LogError(exception, "Error sending message");
throw new WebSocketClientException(message: "Error sending message", innerException: exception);
}
}
@@ -193,45 +237,69 @@ private async Task ProcessWebSocketMessagesAsync(IWebSocket webSocket, Cancellat
throw new InvalidOperationException("WebSocket is not initialized");
}
- var buffer = new byte[1024 * 4]; // Buffer for incoming data
- while (webSocket.State == WebSocketState.Open)
+ try
{
+ var buffer = new byte[1024 * 4]; // Buffer for incoming data
+ while (webSocket.State == WebSocketState.Open)
+ {
- // Read the next message
- WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken);
+ // Read the next message
+ WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken).ConfigureAwait(false);
- if (result.MessageType == WebSocketMessageType.Text)
- {
- // Handle the text message
- string receivedMessage = Encoding.UTF8.GetString(buffer, 0, result.Count);
- _logger?.LogDebug("Message received: {message}", receivedMessage);
- _ = Task.Run(() => MessageReceived?.Invoke(this, new MessageReceivedArgs(receivedMessage)), cancellationToken);
- }
- else if (result.MessageType == WebSocketMessageType.Binary)
- {
- // Handle the binary message
- _logger?.LogDebug("Binary message received, length: {length} bytes", result.Count);
- _ = Task.Run(() => BinaryMessageReceived?.Invoke(this, new BinaryMessageReceivedArgs(buffer[..result.Count])), cancellationToken);
- }
- // We have to check if the client is disconnecting here,
- // because then we already sent the close message and we don't want to send another one
- else if (result.MessageType == WebSocketMessageType.Close && !_clientIsDisconnecting)
- {
- _logger?.LogInformation("Received close message from server");
- _ = Task.Run(() => Disconnected?.Invoke(this, new DisconnectedArgs(result.CloseStatusDescription ?? string.Empty)), cancellationToken);
- await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
- break;
+ if (result.MessageType == WebSocketMessageType.Text)
+ {
+ // Handle the text message
+ string receivedMessage = Encoding.UTF8.GetString(buffer, 0, result.Count);
+ _logger?.LogDebug("Message received: {message}", receivedMessage);
+ _ = Task.Run(() => MessageReceived?.Invoke(this, new MessageReceivedArgs(receivedMessage)), cancellationToken);
+ }
+ else if (result.MessageType == WebSocketMessageType.Binary)
+ {
+ // Handle the binary message
+ _logger?.LogDebug("Binary message received, length: {length} bytes", result.Count);
+ _ = Task.Run(() => BinaryMessageReceived?.Invoke(this, new BinaryMessageReceivedArgs(buffer[..result.Count])), cancellationToken);
+ }
+ // We have to check if the client is disconnecting here,
+ // because then we already sent the close message and we don't want to send another one
+ else if (result.MessageType == WebSocketMessageType.Close && _clientIsDisconnecting == 0)
+ {
+ _logger?.LogInformation("Received close message from server");
+ _ = Task.Run(() => Disconnected?.Invoke(this, new DisconnectedArgs(result.CloseStatusDescription ?? string.Empty)), cancellationToken);
+ await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None).ConfigureAwait(false);
+ break;
+ }
}
}
+ catch (Exception exception)
+ {
+ _logger?.LogError(exception, "Error processing WebSocket messages. Connection Closed.");
+ _ = Task.Run(() => Disconnected?.Invoke(this, new DisconnectedArgs(exception.Message)), cancellationToken);
+ }
}
///
public void Dispose()
{
- _cancellationTokenSource?.Cancel();
- _stream?.Dispose();
- _client?.Dispose();
- GC.SuppressFinalize(this);
+ if (Interlocked.Exchange(ref _disposed, 1) == 0)
+ {
+ // Unsubscribe all event handlers
+ Disconnected = null;
+ MessageReceived = null;
+ BinaryMessageReceived = null;
+ SendingUpgradeRequestAsync = null;
+
+ // Dispose managed resources
+ _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
new file mode 100644
index 0000000..6d02221
--- /dev/null
+++ b/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs
@@ -0,0 +1,466 @@
+// This file is part of the Jung SimpleWebSocket project.
+// The project is licensed under the MIT license.
+
+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;
+using Jung.SimpleWebSocket.Wrappers;
+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;
+
+namespace Jung.SimpleWebSocket
+{
+ ///
+ /// A simple WebSocket server.
+ ///
+ ///
+ /// 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; } = options.LocalIpAddress;
+ ///
+ public int Port { get; } = options.Port;
+
+ ///
+ public event EventHandler? ClientConnected;
+ ///
+ public event EventHandler? ClientDisconnected;
+ ///
+ public event EventHandler? MessageReceived;
+ ///
+ public event EventHandler? BinaryMessageReceived;
+
+ ///
+ public event AsyncEventHandler? ClientUpgradeRequestReceivedAsync;
+
+ ///
+ /// A dictionary of active clients.
+ ///
+ internal ConcurrentDictionary ActiveClients { get; } = [];
+
+ ///
+ public string[] ClientIds => [.. ActiveClients.Keys];
+
+ ///
+ public int ClientCount => ActiveClients.Count;
+
+ ///
+ public bool IsListening => _tcpListener?.IsListening ?? false;
+
+ ///
+ /// A logger to write internal log messages.
+ ///
+ internal ILogger? Logger { get; } = logger;
+
+ ///
+ /// The options for the server.
+ ///
+ internal SimpleWebSocketServerOptions Options { get; } = options;
+
+ ///
+ /// A flag indicating whether the server is started.
+ ///
+ public bool IsStarted => _isStarted == 1;
+
+ ///
+ /// A flag indicating whether the server is shutting down.
+ ///
+ private bool IsShuttingDown => _serverShuttingDown == 1;
+
+ ///
+ /// A flag indicating whether the server is disposed.
+ ///
+ private bool Disposed => _disposed == 1;
+
+ ///
+ /// A flag indicating whether the server is started.
+ /// 0 = false, 1 = true
+ ///
+ private int _isStarted;
+
+ ///
+ /// A flag indicating whether the server is shutting down.
+ /// 0 = false, 1 = true
+ ///
+ private int _serverShuttingDown;
+
+ ///
+ /// A flag indicating whether the server is disposed.
+ /// 0 = false, 1 = true
+ ///
+ private int _disposed;
+
+ ///
+ /// A flag indicating whether the server is disposing.
+ /// 0 = false, 1 = true
+ ///
+ private int _disposing;
+
+ ///
+ /// A cancellation token source to cancel the server.
+ ///
+ private CancellationTokenSource _cancellationTokenSource = new();
+
+ ///
+ /// The server that listens for incoming connection attempts.
+ ///
+ 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(IOptions options, ILogger? logger = null)
+ : this(options.Value, logger)
+ {
+ }
+
+ ///
+ /// Constructor for dependency injection (used in tests)
+ ///
+ /// 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)
+ : this(options, logger)
+ {
+ _tcpListener = tcpListener;
+ }
+
+ ///
+ public void Start(CancellationToken? cancellationToken = null)
+ {
+ ThrowIfDisposed();
+
+ if (Interlocked.Exchange(ref _isStarted, 1) == 1)
+ {
+ throw new WebSocketServerException("Server is already started");
+ }
+
+ cancellationToken ??= CancellationToken.None;
+
+ _cancellationTokenSource = new CancellationTokenSource();
+ var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value, _cancellationTokenSource.Token);
+
+ _tcpListener ??= new TcpListenerWrapper(LocalIpAddress, Port);
+ _tcpListener.Start();
+ _ = Task.Run(async delegate
+ {
+ Logger?.LogInformation("Server started at {LocalIpAddress}:{Port}", LocalIpAddress, Port);
+ while (!linkedTokenSource.IsCancellationRequested)
+ {
+ try
+ {
+ // Accept the client
+ var client = await _tcpListener.AcceptTcpClientAsync(linkedTokenSource.Token).ConfigureAwait(false);
+
+ Logger?.LogDebug("Client connected from {endpoint}", client.ClientConnection!.RemoteEndPoint);
+
+ _ = HandleClientAsync(client, linkedTokenSource.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ // Ignore the exception, because it is thrown when cancellation is requested
+ }
+ catch (Exception exception)
+ {
+ Logger?.LogError(exception, "Error while accepting Client.");
+ }
+ }
+ }, linkedTokenSource.Token);
+ }
+
+ ///
+ public async Task ShutdownServer(CancellationToken? cancellationToken = null)
+ {
+ 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)
+ {
+ try
+ {
+ if (client.WebSocket != null && client.WebSocket.State == WebSocketState.Open)
+ {
+ await client.WebSocket.CloseAsync(WebSocketCloseStatus.EndpointUnavailable, "Server is shutting down", linkedTokenSource.Token).ConfigureAwait(false);
+ ActiveClients.TryRemove(client.Id, out _);
+ client?.Dispose();
+ }
+ }
+ catch
+ {
+ // Ignore the exception, because it's not the server's problem if a client does not close the connection
+ }
+ }
+
+
+ _cancellationTokenSource?.Cancel();
+ _tcpListener?.Dispose();
+ _tcpListener = null;
+ ActiveClients.Clear();
+ _serverShuttingDown = 0;
+ Logger?.LogInformation("Server stopped");
+ }
+
+ ///
+ public async Task SendMessageAsync(string clientId, string message, CancellationToken? cancellationToken = null)
+ {
+ ThrowIfDisposed();
+
+ // Find and check the client
+ if (!ActiveClients.TryGetValue(clientId, out var client)) throw new WebSocketServerException(message: "Client not found");
+ if (client.WebSocket == null) throw new WebSocketServerException(message: "Client is not connected");
+
+ cancellationToken ??= CancellationToken.None;
+ var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value, _cancellationTokenSource.Token);
+
+ try
+ {
+ // Send the message
+ var buffer = Encoding.UTF8.GetBytes(message);
+ await client.WebSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, linkedTokenSource.Token).ConfigureAwait(false);
+ Logger?.LogDebug("Message sent: {message}.", message);
+ }
+ catch (Exception exception)
+ {
+ Logger?.LogError(exception, "Error while sending a message.");
+ throw new WebSocketServerException(message: "An Error occurred sending a message.", innerException: exception);
+ }
+ }
+
+ ///
+ ///
+ public WebSocketServerClient GetClientById(string clientId)
+ {
+ ThrowIfDisposed();
+
+ 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;
+ }
+
+ ///
+ 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");
+
+ // 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.
+ ///
+ /// The client to handle
+ /// The cancellation token
+ /// A asynchronous task
+ private async Task HandleClientAsync(WebSocketServerClient client, CancellationToken cancellationToken)
+ {
+ var flow = new ClientHandlingFlow(this, client, cancellationToken);
+ try
+ {
+ // Load the request context
+ await flow.LoadRequestContext().ConfigureAwait(false);
+
+ // Raise async client upgrade request received event
+ var eventArgs = await flow.RaiseUpgradeEventAsync(ClientUpgradeRequestReceivedAsync).ConfigureAwait(false);
+
+ // Respond to the upgrade request
+ if (eventArgs.AcceptRequest)
+ {
+ // Accept the WebSocket connection
+ 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).ConfigureAwait(false);
+ }
+ else
+ {
+ Logger?.LogDebug("Error while adding Client {clientId} to active clients", flow.Client.Id);
+ }
+ }
+ else
+ {
+ // Reject the WebSocket connection
+ Logger?.LogDebug("Client upgrade request rejected by ClientUpgradeRequestReceivedAsync event.");
+ await flow.RejectWebSocketAsync(eventArgs.ResponseContext).ConfigureAwait(false);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Ignore the exception, because it is thrown when cancellation is requested
+ }
+ catch (Exception exception)
+ {
+ 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 (!IsShuttingDown)
+ {
+ flow.HandleDisconnectedClient();
+ }
+ }
+ }
+
+ ///
+ /// Processes the WebSocket messages.
+ ///
+ /// The client whose messages to process
+ /// The cancellation token
+ /// A asynchronous task
+ ///
+ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, CancellationToken cancellationToken)
+ {
+ if (client.WebSocket == null)
+ {
+ throw new InvalidOperationException("WebSocket is not initialized");
+ }
+
+ var webSocket = client.WebSocket;
+ string? closeStatusDescription = null;
+ var buffer = new byte[1024 * 4]; // Buffer for incoming data
+
+ try
+ {
+ while (webSocket.State == WebSocketState.Open)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ // Read the next message
+ WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken).ConfigureAwait(false);
+
+ if (result.MessageType == WebSocketMessageType.Text)
+ {
+ // Handle the text message
+ string receivedMessage = Encoding.UTF8.GetString(buffer, 0, result.Count);
+ Logger?.LogDebug("Message received: {message}", receivedMessage);
+ AsyncEventRaiser.RaiseAsyncInNewTask(MessageReceived, this, new ClientMessageReceivedArgs(receivedMessage, client.Id), cancellationToken);
+ }
+ else if (result.MessageType == WebSocketMessageType.Binary)
+ {
+ // Handle the binary message
+ Logger?.LogDebug("Binary message received, length: {length} bytes", result.Count);
+ AsyncEventRaiser.RaiseAsyncInNewTask(BinaryMessageReceived, this, new ClientBinaryMessageReceivedArgs(buffer[..result.Count], client.Id), cancellationToken);
+ }
+ // We have to check if the is shutting down here,
+ // because then we already sent the close message and we don't want to send another one
+ else if (result.MessageType == WebSocketMessageType.Close && !IsShuttingDown)
+ {
+ Logger?.LogInformation("Received close message from Client");
+ closeStatusDescription = result.CloseStatusDescription;
+ await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None).ConfigureAwait(false);
+ break;
+ }
+ }
+ }
+ finally
+ {
+ // if we leave the loop, the client disconnected
+ if (!IsShuttingDown)
+ {
+ AsyncEventRaiser.RaiseAsyncInNewTask(ClientDisconnected, this, new ClientDisconnectedArgs(closeStatusDescription, client), cancellationToken);
+ }
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (Interlocked.Exchange(ref _disposing, 1) == 1)
+ {
+ return;
+ }
+
+ 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
+ {
+ Interlocked.Exchange(ref _disposed, 1);
+ }
+ }
+
+ private void ThrowIfDisposed()
+ {
+ ObjectDisposedException.ThrowIf(Disposed, this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs b/src/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs
new file mode 100644
index 0000000..c2a0c91
--- /dev/null
+++ b/src/Jung.SimpleWebSocket/Utility/AsyncEventRaiser.cs
@@ -0,0 +1,76 @@
+// 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).ConfigureAwait(false);
+ }, null);
+ }
+ else
+ {
+ // Execute directly if there's no synchronization context
+ await asyncHandler(sender, e, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Helper method to raise an Event in a new Task.
+ ///
+ /// 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();
+ Task.Run(() =>
+ {
+ foreach (var handler in invocationList)
+ {
+ var handle = (EventHandler)handler;
+ handle(sender, e);
+ }
+ }, cancellationToken);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Jung.SimpleWebSocket/Wrappers/NetworkStreamWrapper.cs b/src/Jung.SimpleWebSocket/Wrappers/NetworkStreamWrapper.cs
similarity index 96%
rename from Jung.SimpleWebSocket/Wrappers/NetworkStreamWrapper.cs
rename to src/Jung.SimpleWebSocket/Wrappers/NetworkStreamWrapper.cs
index 917a18a..13e5ab1 100644
--- a/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/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 95%
rename from Jung.SimpleWebSocket/Wrappers/TcpListenerWrapper.cs
rename to src/Jung.SimpleWebSocket/Wrappers/TcpListenerWrapper.cs
index 331d328..a16eb0f 100644
--- a/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/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs b/src/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs
similarity index 76%
rename from Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs
rename to src/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs
index 6da2ffc..e7ea051 100644
--- a/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs
+++ b/src/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;
@@ -24,7 +25,7 @@ internal partial class WebSocketUpgradeHandler
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)]
@@ -34,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)
@@ -52,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]));
}
@@ -61,16 +62,10 @@ public async Task AwaitContextAsync(CancellationToken cancellationTo
return context;
}
- public async Task AcceptWebSocketAsync(WebContext request, CancellationToken cancellationToken)
- {
- await AcceptWebSocketAsync(request, null, cancellationToken);
- }
-
- public async Task AcceptWebSocketAsync(WebContext request, string? subProtocol, CancellationToken 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,7 +77,8 @@ 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);
- await SendWebSocketResponseHeaders(response, cancellationToken);
+ response.StatusCode = HttpStatusCode.SwitchingProtocols;
+ await SendWebSocketResponseHeaders(response, cancellationToken).ConfigureAwait(false);
_acceptedProtocol = subProtocol;
}
catch (WebSocketUpgradeException)
@@ -91,19 +87,31 @@ public async Task AcceptWebSocketAsync(WebContext request, string? subProtocol,
}
catch (Exception message)
{
- throw new WebSocketException("Error while accepting the websocket", message);
+ throw new WebSocketException("Error while accepting the web socket", message);
}
}
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);
+ CompleteHeaderSection(sb);
+
+ byte[] responseBytes = Encoding.UTF8.GetBytes(sb.ToString());
+ await _networkStream.WriteAsync(responseBytes, cancellationToken).ConfigureAwait(false);
+ }
+
+ private async Task SendWebSocketRejectResponse(WebContext context, CancellationToken cancellationToken)
+ {
+ var sb = new StringBuilder(
+ $"HTTP/1.1 {(int)context.StatusCode} {context.StatusDescription}\r\n");
AddHeaders(context, sb);
- FinishMessage(sb);
+ CompleteHeaderSection(sb);
+ 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)
@@ -112,10 +120,10 @@ 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);
+ await _networkStream.WriteAsync(responseBytes, cancellationToken).ConfigureAwait(false);
}
private static void AddHeaders(WebContext response, StringBuilder sb)
@@ -126,11 +134,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 static void AddBody(WebContext context, StringBuilder sb)
+ {
+ sb.Append(context.BodyContent);
+ }
+
private static void ValidateWebSocketHeaders(WebContext context)
{
if (!context.IsWebSocketRequest)
@@ -186,7 +199,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)
@@ -200,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)
@@ -219,15 +232,24 @@ 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"))
{
+ 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.");
}
@@ -272,6 +294,27 @@ 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)
+ {
+ // This header is optional, but recommended to inform the client that the connection will be closed
+ response.Headers.Add("Connection", "close");
+
+ // 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/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 87%
rename from Jung.SimpleWebSocket/docs/README.md
rename to src/Jung.SimpleWebSocket/docs/README.md
index 7d712e8..a0c5979 100644
--- a/Jung.SimpleWebSocket/docs/README.md
+++ b/src/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/tests/Jung.SimpleWebSocket.IntegrationTests/Jung.SimpleWebSocket.IntegrationTests.csproj b/tests/Jung.SimpleWebSocket.IntegrationTests/Jung.SimpleWebSocket.IntegrationTests.csproj
new file mode 100644
index 0000000..f1ca8ca
--- /dev/null
+++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Jung.SimpleWebSocket.IntegrationTests.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/ProcedureProvider.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/ProcedureProvider.cs
new file mode 100644
index 0000000..ee586bd
--- /dev/null
+++ b/tests/Jung.SimpleWebSocket.IntegrationTests/ProcedureProvider.cs
@@ -0,0 +1,84 @@
+// This file is part of the Jung SimpleWebSocket project.
+// The project is licensed under the MIT license.
+
+using Jung.SimpleWebSocket.IntegrationTests.Tests;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+
+namespace Jung.SimpleWebSocket.IntegrationTests
+{
+ internal class ProcedureProvider
+ {
+ private IOrderedEnumerable _procedures;
+
+ public ProcedureProvider()
+ {
+ _procedures = LoadProcedures();
+ }
+
+ private IOrderedEnumerable LoadProcedures()
+ {
+ var result = new List();
+
+ var types = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsSubclassOf(typeof(BaseTest)));
+ foreach (var type in types)
+ {
+ var testAttribute = (TestInformationAttribute?)Attribute.GetCustomAttribute(type, typeof(TestInformationAttribute));
+ if (testAttribute == null)
+ {
+ Console.WriteLine($"The test class {type.Name} has no TestInformationAttribute.");
+ continue;
+ }
+
+ result.Add(new TestProcedure(testAttribute.Role, testAttribute.Description, type));
+ }
+ return result.OrderBy(x => x.Role);
+ }
+
+ ///
+ /// Get the names of the procedures.
+ ///
+ /// The names of the procedures.
+ public string[] GetNames()
+ {
+ return [.. _procedures.Select(x => $"{x.Role} - {x.Name}: {x.Description}")];
+ }
+
+ ///
+ /// Get a procedure by its index
+ ///
+ /// The index of the procedure
+ ///
+ public TestProcedure GetProcedure(int index)
+ {
+ if (!HasIndex(index))
+ {
+ throw new IndexOutOfRangeException("There is no procedure at the given index.");
+ }
+
+ return _procedures.ElementAt(index);
+ }
+
+ ///
+ /// Try to get a procedure by name.
+ ///
+ /// The name of the procedure.
+ /// The procedure.
+ /// True if the procedure was found, false otherwise.
+ public bool TryGetProcedure(int index, [NotNullWhen(true)] out TestProcedure? procedure)
+ {
+ procedure = null;
+ if (HasIndex(index))
+ {
+ procedure = GetProcedure(index);
+ return true;
+ }
+ return false;
+ }
+
+ internal bool HasIndex(int index)
+ {
+ return _procedures.Count() > index && index >= 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Program.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/Program.cs
new file mode 100644
index 0000000..ec0b4a8
--- /dev/null
+++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Program.cs
@@ -0,0 +1,105 @@
+// This file is part of the Jung SimpleWebSocket project.
+// The project is licensed under the MIT license.
+
+using Jung.SimpleWebSocket.IntegrationTests.Tests;
+using Jung.SimpleWebSocket.Models;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Serilog;
+
+namespace Jung.SimpleWebSocket.IntegrationTests
+{
+ public class Program
+ {
+ ///
+ /// Main entry point for the application.
+ ///
+ /// The command line arguments.
+ public static async Task Main(string[] args)
+ {
+ var procedureProvider = new ProcedureProvider();
+
+ Console.WriteLine("Available tests:\n");
+
+ string[] procedureNames = procedureProvider.GetNames();
+ for (int i = 0; i < procedureNames.Length; i++)
+ {
+ Console.WriteLine($"{i + 1}: {procedureNames[i]}");
+ }
+
+ int chosenProcedureIndex;
+ do
+ {
+ Console.Write("\nEnter the number of the test you want to run: ");
+ var userInput = Console.ReadLine();
+ if (userInput != null)
+ {
+ userInput = userInput.Trim().ToLower();
+ if (userInput == "exit")
+ {
+ return;
+ }
+
+ if (!int.TryParse(userInput, out int procedureNumber))
+ {
+ Console.WriteLine("Invalid input. Please enter a number.");
+ continue;
+ }
+
+ if (procedureNumber >= 1 && procedureNumber <= procedureNames.Length)
+ {
+ chosenProcedureIndex = procedureNumber - 1;
+ break;
+ }
+ }
+ } while (true);
+
+ var procedure = procedureProvider.GetProcedure(chosenProcedureIndex);
+ var serviceProvider = CreateServiceProvider(procedure);
+ var logger = serviceProvider.GetRequiredService>();
+
+ try
+ {
+ if (serviceProvider.GetService(procedure.ProcedureType) is not BaseTest test)
+ {
+ logger.LogError("The chosen test procedure {procedureType} could not be loaded.", procedure.ProcedureType.FullName);
+ }
+ else
+ {
+ logger.LogInformation("Running test: {procedureName} ({procedureDescription})", procedure.Name, procedure.Description);
+ await test.RunAsync();
+ }
+ }
+ catch (Exception exception)
+ {
+ logger.LogError(exception, "An error occurred while running the procedure.");
+ }
+ }
+
+ private static ServiceProvider CreateServiceProvider(TestProcedure procedure)
+ {
+ var serviceCollection = new ServiceCollection();
+
+ Log.Logger = new LoggerConfiguration()
+ .WriteTo.File($"{procedure.Name}-{DateTime.Now:g}-{Guid.NewGuid():n}.txt", rollingInterval: RollingInterval.Day)
+ .WriteTo.Console(Serilog.Events.LogEventLevel.Information, outputTemplate: "{Level:u3}: {Message:lj}{NewLine}{Exception}")
+ .MinimumLevel.Debug()
+ .CreateLogger();
+
+ serviceCollection.AddSerilog();
+ serviceCollection.AddLogging();
+
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+
+ serviceCollection.Configure(options =>
+ {
+ options.LocalIpAddress = System.Net.IPAddress.Any;
+ options.Port = 8085;
+ });
+ return serviceCollection.BuildServiceProvider();
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/TestProcedure.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/TestProcedure.cs
new file mode 100644
index 0000000..312a4d8
--- /dev/null
+++ b/tests/Jung.SimpleWebSocket.IntegrationTests/TestProcedure.cs
@@ -0,0 +1,33 @@
+// This file is part of the Jung SimpleWebSocket project.
+// The project is licensed under the MIT license.
+
+
+namespace Jung.SimpleWebSocket.IntegrationTests
+{
+ internal class TestProcedure
+ {
+ public string Role;
+ public string Description;
+ public Type ProcedureType;
+
+ public TestProcedure(string role, string description, Type type)
+ {
+ Role = role;
+ Description = description;
+ ProcedureType = type;
+ }
+
+ public string Name
+ {
+ get
+ {
+ var result = ProcedureType.Name;
+ if (ProcedureType.Name.EndsWith("Test"))
+ {
+ result = result[0..^4];
+ }
+ return result;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/BaseTest.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/BaseTest.cs
new file mode 100644
index 0000000..bbc3e60
--- /dev/null
+++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/BaseTest.cs
@@ -0,0 +1,14 @@
+// This file is part of the Jung SimpleWebSocket project.
+// The project is licensed under the MIT license.
+
+using Microsoft.Extensions.Logging;
+
+namespace Jung.SimpleWebSocket.IntegrationTests.Tests
+{
+ internal abstract class BaseTest(ILogger logger)
+ {
+ protected readonly ILogger _logger = logger;
+
+ internal abstract Task RunAsync();
+ }
+}
diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/DisplayEventsServerTest.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/DisplayEventsServerTest.cs
new file mode 100644
index 0000000..2ad1875
--- /dev/null
+++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/DisplayEventsServerTest.cs
@@ -0,0 +1,78 @@
+// This file is part of the Jung SimpleWebSocket project.
+// The project is licensed under the MIT license.
+
+using Jung.SimpleWebSocket.Models.EventArguments;
+using Microsoft.Extensions.Logging;
+
+namespace Jung.SimpleWebSocket.IntegrationTests.Tests
+{
+ [TestInformation(Role = "Server", Description = "Display the events of the server.")]
+ internal class DisplayEventsTest(ILogger logger, SimpleWebSocketServer simpleWebSocketServer) : BaseTest(logger)
+ {
+ ///
+ /// The SimpleWebSocketServer instance.
+ ///
+ public SimpleWebSocketServer SimpleWebSocketServer { get; } = simpleWebSocketServer;
+
+ ///
+ /// Runs the server instance.
+ ///
+ internal override async Task RunAsync()
+ {
+ InitializeEventHandlers();
+
+ SimpleWebSocketServer.Start();
+
+ Console.WriteLine("Press any key to stop the SimpleWebSocketServer...");
+ Console.ReadKey();
+
+ UnsubscribeEventHandlers();
+
+ await SimpleWebSocketServer.ShutdownServer();
+ }
+
+ private void InitializeEventHandlers()
+ {
+ SimpleWebSocketServer.ClientConnected += SimpleWebSocketServer_ClientConnected;
+ SimpleWebSocketServer.ClientDisconnected += SimpleWebSocketServer_ClientDisconnected;
+ SimpleWebSocketServer.MessageReceived += SimpleWebSocketServer_MessageReceived;
+ SimpleWebSocketServer.BinaryMessageReceived += SimpleWebSocketServer_BinaryMessageReceived;
+ SimpleWebSocketServer.ClientUpgradeRequestReceivedAsync += ClientUpgradeRequestReceived;
+ }
+
+ private void UnsubscribeEventHandlers()
+ {
+ SimpleWebSocketServer.ClientConnected -= SimpleWebSocketServer_ClientConnected;
+ SimpleWebSocketServer.ClientDisconnected -= SimpleWebSocketServer_ClientDisconnected;
+ SimpleWebSocketServer.MessageReceived -= SimpleWebSocketServer_MessageReceived;
+ SimpleWebSocketServer.BinaryMessageReceived -= SimpleWebSocketServer_BinaryMessageReceived;
+ SimpleWebSocketServer.ClientUpgradeRequestReceivedAsync -= ClientUpgradeRequestReceived;
+ }
+
+ private void SimpleWebSocketServer_ClientConnected(object? sender, ClientConnectedArgs e)
+ {
+ _logger.LogInformation("Client connected: {ClientId}", e.ClientId);
+ }
+
+ private void SimpleWebSocketServer_ClientDisconnected(object? sender, ClientDisconnectedArgs e)
+ {
+ _logger.LogInformation("Client disconnected: {ClientId}", e.Client.Id);
+ }
+
+ private void SimpleWebSocketServer_MessageReceived(object? sender, ClientMessageReceivedArgs e)
+ {
+ _logger.LogInformation("Message received from {ClientId}: {Message}", e.ClientId, e.Message);
+ }
+
+ private void SimpleWebSocketServer_BinaryMessageReceived(object? sender, ClientBinaryMessageReceivedArgs e)
+ {
+ _logger.LogInformation("Binary message received from {ClientId}: {messages}", e.ClientId, string.Join(' ', e.Message));
+ }
+
+ private Task ClientUpgradeRequestReceived(object sender, ClientUpgradeRequestReceivedArgs e, CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Upgrade request received from {ClientId}.", e.Client.Id);
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs
new file mode 100644
index 0000000..1638852
--- /dev/null
+++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs
@@ -0,0 +1,114 @@
+// This file is part of the Jung SimpleWebSocket project.
+// The project is licensed under the MIT license.
+
+using Jung.SimpleWebSocket.Exceptions;
+using Jung.SimpleWebSocket.Models.EventArguments;
+using Microsoft.Extensions.Logging;
+
+namespace Jung.SimpleWebSocket.IntegrationTests.Tests
+{
+ [TestInformation(Role = "Client", Description = "Stability test - Sends messages at random times (between 5s and 20s)")]
+ internal class SendMessagesLoopTest(ILogger logger, ILogger clientLogger) : BaseTest(logger)
+ {
+ internal override async Task RunAsync()
+ {
+ var cancellationTokenSource = new CancellationTokenSource();
+ var token = cancellationTokenSource.Token;
+
+ using var client = new SimpleWebSocketClient("localhost", 8085, string.Empty, clientLogger);
+
+ InitializeClientEvents(client);
+
+ try
+ {
+ await client.ConnectAsync();
+
+ var task = Task.Run(async () => await SendRandomMessages(client, token));
+
+ Console.WriteLine("Press any key to disconnect from the server...");
+ Console.ReadKey();
+
+ await client.DisconnectAsync();
+ cancellationTokenSource.Cancel();
+ await task;
+ }
+ catch (WebSocketConnectionException exception)
+ {
+ _logger.LogError("Failed to connect to the server: {ExceptionMessage}", exception.Message);
+
+ }
+ catch (Exception exception)
+ {
+ _logger.LogError("An error occurred: {ExceptionMessage}", exception.Message);
+ return;
+ }
+ finally
+ {
+ UnsubscribeEvents(client);
+ }
+ }
+
+ private async Task SendRandomMessages(SimpleWebSocketClient client, CancellationToken cancellationToken)
+ {
+ Random random = new();
+ int messageCount = 1;
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
+ {
+ if (!client.IsConnected)
+ {
+ // If the client is not connected, exit the loop.
+ _logger.LogWarning("Client is not connected. Stopping message sending loop.");
+ break;
+ }
+
+ string message = $"Message {messageCount++} sent at {DateTime.Now}";
+ await client.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
+ _logger.LogInformation("Sent: {message}", message);
+
+ int delay = random.Next(5000, 20001); // Random delay between 5s (5000ms) and 20s (20000ms)
+ await Task.Delay(delay, cancellationToken);
+ }
+ catch (Exception exception)
+ {
+ if (exception is not OperationCanceledException)
+ {
+ _logger.LogError(exception, "Error while sending the message.");
+ }
+ break;
+ }
+ }
+ }
+
+ private void InitializeClientEvents(SimpleWebSocketClient client)
+ {
+ client.Disconnected += Client_Disconnected;
+ client.MessageReceived += Client_MessageReceived;
+ client.BinaryMessageReceived += Client_BinaryMessageReceived;
+ }
+
+ private void UnsubscribeEvents(SimpleWebSocketClient client)
+ {
+ client.Disconnected -= Client_Disconnected;
+ client.MessageReceived -= Client_MessageReceived;
+ client.BinaryMessageReceived -= Client_BinaryMessageReceived;
+ }
+
+
+ private void Client_BinaryMessageReceived(object sender, BinaryMessageReceivedArgs e)
+ {
+ _logger.LogInformation("Binary message received: {binaryMessage}", BitConverter.ToString(e.Message));
+ }
+
+ private void Client_MessageReceived(object sender, MessageReceivedArgs e)
+ {
+ _logger.LogInformation("Message received: {message}", e.Message);
+ }
+
+ private void Client_Disconnected(object sender, DisconnectedArgs e)
+ {
+ _logger.LogInformation("Disconnected");
+ }
+ }
+}
diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/TestInformationAttribute.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/TestInformationAttribute.cs
new file mode 100644
index 0000000..e386c96
--- /dev/null
+++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/TestInformationAttribute.cs
@@ -0,0 +1,12 @@
+// This file is part of the Jung SimpleWebSocket project.
+// The project is licensed under the MIT license.
+
+
+namespace Jung.SimpleWebSocket.IntegrationTests.Tests
+{
+ internal class TestInformationAttribute : Attribute
+ {
+ public string Role { get; set; } = string.Empty;
+ public string Description { get; set; } = string.Empty;
+ }
+}
\ No newline at end of file
diff --git a/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/tests/Jung.SimpleWebSocket.UnitTests/Mock/ILoggerMockHelper.cs b/tests/Jung.SimpleWebSocket.UnitTests/Mock/ILoggerMockHelper.cs
new file mode 100644
index 0000000..b784ee1
--- /dev/null
+++ b/tests/Jung.SimpleWebSocket.UnitTests/Mock/ILoggerMockHelper.cs
@@ -0,0 +1,42 @@
+// 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.SimpleWebSocket.UnitTests.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/tests/Jung.SimpleWebSocket.UnitTests/Mock/LoggerMessages.cs b/tests/Jung.SimpleWebSocket.UnitTests/Mock/LoggerMessages.cs
new file mode 100644
index 0000000..86af14e
--- /dev/null
+++ b/tests/Jung.SimpleWebSocket.UnitTests/Mock/LoggerMessages.cs
@@ -0,0 +1,27 @@
+// 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
+ {
+ 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/tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs
similarity index 57%
rename from Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs
rename to tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs
index 0b058ec..4699925 100644
--- a/Jung.SimpleWebSocketTest/SimpleWebSocketTest.cs
+++ b/tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs
@@ -1,25 +1,24 @@
// This file is part of the Jung SimpleWebSocket project.
// The project is licensed under the MIT license.
-using Jung.SimpleWebSocket;
-using Microsoft.Extensions.Logging;
-using Moq;
+using Jung.SimpleWebSocket.Exceptions;
+using Jung.SimpleWebSocket.Models;
+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
{
- private List _logMessages = [];
- private Mock> _serverLogger;
- private Mock> _clientLogger;
+ private ILoggerMockHelper _serverLoggerMockHelper;
+ private ILoggerMockHelper _clientLoggerMockHelper;
[OneTimeSetUp]
public void SetUpOnce()
@@ -30,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]
@@ -43,26 +39,79 @@ public void EndTest()
Trace.Flush();
}
- private void SetUpLogger(Mock> mock, string loggerName)
+ [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()
{
- mock.Setup(m => m.Log(
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny(),
- It.IsAny>()!
- )).Callback(new InvocationAction(invocation =>
+ // Arrange
+ var serverOptions = new SimpleWebSocketServerOptions
{
- 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, new[] { state, exception });
- _logMessages.Add($"{loggerName}({logLevel}): {logMessage}");
- }));
+ 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]
@@ -70,8 +119,14 @@ private void SetUpLogger(Mock> mock, string loggerName)
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);
+ var serverOptions = new SimpleWebSocketServerOptions
+ {
+ LocalIpAddress = IPAddress.Any,
+ Port = 8010,
+ };
+
+ 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";
@@ -96,7 +151,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld()
server.ClientDisconnected += (sender, obj) =>
{
- receivedClosingDescription = obj.ClosingStatusDescription;
+ receivedClosingDescription = obj.ClosingStatusDescription ?? string.Empty;
disconnectResetEvent.Set();
};
@@ -106,20 +161,48 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld()
messageResetEvent.Set();
};
+ server.ClientUpgradeRequestReceivedAsync += async (sender, args, cancellationToken) =>
+ {
+ // Get the IP address of the client
+ var IpAddress = (args.Client.RemoteEndPoint as IPEndPoint)?.Address;
+ if (IpAddress == null)
+ {
+ args.AcceptRequest = false;
+ return;
+ }
+
+ // Check the IpAddress against the database
+ var isWhitelistedEndPoint = await DbContext_IpAddresses_Contains(IpAddress, cancellationToken);
+ if (!isWhitelistedEndPoint)
+ {
+ args.ResponseContext.StatusCode = HttpStatusCode.Forbidden;
+ args.ResponseContext.BodyContent = "Connection only possible via local network.";
+ args.AcceptRequest = false;
+ }
+ args.Client.Properties["test"] = "test";
+ };
+
// 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, "/", logger: _clientLoggerMockHelper.Logger);
+ await client2.ConnectAsync();
+
+ await Task.Delay(100);
+
+ 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(() =>
@@ -129,13 +212,32 @@ 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(IPAddress ipAddress, CancellationToken cancellationToken)
+ {
+ await Task.Delay(100, cancellationToken);
+ 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_ShouldSendAndReceiveHelloWorld2()
{
// Arrange
- using var server = new SimpleWebSocketServer(IPAddress.Any, 8010, _serverLogger.Object);
- using var client = new SimpleWebSocketClient(IPAddress.Loopback.ToString(), 8010, "/", _clientLogger.Object);
+ var serverOptions = new SimpleWebSocketServerOptions
+ {
+ LocalIpAddress = IPAddress.Any,
+ Port = 8010
+ };
+
+ 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";
@@ -182,7 +284,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(() =>
@@ -197,7 +299,13 @@ 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
+ };
+
+ using var server = new SimpleWebSocketServer(serverOptions);
List clients = [];
var message = "Hello World";
const int clientsCount = 200;
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 95%
rename from Jung.SimpleWebSocketTest/WebSocketUpgradeHandlerTests.cs
rename to tests/Jung.SimpleWebSocket.UnitTests/WebSocketUpgradeHandlerTests.cs
index f8ba27e..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
{
@@ -64,7 +66,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, new WebContext(), null, cancellationToken);
// Assert
Assert.That(response, Does.Contain("HTTP/1.1 101 Switching Protocols"));
@@ -90,7 +92,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, new WebContext(), serverSubprotocol, cancellationToken);
// Assert
Assert.Multiple(() =>