diff --git a/Jung.SimpleWebSocket.sln b/Jung.SimpleWebSocket.sln
index 6e0dc3c..fec2fcb 100644
--- a/Jung.SimpleWebSocket.sln
+++ b/Jung.SimpleWebSocket.sln
@@ -3,12 +3,26 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.11.35222.181
MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{0487DC39-481D-4828-81A5-58CF9BCA2E98}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{68AB7986-ED88-4C74-A447-934ED6D1B657}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jung.SimpleWebSocket", "src\Jung.SimpleWebSocket\Jung.SimpleWebSocket.csproj", "{793B04E9-6326-425A-A29C-A736CFD1E0C0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jung.SimpleWebSocket.UnitTests", "tests\Jung.SimpleWebSocket.UnitTests\Jung.SimpleWebSocket.UnitTests.csproj", "{26725C3C-8E90-49AC-9EE4-2A77ADB2229D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jung.SimpleWebSocket.IntegrationTests", "tests\Jung.SimpleWebSocket.IntegrationTests\Jung.SimpleWebSocket.IntegrationTests.csproj", "{D052400A-9F1E-4F2E-98B9-AF74A7A16A2F}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicServerExample", "examples\BasicServerExample\BasicServerExample.csproj", "{0C73E461-DE3D-4D14-B81B-732B7C6971A1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicClientExample", "examples\BasicClientExample\BasicClientExample.csproj", "{9D4AD09E-B6FF-4E2A-894E-49B97729E190}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicUserHandlingServerExample", "examples\BasicUserHandlingServerExample\BasicUserHandlingServerExample.csproj", "{A538895A-481B-44A5-8E6F-6D617C3F5378}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicUserHandlingClientExample", "examples\BasicUserHandlingClientExample\BasicUserHandlingClientExample.csproj", "{C79EBA14-EFA6-424D-9C6E-609C98994473}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -27,10 +41,35 @@ Global
{D052400A-9F1E-4F2E-98B9-AF74A7A16A2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D052400A-9F1E-4F2E-98B9-AF74A7A16A2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D052400A-9F1E-4F2E-98B9-AF74A7A16A2F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0C73E461-DE3D-4D14-B81B-732B7C6971A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0C73E461-DE3D-4D14-B81B-732B7C6971A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0C73E461-DE3D-4D14-B81B-732B7C6971A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0C73E461-DE3D-4D14-B81B-732B7C6971A1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9D4AD09E-B6FF-4E2A-894E-49B97729E190}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9D4AD09E-B6FF-4E2A-894E-49B97729E190}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9D4AD09E-B6FF-4E2A-894E-49B97729E190}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9D4AD09E-B6FF-4E2A-894E-49B97729E190}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A538895A-481B-44A5-8E6F-6D617C3F5378}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A538895A-481B-44A5-8E6F-6D617C3F5378}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A538895A-481B-44A5-8E6F-6D617C3F5378}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A538895A-481B-44A5-8E6F-6D617C3F5378}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C79EBA14-EFA6-424D-9C6E-609C98994473}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C79EBA14-EFA6-424D-9C6E-609C98994473}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C79EBA14-EFA6-424D-9C6E-609C98994473}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C79EBA14-EFA6-424D-9C6E-609C98994473}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {793B04E9-6326-425A-A29C-A736CFD1E0C0} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {26725C3C-8E90-49AC-9EE4-2A77ADB2229D} = {68AB7986-ED88-4C74-A447-934ED6D1B657}
+ {D052400A-9F1E-4F2E-98B9-AF74A7A16A2F} = {68AB7986-ED88-4C74-A447-934ED6D1B657}
+ {0C73E461-DE3D-4D14-B81B-732B7C6971A1} = {0487DC39-481D-4828-81A5-58CF9BCA2E98}
+ {9D4AD09E-B6FF-4E2A-894E-49B97729E190} = {0487DC39-481D-4828-81A5-58CF9BCA2E98}
+ {A538895A-481B-44A5-8E6F-6D617C3F5378} = {0487DC39-481D-4828-81A5-58CF9BCA2E98}
+ {C79EBA14-EFA6-424D-9C6E-609C98994473} = {0487DC39-481D-4828-81A5-58CF9BCA2E98}
+ EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5F0E3FEC-7DDE-4E02-941B-CEF2DE33DB1C}
EndGlobalSection
diff --git a/examples/BasicClientExample/BasicClientExample.csproj b/examples/BasicClientExample/BasicClientExample.csproj
new file mode 100644
index 0000000..6467dee
--- /dev/null
+++ b/examples/BasicClientExample/BasicClientExample.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/examples/BasicClientExample/Program.cs b/examples/BasicClientExample/Program.cs
new file mode 100644
index 0000000..fec5318
--- /dev/null
+++ b/examples/BasicClientExample/Program.cs
@@ -0,0 +1,59 @@
+// This file is part of the Jung SimpleWebSocket project.
+// The project is licensed under the MIT license.
+
+using Jung.SimpleWebSocket;
+
+namespace BasicClientExample
+{
+ internal class Program
+ {
+ ///
+ /// An example of a basic WebSocket client using the Jung.SimpleWebSocket library.
+ ///
+ ///
+ static void Main(string[] args)
+ {
+ // Create the WebSocket client and connect to the server at ws://127.0.0.1:8085/chat
+ using var simpleWebSocketClient = new SimpleWebSocketClient("127.0.0.1", 8085, "/chat");
+
+ // Subscribe to client events
+ simpleWebSocketClient.Disconnected += (s, e) => Console.WriteLine($"Disconnected from the server. Reason: {e.ClosingStatusDescription}");
+ simpleWebSocketClient.MessageReceived += (s, e) => Console.WriteLine($"Message received from server: {e.Message}");
+ simpleWebSocketClient.BinaryMessageReceived += (s, e) => Console.WriteLine($"Binary message received from server: {BitConverter.ToString(e.Message)}");
+
+ try
+ {
+ // Connect to the server
+ simpleWebSocketClient.ConnectAsync().GetAwaiter().GetResult();
+
+ // Simulate any delay
+ Thread.Sleep(1000);
+
+ // Send a message to the server
+ Console.WriteLine("Sending message to the server: Hello, Server!");
+ simpleWebSocketClient.SendMessageAsync("Hello, Server!").GetAwaiter().GetResult();
+
+ // Keep the server running until a key is pressed
+ Console.WriteLine("Press Enter to stop the server...");
+ Console.ReadKey();
+
+ // You do not have to explicitly disconnect the client because of the using statement
+ // simpleWebSocketClient.DisconnectAsync("Client is shutting down").Wait();
+ }
+ catch (Exception exception)
+ {
+
+ var exceptionMessage = exception.Message;
+ if (exception.InnerException != null)
+ {
+ exceptionMessage += $" Inner exception: {exception.InnerException.Message}";
+ }
+ Console.WriteLine($"An error occurred: {exceptionMessage}");
+
+ // Keep the console application running until a key is pressed
+ Console.WriteLine("Press Enter close this window...");
+ Console.ReadKey();
+ }
+ }
+ }
+}
diff --git a/examples/BasicServerExample/BasicServerExample.csproj b/examples/BasicServerExample/BasicServerExample.csproj
new file mode 100644
index 0000000..6467dee
--- /dev/null
+++ b/examples/BasicServerExample/BasicServerExample.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/examples/BasicServerExample/Program.cs b/examples/BasicServerExample/Program.cs
new file mode 100644
index 0000000..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/src/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs b/src/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs
index 1a73240..9c00f62 100644
--- a/src/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs
+++ b/src/Jung.SimpleWebSocket/Contracts/IWebSocketClient.cs
@@ -2,6 +2,7 @@
// The project is licensed under the MIT license.
using Jung.SimpleWebSocket.Delegates;
+using Jung.SimpleWebSocket.Models.EventArguments;
namespace Jung.SimpleWebSocket.Contracts;
@@ -45,6 +46,15 @@ public interface IWebSocketClient : IDisposable
///
event DisconnectedEventHandler? Disconnected;
+ ///
+ /// Occurs before an upgrade request is sent, allowing the request to be inspected or modified asynchronously.
+ ///
+ /// This event is triggered when an upgrade request is about to be sent. Subscribers can use this
+ /// event to inspect or modify the request by handling the parameter. The
+ /// event handler is asynchronous, so any modifications or operations should be performed within the provided
+ /// asynchronous context.
+ event AsyncEventHandler? SendingUpgradeRequestAsync;
+
///
/// Sends a message to all connected clients asynchronously.
///
diff --git a/src/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs b/src/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs
index a12093e..03d8b27 100644
--- a/src/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs
+++ b/src/Jung.SimpleWebSocket/Contracts/IWebSocketServer.cs
@@ -5,6 +5,7 @@
using Jung.SimpleWebSocket.Exceptions;
using Jung.SimpleWebSocket.Models;
using Jung.SimpleWebSocket.Models.EventArguments;
+using System.Diagnostics.CodeAnalysis;
using System.Net;
namespace Jung.SimpleWebSocket.Contracts;
@@ -71,6 +72,14 @@ public interface IWebSocketServer : IDisposable
/// The client
WebSocketServerClient GetClientById(string clientId);
+ ///
+ /// Attempts to get a client by its id.
+ ///
+ /// The id of the client
+ /// The client if found, otherwise null
+ /// if the client was found, otherwise ."
+ bool TryGetClientById(string clientId, [NotNullWhen(true)] out WebSocketServerClient? client);
+
///
/// Sends a message to all connected clients asynchronously.
///
diff --git a/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs b/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs
index c5f68b8..fa1288d 100644
--- a/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs
+++ b/src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs
@@ -89,7 +89,16 @@ internal async Task AcceptWebSocketAsync()
/// The response context to send to the client.
internal async Task RejectWebSocketAsync(WebContext responseContext)
{
- // The client is rejected
+ // If the status code is SwitchingProtocols, change it to BadRequest
+ if (responseContext.StatusCode == System.Net.HttpStatusCode.SwitchingProtocols)
+ {
+ // If we would send a SwitchingProtocols status code, the client would expect a WebSocket connection.
+ // We want to reject the connection, so we send a BadRequest status code.
+ // We could get here if the user sets the status code to SwitchingProtocols in the upgrade event.
+ responseContext.StatusCode = System.Net.HttpStatusCode.BadRequest;
+ }
+
+ // Reject the client
await _upgradeHandler!.RejectWebSocketAsync(responseContext, _cancellationToken).ConfigureAwait(false);
Cleanup();
}
diff --git a/src/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs
index 51cf852..eda1d8e 100644
--- a/src/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs
+++ b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientDisconnectedArgs.cs
@@ -7,5 +7,9 @@ namespace Jung.SimpleWebSocket.Models.EventArguments;
/// Represents the arguments of the event when a client disconnects from the server.
///
/// The reason for the connection closure. if the remote party closed the WebSocket connection without completing the close handshake.
-/// The unique identifier of the client that disconnected from the server.
-public record ClientDisconnectedArgs(string? ClosingStatusDescription, string ClientId);
+/// The Client that disconnected from the server.
+public record ClientDisconnectedArgs(
+ string? ClosingStatusDescription,
+ // We use the client object here instead of just the client ID to give more context about the disconnected client.
+ // When the event is fired, the client is already removed from the active clients list, so we can't access it there.
+ WebSocketServerClient Client);
diff --git a/src/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs
index 0f238b0..e7b3150 100644
--- a/src/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs
+++ b/src/Jung.SimpleWebSocket/Models/EventArguments/ClientUpgradeRequestReceivedArgs.cs
@@ -16,9 +16,9 @@ public record ClientUpgradeRequestReceivedArgs(WebSocketServerClient Client, Web
private WebContext? _responseContext;
///
- /// Gets or sets a value indicating whether the upgrade request should be handled.
+ /// Gets or sets a value indicating whether the upgrade request should be Accepted. Default is true.
///
- public bool Handle { get; set; } = true;
+ public bool AcceptRequest { get; set; } = true;
///
/// The context that is being use to response to the client.
diff --git a/src/Jung.SimpleWebSocket/Models/EventArguments/SendingUpdateRequestArgs.cs b/src/Jung.SimpleWebSocket/Models/EventArguments/SendingUpdateRequestArgs.cs
new file mode 100644
index 0000000..4d59437
--- /dev/null
+++ b/src/Jung.SimpleWebSocket/Models/EventArguments/SendingUpdateRequestArgs.cs
@@ -0,0 +1,13 @@
+// This file is part of the Jung SimpleWebSocket project.
+// The project is licensed under the MIT license.
+
+using Microsoft.Extensions.Logging;
+
+namespace Jung.SimpleWebSocket.Models.EventArguments;
+
+///
+/// Represents the arguments of the event when a upgrade request is sent to a server.
+///
+/// The context of the request.
+/// The current Logger.
+public record SendingUpgradeRequestArgs(WebContext WebContext, ILogger? Logger);
diff --git a/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs b/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs
index 89e5383..f77671e 100644
--- a/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs
+++ b/src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs
@@ -6,6 +6,7 @@
using Jung.SimpleWebSocket.Exceptions;
using Jung.SimpleWebSocket.Models;
using Jung.SimpleWebSocket.Models.EventArguments;
+using Jung.SimpleWebSocket.Utility;
using Jung.SimpleWebSocket.Wrappers;
using Microsoft.Extensions.Logging;
using System.Net.Sockets;
@@ -43,6 +44,9 @@ public class SimpleWebSocketClient(string hostName, int port, string requestPath
///
public event BinaryMessageReceivedEventHandler? BinaryMessageReceived;
+ ///
+ public event AsyncEventHandler? SendingUpgradeRequestAsync;
+
///
/// The CancellationTokenSource for the client.
///
@@ -114,7 +118,7 @@ public async Task ConnectAsync(CancellationToken? cancellationToken = null)
{
throw new WebSocketConnectionException(message: "Error connecting to Server", innerException: exception);
}
- else if (exception is WebSocketException)
+ else if (exception is WebSocketException || exception is WebSocketUpgradeException)
{
throw;
}
@@ -173,6 +177,7 @@ private async Task HandleWebSocketInitiation(TcpClientWrapper client, Cancellati
var socketWrapper = new WebSocketUpgradeHandler(_stream);
var requestContext = WebContext.CreateRequest(HostName, Port, RequestPath);
+ requestContext = await RaiseUpgradeEventAsync(requestContext, cancellationToken).ConfigureAwait(false);
await socketWrapper.SendUpgradeRequestAsync(requestContext, cancellationToken).ConfigureAwait(false);
var response = await socketWrapper.AwaitContextAsync(cancellationToken).ConfigureAwait(false);
WebSocketUpgradeHandler.ValidateUpgradeResponse(response, requestContext);
@@ -180,6 +185,19 @@ private async Task HandleWebSocketInitiation(TcpClientWrapper client, Cancellati
_webSocket = socketWrapper.CreateWebSocket(isServer: false);
}
+ ///
+ /// Raises the upgrade event.
+ ///
+ /// The request context to use for the upgrade event
+ /// The cancellation token
+ /// The event arguments of the upgrade request.
+ internal async Task RaiseUpgradeEventAsync(WebContext requestContext, CancellationToken cancellationToken)
+ {
+ var eventArgs = new SendingUpgradeRequestArgs(requestContext, _logger);
+ await AsyncEventRaiser.RaiseAsync(SendingUpgradeRequestAsync, this, eventArgs, cancellationToken).ConfigureAwait(false);
+ return requestContext;
+ }
+
///
public async Task SendMessageAsync(string message, CancellationToken? cancellationToken = null)
{
@@ -264,9 +282,17 @@ public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) == 0)
{
+ // Unsubscribe all event handlers
+ Disconnected = null;
+ MessageReceived = null;
+ BinaryMessageReceived = null;
+ SendingUpgradeRequestAsync = null;
+
+ // Dispose managed resources
_cancellationTokenSource?.Cancel();
_stream?.Dispose();
_client?.Dispose();
+
GC.SuppressFinalize(this);
}
}
diff --git a/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs b/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs
index fd296a6..6d02221 100644
--- a/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs
+++ b/src/Jung.SimpleWebSocket/SimpleWebSocketServer.cs
@@ -12,6 +12,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.WebSockets;
using System.Text;
@@ -267,8 +268,26 @@ public WebSocketServerClient GetClientById(string clientId)
{
ThrowIfDisposed();
- if (!ActiveClients.TryGetValue(clientId, out var client)) throw new WebSocketServerException(message: "Client not found");
- return client;
+ if (TryGetClientById(clientId, out var client))
+ {
+ return client;
+ }
+ throw new WebSocketServerException(message: "Client not found");
+ }
+
+ ///
+ public bool TryGetClientById(string clientId, [NotNullWhen(true)] out WebSocketServerClient? client)
+ {
+ ThrowIfDisposed();
+
+ if (ActiveClients.TryGetValue(clientId, out client))
+ {
+ if (client != null)
+ {
+ return true;
+ }
+ }
+ return false;
}
///
@@ -305,7 +324,7 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT
var eventArgs = await flow.RaiseUpgradeEventAsync(ClientUpgradeRequestReceivedAsync).ConfigureAwait(false);
// Respond to the upgrade request
- if (eventArgs.Handle)
+ if (eventArgs.AcceptRequest)
{
// Accept the WebSocket connection
await flow.AcceptWebSocketAsync().ConfigureAwait(false);
@@ -333,10 +352,6 @@ private async Task HandleClientAsync(WebSocketServerClient client, CancellationT
{
// Ignore the exception, because it is thrown when cancellation is requested
}
- catch (UserNotHandledException userNotHandledException)
- {
- await flow.RejectWebSocketAsync(userNotHandledException.ResponseContext).ConfigureAwait(false);
- }
catch (Exception exception)
{
Logger?.LogError(exception, "Error while handling the Client {clientId}", flow.Client.Id);
@@ -407,7 +422,7 @@ private async Task ProcessWebSocketMessagesAsync(WebSocketServerClient client, C
// if we leave the loop, the client disconnected
if (!IsShuttingDown)
{
- AsyncEventRaiser.RaiseAsyncInNewTask(ClientDisconnected, this, new ClientDisconnectedArgs(closeStatusDescription, client.Id), cancellationToken);
+ AsyncEventRaiser.RaiseAsyncInNewTask(ClientDisconnected, this, new ClientDisconnectedArgs(closeStatusDescription, client), cancellationToken);
}
}
}
@@ -422,10 +437,19 @@ public void Dispose()
try
{
+ // unsubscribe all event handlers
+ ClientConnected = null;
+ ClientDisconnected = null;
+ MessageReceived = null;
+ BinaryMessageReceived = null;
+ ClientUpgradeRequestReceivedAsync = null;
+
+ // shutdown server and free resources
ShutdownServer().GetAwaiter().GetResult();
_cancellationTokenSource?.Cancel();
_tcpListener?.Dispose();
_tcpListener = null;
+
GC.SuppressFinalize(this);
}
finally
diff --git a/src/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs b/src/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs
index 57ded7b..e7ea051 100644
--- a/src/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs
+++ b/src/Jung.SimpleWebSocket/Wrappers/WebSocketUpgradeHandler.cs
@@ -105,7 +105,7 @@ private async Task SendWebSocketResponseHeaders(WebContext context, Cancellation
private async Task SendWebSocketRejectResponse(WebContext context, CancellationToken cancellationToken)
{
var sb = new StringBuilder(
- $"HTTP/1.1 409 Conflict\r\n");
+ $"HTTP/1.1 {(int)context.StatusCode} {context.StatusDescription}\r\n");
AddHeaders(context, sb);
CompleteHeaderSection(sb);
AddBody(context, sb);
@@ -299,9 +299,22 @@ internal IWebSocket CreateWebSocket(bool isServer, TimeSpan? keepAliveInterval =
internal async Task RejectWebSocketAsync(WebContext response, CancellationToken cancellationToken)
{
+ // This header is optional, but recommended to inform the client that the connection will be closed
response.Headers.Add("Connection", "close");
- response.Headers.Add("Content-Type", "text/plain");
- response.Headers.Add("Content-Length", response.BodyContent.Length.ToString());
+
+ // If there is body content, ensure Content-Type and Content-Length headers are set
+ if (!string.IsNullOrEmpty(response.BodyContent))
+ {
+ // Set default Content-Type if not already set
+ if (response.Headers["Content-Type"] == null)
+ {
+ response.Headers.Add("Content-Type", "text/plain");
+ }
+ // Set Content-Length based on the body content length
+ response.Headers.Add("Content-Length", response.BodyContent.Length.ToString());
+ }
+
+ // Send the rejection response
await SendWebSocketRejectResponse(response, cancellationToken).ConfigureAwait(false);
}
}
\ No newline at end of file
diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/DisplayEventsServerTest.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/DisplayEventsServerTest.cs
index 129ae9e..2ad1875 100644
--- a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/DisplayEventsServerTest.cs
+++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/DisplayEventsServerTest.cs
@@ -56,7 +56,7 @@ private void SimpleWebSocketServer_ClientConnected(object? sender, ClientConnect
private void SimpleWebSocketServer_ClientDisconnected(object? sender, ClientDisconnectedArgs e)
{
- _logger.LogInformation("Client disconnected: {ClientId}", e.ClientId);
+ _logger.LogInformation("Client disconnected: {ClientId}", e.Client.Id);
}
private void SimpleWebSocketServer_MessageReceived(object? sender, ClientMessageReceivedArgs e)
diff --git a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs
index 9bd26ba..1638852 100644
--- a/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs
+++ b/tests/Jung.SimpleWebSocket.IntegrationTests/Tests/SendMessagesLoopTest.cs
@@ -15,7 +15,7 @@ internal override async Task RunAsync()
var cancellationTokenSource = new CancellationTokenSource();
var token = cancellationTokenSource.Token;
- using var client = new SimpleWebSocketClient("localhost", 8085, "", clientLogger);
+ using var client = new SimpleWebSocketClient("localhost", 8085, string.Empty, clientLogger);
InitializeClientEvents(client);
diff --git a/tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs b/tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs
index 762f70d..4699925 100644
--- a/tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs
+++ b/tests/Jung.SimpleWebSocket.UnitTests/SimpleWebSocketTest.cs
@@ -167,7 +167,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld()
var IpAddress = (args.Client.RemoteEndPoint as IPEndPoint)?.Address;
if (IpAddress == null)
{
- args.Handle = false;
+ args.AcceptRequest = false;
return;
}
@@ -177,7 +177,7 @@ public async Task TestClientServerConnection_ShouldSendAndReceiveHelloWorld()
{
args.ResponseContext.StatusCode = HttpStatusCode.Forbidden;
args.ResponseContext.BodyContent = "Connection only possible via local network.";
- args.Handle = false;
+ args.AcceptRequest = false;
}
args.Client.Properties["test"] = "test";
};