mirror of
https://github.com/yawaflua/WebSockets.git
synced 2025-12-10 04:19:33 +02:00
Create project files
This commit is contained in:
40
yawaflua.WebSockets/Attributes/WebSocketAttribute.cs
Normal file
40
yawaflua.WebSockets/Attributes/WebSocketAttribute.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace yawaflua.WebSockets.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute that marks WebSocket endpoint handlers.
|
||||
/// Applied to classes or methods to define WebSocket route templates.
|
||||
///
|
||||
/// Usage examples:
|
||||
/// [WebSocket("/chat")] - basic route
|
||||
/// [WebSocket("/notifications/{userId}")] - route with parameter
|
||||
/// [WebSocket("/game/{roomId}/players")] - complex route
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Inherits from ASP.NET Core's RouteAttribute to leverage standard routing syntax.
|
||||
/// When applied to a class, defines the base path for all WebSocket endpoints in controller.
|
||||
/// When applied to methods, defines specific sub-routes (requires class-level base path).
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
||||
public class WebSocketAttribute : RouteAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Original route template specified in attribute
|
||||
/// </summary>
|
||||
public string Template { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates WebSocket route definition
|
||||
/// </summary>
|
||||
/// <param name="path">Route template using ASP.NET Core syntax:
|
||||
/// - Static: "/status"
|
||||
/// - Parameters: "/user/{id}"
|
||||
/// - Constraints: "/file/{name:alpha}"
|
||||
/// - Optional: "/feed/{category?}"</param>
|
||||
public WebSocketAttribute([RouteTemplate]string path) : base(path)
|
||||
{
|
||||
Template = path;
|
||||
}
|
||||
}
|
||||
25
yawaflua.WebSockets/Core/Middleware/WebSocketMiddleware.cs
Normal file
25
yawaflua.WebSockets/Core/Middleware/WebSocketMiddleware.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace yawaflua.WebSockets.Core.Middleware;
|
||||
|
||||
public class WebSocketMiddleware : IMiddleware
|
||||
{
|
||||
private readonly WebSocketRouter _router;
|
||||
|
||||
public WebSocketMiddleware(WebSocketRouter router)
|
||||
{
|
||||
_router = router;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
|
||||
{
|
||||
if (context.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
await _router.HandleRequest(context);
|
||||
}
|
||||
else
|
||||
{
|
||||
await next(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
43
yawaflua.WebSockets/Core/WebSocket.cs
Normal file
43
yawaflua.WebSockets/Core/WebSocket.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using yawaflua.WebSockets.Models.Interfaces;
|
||||
|
||||
namespace yawaflua.WebSockets.Core;
|
||||
|
||||
public class WebSocket : IDisposable
|
||||
{
|
||||
private readonly System.Net.WebSockets.WebSocket _webSocket;
|
||||
private readonly WebSocketReceiveResult? _webSocketReceiveResult;
|
||||
private readonly string? _message;
|
||||
|
||||
public WebSocketState State => _webSocket.State;
|
||||
public WebSocketCloseStatus? CloseStatus => _webSocket.CloseStatus;
|
||||
public string? SubProtocol => _webSocket.SubProtocol;
|
||||
public string? CloseStatusDescription => _webSocket.CloseStatusDescription;
|
||||
public string? Message => _message;
|
||||
public WebSocketMessageType? MessageType => _webSocketReceiveResult?.MessageType;
|
||||
public IWebSocketClient Client;
|
||||
internal WebSocket(System.Net.WebSockets.WebSocket webSocket, WebSocketReceiveResult? webSocketReceiveResult, string? message, IWebSocketClient client)
|
||||
{
|
||||
_webSocket = webSocket;
|
||||
_webSocketReceiveResult = webSocketReceiveResult;
|
||||
_message = message;
|
||||
Client = client;
|
||||
}
|
||||
|
||||
public async Task SendAsync(string m, WebSocketMessageType messageType = WebSocketMessageType.Text, CancellationToken cts = default)
|
||||
=> await _webSocket.SendAsync(
|
||||
Encoding.UTF8.GetBytes(m),
|
||||
messageType,
|
||||
true,
|
||||
cts);
|
||||
|
||||
public async Task CloseAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.NormalClosure, string? reason = null, CancellationToken cts = default)
|
||||
=> await _webSocket.CloseAsync(closeStatus, reason, cts);
|
||||
|
||||
|
||||
|
||||
public void Abort() => _webSocket.Abort();
|
||||
public void Dispose() => _webSocket.Dispose();
|
||||
|
||||
}
|
||||
34
yawaflua.WebSockets/Core/WebSocketManager.cs
Normal file
34
yawaflua.WebSockets/Core/WebSocketManager.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using yawaflua.WebSockets.Models.Interfaces;
|
||||
|
||||
namespace yawaflua.WebSockets.Core;
|
||||
|
||||
internal class WebSocketManager : IWebSocketManager
|
||||
{
|
||||
public async Task Broadcast(Func<IWebSocketClient, bool> selector, string message,
|
||||
WebSocketMessageType messageType = WebSocketMessageType.Text, CancellationToken cts = default)
|
||||
{
|
||||
foreach (var client in WebSocketRouter.Clients.Where(selector))
|
||||
{
|
||||
await client.webSocket.SendAsync(Encoding.UTF8.GetBytes(message),
|
||||
messageType,
|
||||
true,
|
||||
cts);
|
||||
}
|
||||
}
|
||||
|
||||
public List<IWebSocketClient> GetAllClients()
|
||||
{
|
||||
return WebSocketRouter.Clients;
|
||||
}
|
||||
|
||||
public async Task SendToUser(Guid id, string message, WebSocketMessageType messageType = WebSocketMessageType.Text, CancellationToken cts = default)
|
||||
{
|
||||
await WebSocketRouter.Clients.First(k => k.Id == id).webSocket.SendAsync(Encoding.UTF8.GetBytes(message),
|
||||
messageType,
|
||||
true,
|
||||
cts);
|
||||
}
|
||||
}
|
||||
182
yawaflua.WebSockets/Core/WebSocketRouter.cs
Normal file
182
yawaflua.WebSockets/Core/WebSocketRouter.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using yawaflua.WebSockets.Attributes;
|
||||
using yawaflua.WebSockets.Models;
|
||||
using yawaflua.WebSockets.Models.Abstracts;
|
||||
using yawaflua.WebSockets.Models.Interfaces;
|
||||
|
||||
namespace yawaflua.WebSockets.Core;
|
||||
|
||||
public class WebSocketRouter
|
||||
{
|
||||
internal static readonly Dictionary<string, Func<WebSocket, HttpContext, Task>> Routes = new();
|
||||
internal static readonly List<IWebSocketClient> Clients = new();
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<WebSocketRouter> _logger;
|
||||
|
||||
public WebSocketRouter(IServiceProvider serviceProvider, ILogger<WebSocketRouter> logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
this._logger = logger;
|
||||
DiscoverHandlers();
|
||||
Task.Run(() =>
|
||||
{
|
||||
Clients.ForEach(async l =>
|
||||
{
|
||||
await l.webSocket.SendAsync(ArraySegment<byte>.Empty, WebSocketMessageType.Binary,
|
||||
WebSocketMessageFlags.EndOfMessage, default);
|
||||
await Task.Delay(TimeSpan.FromSeconds(10));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
internal void DiscoverHandlers()
|
||||
{
|
||||
try
|
||||
{
|
||||
var handlerTypes = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(a => a.GetTypes())
|
||||
.Where(t =>
|
||||
t.IsSubclassOf(typeof(IWebSocketController))
|
||||
|| t.IsSubclassOf(typeof(WebSocketController))
|
||||
|| t.IsInstanceOfType(typeof(WebSocketController))
|
||||
|| t.IsInstanceOfType(typeof(IWebSocketController))
|
||||
);
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
foreach (var type in handlerTypes.Where(k => k.GetMethods().Length > 0))
|
||||
{
|
||||
var parentAttributeTemplate =
|
||||
new PathString((type.GetCustomAttribute(typeof(WebSocketAttribute)) as WebSocketAttribute)?.Template ?? "/");
|
||||
var methods = type.GetMethods()
|
||||
.Where(m => m.GetCustomAttributes(typeof(WebSocketAttribute), false).Length > 0).ToList();
|
||||
|
||||
|
||||
if (methods.Count == 0 && type.GetMethods().Any(k => k.Name.StartsWith("OnMessage")))
|
||||
{
|
||||
var func = type.GetMethods()
|
||||
.First(k => k.Name.StartsWith("OnMessage"));
|
||||
|
||||
var parameters = func.GetParameters();
|
||||
if (parameters.Length != 2 ||
|
||||
parameters[0].ParameterType != typeof(WebSocket) ||
|
||||
parameters[1].ParameterType != typeof(HttpContext) ||
|
||||
func.ReturnType != typeof(Task))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid handler signature in {type.Name}.{func.Name}");
|
||||
}
|
||||
|
||||
if (func.IsStatic)
|
||||
{
|
||||
var delegateFunc = (Func<WebSocket, HttpContext, Task>)Delegate.CreateDelegate(
|
||||
typeof(Func<WebSocket, HttpContext, Task>),
|
||||
func
|
||||
);
|
||||
Routes.Add(parentAttributeTemplate, delegateFunc);
|
||||
}
|
||||
else
|
||||
{
|
||||
Routes.Add(parentAttributeTemplate, async (ws, context) =>
|
||||
{
|
||||
var instance = context.RequestServices.GetRequiredService(type);
|
||||
await (Task)func.Invoke(instance, new object[] { ws, context })!;
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var method in methods)
|
||||
{
|
||||
var attribute =
|
||||
(WebSocketAttribute)method.GetCustomAttributes(typeof(WebSocketAttribute), false).First();
|
||||
if (method.IsStatic)
|
||||
{
|
||||
var delegateFunc = (Func<WebSocket, HttpContext, Task>)Delegate.CreateDelegate(
|
||||
typeof(Func<WebSocket, HttpContext, Task>),
|
||||
method
|
||||
);
|
||||
Routes.Add(parentAttributeTemplate+attribute.Template, delegateFunc);
|
||||
}
|
||||
else
|
||||
{
|
||||
Routes.Add(parentAttributeTemplate+attribute.Template, async (ws, context) =>
|
||||
{
|
||||
var instance = context.RequestServices.GetRequiredService(type);
|
||||
await (Task)method.Invoke(instance, new object[] { ws, context })!;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
var constructors = type.GetConstructors();
|
||||
if (constructors.Length != 0)
|
||||
{
|
||||
var parameters = constructors[0].GetParameters()
|
||||
.Select(param => scope.ServiceProvider.GetRequiredService(param.ParameterType))
|
||||
.ToArray();
|
||||
|
||||
constructors[0].Invoke(parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogCritical(message:"Error when parsing attributes from assemblies: ", exception:ex);
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task HandleRequest(HttpContext context, CancellationToken cts = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!context.WebSockets.IsWebSocketRequest)
|
||||
return;
|
||||
|
||||
var path = context.Request.Path.Value;
|
||||
|
||||
if (Routes.TryGetValue(path, out var handler))
|
||||
{
|
||||
var webSocket = await context.WebSockets.AcceptWebSocketAsync();
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = new WebSocketClient(context, webSocket, path);
|
||||
Clients.Add(client);
|
||||
var buffer = new byte[1024 * 4];
|
||||
while (webSocket.State == WebSocketState.Open)
|
||||
{
|
||||
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cts);
|
||||
if (result.MessageType != WebSocketMessageType.Close)
|
||||
await handler(
|
||||
new WebSocket(
|
||||
webSocket,
|
||||
result,
|
||||
Encoding.UTF8.GetString(buffer, 0, result.Count),
|
||||
client),
|
||||
context);
|
||||
else
|
||||
Clients.Remove(client);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(message:"Error with handling request: ",exception: ex);
|
||||
}
|
||||
|
||||
}, cts);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.StatusCode = 404;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Error when handle request {context.Connection.Id}: ");
|
||||
}
|
||||
}
|
||||
}
|
||||
16
yawaflua.WebSockets/Models/Abstracts/WebSocketController.cs
Normal file
16
yawaflua.WebSockets/Models/Abstracts/WebSocketController.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using yawaflua.WebSockets.Core;
|
||||
using yawaflua.WebSockets.Models.Interfaces;
|
||||
using WebSocketManager = yawaflua.WebSockets.Core.WebSocketManager;
|
||||
|
||||
namespace yawaflua.WebSockets.Models.Abstracts;
|
||||
|
||||
public abstract class WebSocketController : IWebSocketController
|
||||
{
|
||||
public IWebSocketManager WebSocketManager => new WebSocketManager();
|
||||
|
||||
public virtual Task OnMessageAsync(WebSocket webSocket, HttpContext httpContext)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
15
yawaflua.WebSockets/Models/Interfaces/IWebSocketClient.cs
Normal file
15
yawaflua.WebSockets/Models/Interfaces/IWebSocketClient.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Net.WebSockets;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace yawaflua.WebSockets.Models.Interfaces;
|
||||
|
||||
public interface IWebSocketClient
|
||||
{
|
||||
public Guid Id { get; }
|
||||
public string Path { get; }
|
||||
public ConnectionInfo? ConnectionInfo { get; }
|
||||
public IDictionary<object, object>? Items { get; set; }
|
||||
public HttpRequest? HttpRequest { get; }
|
||||
internal WebSocket webSocket { get; }
|
||||
public Task Abort();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using yawaflua.WebSockets.Core;
|
||||
|
||||
namespace yawaflua.WebSockets.Models.Interfaces;
|
||||
|
||||
internal interface IWebSocketController
|
||||
{
|
||||
Task OnMessageAsync(WebSocket webSocket, HttpContext httpContext);
|
||||
}
|
||||
11
yawaflua.WebSockets/Models/Interfaces/IWebSocketManager.cs
Normal file
11
yawaflua.WebSockets/Models/Interfaces/IWebSocketManager.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace yawaflua.WebSockets.Models.Interfaces;
|
||||
|
||||
public interface IWebSocketManager
|
||||
{
|
||||
public Task Broadcast(Func<IWebSocketClient, bool> selector,string message, WebSocketMessageType messageType = WebSocketMessageType.Text, CancellationToken cts = default);
|
||||
public List<IWebSocketClient> GetAllClients();
|
||||
public Task SendToUser(Guid id, string message, WebSocketMessageType messageType, CancellationToken cts);
|
||||
}
|
||||
34
yawaflua.WebSockets/Models/WebSocketClient.cs
Normal file
34
yawaflua.WebSockets/Models/WebSocketClient.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.Net.WebSockets;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using yawaflua.WebSockets.Models.Interfaces;
|
||||
|
||||
namespace yawaflua.WebSockets.Models;
|
||||
|
||||
internal class WebSocketClient : IWebSocketClient
|
||||
{
|
||||
private HttpContext HttpContext { get; set; }
|
||||
public Guid Id { get; } = Guid.NewGuid();
|
||||
public string Path { get; }
|
||||
public ConnectionInfo ConnectionInfo { get => HttpContext.Connection; }
|
||||
public IDictionary<object, object> Items
|
||||
{
|
||||
get => HttpContext.Items;
|
||||
set => HttpContext.Items = (value);
|
||||
}
|
||||
|
||||
public HttpRequest HttpRequest { get => HttpContext.Request; }
|
||||
public WebSocket webSocket { get; }
|
||||
|
||||
internal WebSocketClient(HttpContext httpContext, WebSocket webSocket, string path)
|
||||
{
|
||||
this.webSocket = webSocket;
|
||||
Path = path;
|
||||
this.HttpContext = httpContext;
|
||||
}
|
||||
|
||||
public Task Abort()
|
||||
{
|
||||
webSocket.Abort();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
1
yawaflua.WebSockets/Properties/AssemblyInfo.cs
Normal file
1
yawaflua.WebSockets/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1 @@
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("yawaflua.WebSockets.Tests")]
|
||||
25
yawaflua.WebSockets/ServiceBindings.cs
Normal file
25
yawaflua.WebSockets/ServiceBindings.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using yawaflua.WebSockets.Core;
|
||||
using yawaflua.WebSockets.Core.Middleware;
|
||||
using yawaflua.WebSockets.Models.Interfaces;
|
||||
|
||||
namespace yawaflua.WebSockets;
|
||||
|
||||
public static class ServiceBindings
|
||||
{
|
||||
public static IServiceCollection SettingUpWebSockets(this IServiceCollection isc)
|
||||
{
|
||||
isc.AddSingleton<WebSocketRouter>();
|
||||
isc.AddScoped<IWebSocketManager, WebSocketManager>();
|
||||
isc.AddSingleton<WebSocketMiddleware>();
|
||||
return isc;
|
||||
}
|
||||
|
||||
public static IApplicationBuilder ConnectWebSockets(this IApplicationBuilder iab)
|
||||
{
|
||||
iab.UseWebSockets();
|
||||
iab.UseMiddleware<WebSocketMiddleware>();
|
||||
return iab;
|
||||
}
|
||||
}
|
||||
30
yawaflua.WebSockets/yawaflua.WebSockets.csproj
Normal file
30
yawaflua.WebSockets/yawaflua.WebSockets.csproj
Normal file
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>1.0.0</Version>
|
||||
<Title>yawaflua.WebSockets</Title>
|
||||
<Description>New AspNet controllers looks like websocket manager </Description>
|
||||
<Copyright>Dmitrii Shimanskii</Copyright>
|
||||
<PackageProjectUrl>https://github.com/yawaflua/WebSockets</PackageProjectUrl>
|
||||
<PackageLicenseUrl>https://github.com/yawaflua/WebSockets/LICENCE</PackageLicenseUrl>
|
||||
<RepositoryUrl>https://github.com/yawaflua/WebSockets</RepositoryUrl>
|
||||
<PackageTags>websocket</PackageTags>
|
||||
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations" Version="(2023.3.0,)"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="(2.1.7,)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="(6.0.0,)" />
|
||||
<PackageReference Include="System.Net.WebSockets" Version="(4.0.0,)"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="(2.2.0,)" PrivateAssets="all">
|
||||
<Private>False</Private>
|
||||
</PackageReference>
|
||||
<None Include="..\README.md" Pack="true" PackagePath="\"/>
|
||||
<None Include="..\LICENSE" Pack="true" PackagePath=""/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user