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