Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Jung.SimpleWebSocket.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Represents an exception that occurs during a WebSocket connection attempt.
/// </summary>
public class WebSocketConnectionException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="WebSocketConnectionException"/> class.
/// </summary>
public WebSocketConnectionException() { }

/// <summary>
/// Initializes a new instance of the <see cref="WebSocketConnectionException"/> class with a specified error message.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
public WebSocketConnectionException(string message) : base(message) { }

/// <summary>
/// Initializes a new instance of the <see cref="WebSocketConnectionException"/> class with a specified error
/// message and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or <see langword="null"/> if no inner exception is
/// specified.</param>
public WebSocketConnectionException(string message, Exception innerException) : base(message, innerException) { }
}
}
8 changes: 4 additions & 4 deletions src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/// <summary>
Expand All @@ -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));
Expand All @@ -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();
}

Expand All @@ -113,7 +113,7 @@ internal void HandleDisconnectedClient()
internal async Task<ClientUpgradeRequestReceivedArgs> RaiseUpgradeEventAsync(AsyncEventHandler<ClientUpgradeRequestReceivedArgs>? 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

namespace Jung.SimpleWebSocket.Models.EventArguments;

/// <summary>
/// Represents the arguments of the event when a client disconnects from the server.
/// </summary>
/// <param name="ClosingStatusDescription">The description why the closing status was initiated.</param>
/// <param name="ClientId">The unique identifier of the client that disconnected from the server.</param>
public record ClientDisconnectedArgs(string ClosingStatusDescription, string ClientId);
/// <summary>
/// Represents the arguments of the event when a client disconnects from the server.
/// </summary>
/// <param name="ClosingStatusDescription">The reason for the connection closure. <see langword="null"/> if the remote party closed the WebSocket connection without completing the close handshake.</param>
/// <param name="ClientId">The unique identifier of the client that disconnected from the server.</param>
public record ClientDisconnectedArgs(string? ClosingStatusDescription, string ClientId);
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,10 @@ public class SimpleWebSocketServerOptions
/// Gets or sets the port of the server.
/// </summary>
public int Port { get; set; }

/// <summary>
/// Gets or sets the log level of the server.
/// </summary>
public string LogLevel { get; set; } = "Information";
}
}
125 changes: 83 additions & 42 deletions src/Jung.SimpleWebSocket/SimpleWebSocketClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -23,7 +24,7 @@ namespace Jung.SimpleWebSocket
/// <param name="port">The port to connect to</param>
/// <param name="requestPath">The web socket request path</param>
/// <param name="logger">A logger to write internal log messages</param>
public class SimpleWebSocketClient(string hostName, int port, string requestPath, ILogger? logger = null) : IWebSocketClient, IDisposable
public class SimpleWebSocketClient(string hostName, int port, string requestPath, ILogger<SimpleWebSocketClient>? logger = null) : IWebSocketClient, IDisposable
{
/// <inheritdoc/>
public string HostName { get; } = hostName;
Expand Down Expand Up @@ -64,8 +65,21 @@ public class SimpleWebSocketClient(string hostName, int port, string requestPath

/// <summary>
/// A value indicating whether the client is disconnecting.
/// <para>0=Not disconnecting, 1=Disconnecting</para>
/// </summary>
private bool _clientIsDisconnecting;
private int _clientIsDisconnecting = 0;


/// <summary>
/// A value indicating whether the client is disposed.
/// <para>0=Not Disposed, 1=Disposed</para>
/// </summary>
private int _disposed = 0;

/// <summary>
/// Gets a value indicating whether the client is disposed.
/// </summary>
private bool Disposed => _disposed == 1;

/// <summary>
/// The logger to write internal log messages.
Expand All @@ -75,6 +89,8 @@ public class SimpleWebSocketClient(string hostName, int port, string requestPath
/// <inheritdoc/>
public async Task ConnectAsync(CancellationToken? cancellationToken = null)
{
ThrowIfDisposed();

if (IsConnected) throw new WebSocketClientException(message: "Client is already connected");
cancellationToken ??= CancellationToken.None;

Expand All @@ -85,16 +101,20 @@ 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);
}
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;
}
Expand All @@ -108,19 +128,22 @@ public async Task ConnectAsync(CancellationToken? cancellationToken = null)
/// <inheritdoc/>
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)
{
Expand All @@ -135,7 +158,6 @@ public async Task DisconnectAsync(string closingStatusDescription = "Closing", C
}
}
}
_client?.Dispose();
}

/// <summary>
Expand All @@ -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);
Expand All @@ -161,6 +183,8 @@ private async Task HandleWebSocketInitiation(TcpClientWrapper client, Cancellati
/// <inheritdoc/>
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");

Expand All @@ -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<byte>(buffer), WebSocketMessageType.Text, true, linkedTokenSource.Token);
await _webSocket.SendAsync(new ArraySegment<byte>(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);
}
}
Expand All @@ -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<byte>(buffer), cancellationToken);
// Read the next message
WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(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);
}
}

/// <inheritdoc/>
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);
}
}
}
Loading