Skip to content
Closed
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
10 changes: 8 additions & 2 deletions Jung.SimpleWebSocket.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.11.35222.181
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jung.SimpleWebSocket", "Jung.SimpleWebSocket\Jung.SimpleWebSocket.csproj", "{793B04E9-6326-425A-A29C-A736CFD1E0C0}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jung.SimpleWebSocket", "src\Jung.SimpleWebSocket\Jung.SimpleWebSocket.csproj", "{793B04E9-6326-425A-A29C-A736CFD1E0C0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jung.SimpleWebSocketTest", "Jung.SimpleWebSocketTest\Jung.SimpleWebSocketTest.csproj", "{26725C3C-8E90-49AC-9EE4-2A77ADB2229D}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jung.SimpleWebSocket.UnitTests", "tests\Jung.SimpleWebSocket.UnitTests\Jung.SimpleWebSocket.UnitTests.csproj", "{26725C3C-8E90-49AC-9EE4-2A77ADB2229D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jung.SimpleWebSocket.IntegrationTests", "tests\Jung.SimpleWebSocket.IntegrationTests\Jung.SimpleWebSocket.IntegrationTests.csproj", "{144CF5BC-D92F-4BC4-80E9-A2FABA93C8A2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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
{144CF5BC-D92F-4BC4-80E9-A2FABA93C8A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{144CF5BC-D92F-4BC4-80E9-A2FABA93C8A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{144CF5BC-D92F-4BC4-80E9-A2FABA93C8A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{144CF5BC-D92F-4BC4-80E9-A2FABA93C8A2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@
[assembly: Guid("ca34219d-7a2e-4993-ad9d-f27fda1bb9dc")]

// Make internals visible to the test project and the dynamic proxy assembly (moq)
[assembly: InternalsVisibleTo("Jung.SimpleWebSocketTest")]
[assembly: InternalsVisibleTo("Jung.SimpleWebSocket.UnitTests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The project is licensed under the MIT license.

using Jung.SimpleWebSocket.Delegates;
using Jung.SimpleWebSocket.Exceptions;
using Jung.SimpleWebSocket.Models;
using Jung.SimpleWebSocket.Models.EventArguments;
using System.Net;
Expand Down Expand Up @@ -41,22 +42,27 @@ public interface IWebSocketServer : IDisposable
/// <summary>
/// Event that is raised when a client is connected.
/// </summary>
event ClientConnectedEventHandler? ClientConnected;
event EventHandler<ClientConnectedArgs>? ClientConnected;

/// <summary>
/// Event that is raised when a client is disconnected.
/// </summary>
event ClientDisconnectedEventHandler ClientDisconnected;
event EventHandler<ClientDisconnectedArgs>? ClientDisconnected;

/// <summary>
/// Event that is raised when a message is received from a client.
/// </summary>
event ClientMessageReceivedEventHandler? MessageReceived;
event EventHandler<ClientMessageReceivedArgs>? MessageReceived;

/// <summary>
/// Event that is raised when a binary message is received from a client.
/// </summary>
event ClientBinaryMessageReceivedEventHandler? BinaryMessageReceived;
event EventHandler<ClientBinaryMessageReceivedArgs>? BinaryMessageReceived;

/// <summary>
/// Async Event that is raised when a client upgrade request is received.
/// </summary>
event AsyncEventHandler<ClientUpgradeRequestReceivedArgs>? ClientUpgradeRequestReceivedAsync;

/// <summary>
/// Gets a client by its id.
Expand Down Expand Up @@ -87,4 +93,13 @@ public interface IWebSocketServer : IDisposable
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
void Start(CancellationToken? cancellationToken = null);

/// <summary>
/// Changes the id of a client.
/// </summary>
/// <param name="client">The client to update</param>
/// <param name="newId">The new id of the client</param>
/// <exception cref="ClientNotFoundException">Throws when the client is not found</exception>
/// <exception cref="ClientIdAlreadyExistsException">Throws when the new id is already in use</exception>
void ChangeClientId(WebSocketServerClient client, string newId);
}
14 changes: 14 additions & 0 deletions src/Jung.SimpleWebSocket/Delegates/AsyncEventHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// This file is part of the Jung SimpleWebSocket project.
// The project is licensed under the MIT license.

namespace Jung.SimpleWebSocket.Delegates;

/// <summary>
/// Represents an asynchronous event handler.
/// </summary>
/// <typeparam name="TEventArgs">The type of the event arguments.</typeparam>
/// <param name="sender">The sender of the event.</param>
/// <param name="e">The event arguments.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public delegate Task AsyncEventHandler<TEventArgs>(object sender, TEventArgs e, CancellationToken cancellationToken) where TEventArgs : class;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// This file is part of the Jung SimpleWebSocket project.
// The project is licensed under the MIT license.

namespace Jung.SimpleWebSocket.Exceptions
{
/// <summary>
/// Exception thrown when a client with the same id already exists in the client list.
/// </summary>
/// <param name="message">The message to display when the exception is thrown.</param>
public class ClientIdAlreadyExistsException(string message) : Exception(message)
{
}
}
13 changes: 13 additions & 0 deletions src/Jung.SimpleWebSocket/Exceptions/ClientNotFoundException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// This file is part of the Jung SimpleWebSocket project.
// The project is licensed under the MIT license.

namespace Jung.SimpleWebSocket.Exceptions
{
/// <summary>
/// Exception thrown when a client with the given id is not found in the client list.
/// </summary>
/// <param name="message">The message to display when the exception is thrown.</param>
public class ClientNotFoundException(string message) : Exception(message)
{
}
}
13 changes: 13 additions & 0 deletions src/Jung.SimpleWebSocket/Exceptions/UserNotHandledException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// This file is part of the Jung SimpleWebSocket project.
// The project is licensed under the MIT license.

using Jung.SimpleWebSocket.Models;

namespace Jung.SimpleWebSocket.Exceptions
{
[Serializable]
internal class UserNotHandledException(WebContext responseContext) : Exception
{
public WebContext ResponseContext { get; set; } = responseContext;
}
}
153 changes: 153 additions & 0 deletions src/Jung.SimpleWebSocket/Flows/ClientHandlingFlow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// This file is part of the Jung SimpleWebSocket project.
// The project is licensed under the MIT license.

using Jung.SimpleWebSocket.Delegates;
using Jung.SimpleWebSocket.Models;
using Jung.SimpleWebSocket.Models.EventArguments;
using Jung.SimpleWebSocket.Utility;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;

namespace Jung.SimpleWebSocket.Flows
{
/// <summary>
/// A flow that handles the client connection.
/// </summary>
/// <remarks>
/// Creates a new instance of the <see cref="ClientHandlingFlow"/> class.
/// </remarks>
/// <param name="client">The client to handle.</param>
/// <param name="server">The server that handles the client.</param>
/// <param name="cancellationToken">The cancellation token of the server.</param>
internal class ClientHandlingFlow(SimpleWebSocketServer server, WebSocketServerClient client, CancellationToken cancellationToken)
{
/// <summary>
/// Gets the client associated with the flow.
/// </summary>
internal WebSocketServerClient Client { get; set; } = client;

/// <summary>
/// Gets the request context of the client.
/// </summary>
internal WebContext? Request { get; set; } = null!;

/// <summary>
/// Gets the upgrade handler for the client.
/// </summary>
private WebSocketUpgradeHandler? _upgradeHandler = null;

/// <summary>
/// Gets the response context that is being use to response to the client.
/// </summary>
private WebContext? _responseContext = null;

/// <summary>
/// Gets the active clients of the server.
/// </summary>
private readonly ConcurrentDictionary<string, WebSocketServerClient> _activeClients = server.ActiveClients;

/// <summary>
/// Gets the logger of the server.
/// </summary>
private readonly ILogger? _logger = server.Logger;

/// <summary>
/// Gets the cancellation token of the server.
/// </summary>
private readonly CancellationToken _cancellationToken = cancellationToken;

/// <summary>
/// Loads the request context.
/// </summary>
internal async Task LoadRequestContext()
{
var stream = Client.ClientConnection!.GetStream();
_upgradeHandler = new WebSocketUpgradeHandler(stream);
Request = await _upgradeHandler.AwaitContextAsync(_cancellationToken);
}

/// <summary>
/// Accepts the web socket connection.
/// </summary>
internal async Task AcceptWebSocketAsync()
{
// Check if the response context are initialized
ThrowForResponseContextNotInitialized(_responseContext);

// The client is accepted
await _upgradeHandler!.AcceptWebSocketAsync(Request!, _responseContext, null, _cancellationToken);

// Use the web socket for the client
Client.UseWebSocket(_upgradeHandler.CreateWebSocket(isServer: true));
Cleanup();
}

/// <summary>
/// Rejects the web socket connection.
/// </summary>
/// <param name="responseContext">The response context to send to the client.</param>
internal async Task RejectWebSocketAsync(WebContext responseContext)
{
// The client is rejected
await _upgradeHandler!.RejectWebSocketAsync(responseContext, _cancellationToken);
Cleanup();
}

/// <summary>
/// Handles the disconnected client.
/// </summary>
internal void HandleDisconnectedClient()
{
_activeClients.TryRemove(Client.Id, out _);
Client.Dispose();

_logger?.LogDebug("Client {clientId} is removed.", Client.Id);
}

/// <summary>
/// Raises the upgrade event.
/// </summary>
/// <param name="clientUpgradeRequestReceivedAsync">The event handler for the upgrade request.</param>
/// <returns>The event arguments of the upgrade request.</returns>
internal async Task<ClientUpgradeRequestReceivedArgs> RaiseUpgradeEventAsync(AsyncEventHandler<ClientUpgradeRequestReceivedArgs>? clientUpgradeRequestReceivedAsync)
{
var eventArgs = new ClientUpgradeRequestReceivedArgs(Client, Request!, _logger);
await AsyncEventRaiser.RaiseAsync(clientUpgradeRequestReceivedAsync, server, eventArgs, _cancellationToken);
_responseContext = eventArgs.ResponseContext;
return eventArgs;
}

/// <summary>
/// Tries to add the client to the active user list.
/// </summary>
/// <returns>True if the client was added to the active user list. False if the client is already connected.</returns>
internal bool TryAddClientToActiveUserList()
{
return _activeClients.TryAdd(Client.Id, Client);
}

/// <summary>
/// Throws an exception if the response context is not initialized.
/// </summary>
/// <param name="responseContext">The response context to check.</param>
/// <exception cref="InvalidOperationException"></exception>
private static void ThrowForResponseContextNotInitialized([NotNull] WebContext? responseContext)
{
if (responseContext is null)
{
throw new InvalidOperationException("The response context is not initialized.");
}
}

/// <summary>
/// Disposes the upgrade handler.
/// </summary>
private void Cleanup()
{
_upgradeHandler = null;
_responseContext = null;
Request = null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// This file is part of the Jung SimpleWebSocket project.
// The project is licensed under the MIT license.

using Microsoft.Extensions.Logging;

namespace Jung.SimpleWebSocket.Models.EventArguments;

/// <summary>
/// Represents the arguments of the event when a upgrade request is received from a client.
/// </summary>
/// <param name="Client">The client that is sending the upgrade request.</param>
/// <param name="WebContext">The context of the request.</param>
/// <param name="Logger">The current Logger.</param>
public record ClientUpgradeRequestReceivedArgs(WebSocketServerClient Client, WebContext WebContext, ILogger? Logger)
{
private WebContext? _responseContext;

/// <summary>
/// Gets or sets a value indicating whether the upgrade request should be handled.
/// </summary>
public bool Handle { get; set; } = true;

/// <summary>
/// The context that is being use to response to the client.
/// </summary>
public WebContext ResponseContext { get => _responseContext ??= new WebContext(); }
}
10 changes: 10 additions & 0 deletions src/Jung.SimpleWebSocket/Models/EventArguments/ItemExpiredArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// This file is part of the Jung SimpleWebSocket project.
// The project is licensed under the MIT license.

namespace Jung.SimpleWebSocket.Models.EventArguments;

/// <summary>
/// Represents the arguments of the event when an item is expired.
/// </summary>
/// <param name="Item">The item that is expired.</param>
public record ItemExpiredArgs<TValue>(TValue Item);
23 changes: 23 additions & 0 deletions src/Jung.SimpleWebSocket/Models/SimpleWebSocketServerOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// This file is part of the Jung SimpleWebSocket project.
// The project is licensed under the MIT license.

using System.Net;

namespace Jung.SimpleWebSocket.Models
{
/// <summary>
/// Represents the options for the SimpleWebSocketServer.
/// </summary>
public class SimpleWebSocketServerOptions
{
/// <summary>
/// Gets or sets the local IP address of the server.
/// </summary>
public IPAddress LocalIpAddress { get; set; } = IPAddress.Any;

/// <summary>
/// Gets or sets the port of the server.
/// </summary>
public int Port { get; set; }
}
}
Loading
Loading