mirror of
https://github.com/AlexMacocian/Net.Sdk.Web.Extensions.git
synced 2026-07-15 13:39:29 +00:00
Zero memory allocation websocket routes
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputType>Library</OutputType>
|
||||
<IsPackable>true</IsPackable>
|
||||
<Version>0.8.4</Version>
|
||||
<Version>0.8.5</Version>
|
||||
<Authors>Alexandru Macocian</Authors>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
||||
@@ -25,6 +25,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CorrelationVector" Version="1.0.42" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageReference Include="SystemExtensions.NetCore" Version="1.6.9" />
|
||||
|
||||
@@ -3,11 +3,20 @@ using Net.Sdk.Web.Middleware;
|
||||
using Net.Sdk.Web.Extensions.Http;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions;
|
||||
using Net.Sdk.Web.Websockets;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Net.Sdk.Web;
|
||||
|
||||
public static class WebApplicationBuilderExtensions
|
||||
{
|
||||
public static WebApplicationBuilder WithWebSocketRoute<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TWebSocketRoute>(this WebApplicationBuilder builder)
|
||||
where TWebSocketRoute : WebSocketRouteBase
|
||||
{
|
||||
builder.Services.AddScoped<TWebSocketRoute>();
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static WebApplicationBuilder ConfigureExtended<TOptions>(this WebApplicationBuilder builder)
|
||||
where TOptions : class, new()
|
||||
{
|
||||
|
||||
@@ -6,11 +6,14 @@ using System.Core.Extensions;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Extensions;
|
||||
using System.Net.WebSockets;
|
||||
using Microsoft.IO;
|
||||
|
||||
namespace Net.Sdk.Web.Websockets.Extensions;
|
||||
|
||||
public static class WebApplicationExtensions
|
||||
{
|
||||
private static readonly RecyclableMemoryStreamManager StreamManager = new();
|
||||
|
||||
public static WebApplication UseCorrelationVector(this WebApplication webApplication)
|
||||
{
|
||||
webApplication.ThrowIfNull()
|
||||
@@ -27,16 +30,16 @@ public static class WebApplicationExtensions
|
||||
return webApplication;
|
||||
}
|
||||
|
||||
public static WebApplication MapWebSocket<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TWebSocketRoute>(this WebApplication app, string route)
|
||||
public static IEndpointConventionBuilder UseWebSocketRoute<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TWebSocketRoute>(this WebApplication app, string route)
|
||||
where TWebSocketRoute : WebSocketRouteBase
|
||||
{
|
||||
app.ThrowIfNull();
|
||||
app.Map(route, async context =>
|
||||
return app.Map(route, async context =>
|
||||
{
|
||||
if (context.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
var logger = app.Services.GetRequiredService<ILogger<WebSocketRouteBase>>();
|
||||
var route = GetRoute<TWebSocketRoute>(context);
|
||||
var logger = app.Services.GetRequiredService<ILogger<TWebSocketRoute>>();
|
||||
var route = app.Services.GetRequiredService<TWebSocketRoute>();
|
||||
var routeFilters = GetRouteFilters<TWebSocketRoute>(context).ToList();
|
||||
|
||||
var actionContext = new ActionContext(
|
||||
@@ -79,8 +82,6 @@ public static class WebApplicationExtensions
|
||||
context.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
}
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static async Task ProcessWebSocketRequest(WebSocketRouteBase route, HttpContext httpContext)
|
||||
@@ -134,16 +135,19 @@ public static class WebApplicationExtensions
|
||||
|
||||
private static async Task HandleWebSocket(WebSocket webSocket, WebSocketRouteBase route, CancellationToken cancellationToken)
|
||||
{
|
||||
var buffer = new byte[1024];
|
||||
using var memoryStream = new MemoryStream(1024);
|
||||
if (route.Context is null)
|
||||
{
|
||||
throw new InvalidOperationException("Route HttpContext is null");
|
||||
}
|
||||
|
||||
using var ms = StreamManager.GetStream(route.Context.Session.Id);
|
||||
ValueWebSocketReceiveResult result;
|
||||
while(webSocket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
do
|
||||
{
|
||||
var memory = new Memory<byte>(buffer);
|
||||
result = await webSocket.ReceiveAsync(memory, cancellationToken);
|
||||
await memoryStream.WriteAsync(buffer, 0, result.Count, cancellationToken);
|
||||
var buffer = ms.GetMemory(8 * 1024);
|
||||
result = await webSocket.ReceiveAsync(buffer, cancellationToken);
|
||||
} while (!result.EndOfMessage);
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
@@ -151,30 +155,10 @@ public static class WebApplicationExtensions
|
||||
return;
|
||||
}
|
||||
|
||||
await route.ExecuteAsync(result.MessageType, memoryStream.ToArray(), cancellationToken);
|
||||
memoryStream.SetLength(0);
|
||||
await route.ExecuteAsync(result.MessageType, ms.GetReadOnlySequence(), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private static WebSocketRouteBase GetRoute<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TWebSocketRoute>(HttpContext context)
|
||||
where TWebSocketRoute : WebSocketRouteBase
|
||||
{
|
||||
var constructors = typeof(TWebSocketRoute).GetConstructors();
|
||||
foreach(var constructor in constructors)
|
||||
{
|
||||
var dependencies = constructor.GetParameters().Select(param => context.RequestServices.GetService(param.ParameterType));
|
||||
if (dependencies.Any(d => d is null))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var route = constructor.Invoke(dependencies.ToArray());
|
||||
return route.Cast<WebSocketRouteBase>();
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Unable to resolve {typeof(TWebSocketRoute).Name}");
|
||||
}
|
||||
|
||||
private static IEnumerable<IFilterMetadata> GetRouteFilters<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TWebSocketRoute>(HttpContext context)
|
||||
where TWebSocketRoute : WebSocketRouteBase
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Net.Sdk.Web.Attributes;
|
||||
using System.Buffers;
|
||||
using System.Core.Extensions;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -26,7 +27,12 @@ public class JsonWebSocketMessageConverter<T> : WebSocketMessageConverter<T>
|
||||
throw new InvalidOperationException($"Unable to deserialize message. Message is not text");
|
||||
}
|
||||
|
||||
var stringData = Encoding.UTF8.GetString(request.Payload!);
|
||||
if (request.Payload is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to deserialize message. Payload is null");
|
||||
}
|
||||
|
||||
var stringData = Encoding.UTF8.GetString(request.Payload.Value);
|
||||
var objData = JsonSerializer.Deserialize<T>(stringData, this.jsonSerializerOptions);
|
||||
return objData ?? throw new InvalidOperationException($"Unable to deserialize message to {typeof(T).Name}");
|
||||
}
|
||||
@@ -35,6 +41,7 @@ public class JsonWebSocketMessageConverter<T> : WebSocketMessageConverter<T>
|
||||
{
|
||||
var serialized = JsonSerializer.Serialize(message, this.jsonSerializerOptions);
|
||||
var data = Encoding.UTF8.GetBytes(serialized);
|
||||
return new WebSocketConverterResponse { EndOfMessage = true, Type = System.Net.WebSockets.WebSocketMessageType.Text, Payload = data };
|
||||
var readOnlySequence = new ReadOnlySequence<byte>(data);
|
||||
return new WebSocketConverterResponse { EndOfMessage = true, Type = System.Net.WebSockets.WebSocketMessageType.Text, Payload = readOnlySequence };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text;
|
||||
using System.Buffers;
|
||||
using System.Text;
|
||||
|
||||
namespace Net.Sdk.Web.Websockets.Converters;
|
||||
|
||||
@@ -11,7 +12,12 @@ public sealed class TextWebSocketMessageConverter : WebSocketMessageConverter<Te
|
||||
throw new InvalidOperationException($"Cannot parse message of type {request.Type}");
|
||||
}
|
||||
|
||||
var message = Encoding.UTF8.GetString(request.Payload!);
|
||||
if (request.Payload is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to serialize message. Payload is null");
|
||||
}
|
||||
|
||||
var message = Encoding.UTF8.GetString(request.Payload.Value);
|
||||
return new TextContent { Text = message };
|
||||
}
|
||||
|
||||
@@ -21,7 +27,7 @@ public sealed class TextWebSocketMessageConverter : WebSocketMessageConverter<Te
|
||||
{
|
||||
Type = System.Net.WebSockets.WebSocketMessageType.Text,
|
||||
EndOfMessage = true,
|
||||
Payload = Encoding.UTF8.GetBytes(message.Text ?? string.Empty)
|
||||
Payload = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(message.Text ?? string.Empty))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Buffers;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace Net.Sdk.Web.Websockets;
|
||||
|
||||
public sealed class WebSocketConverterRequest
|
||||
{
|
||||
public WebSocketMessageType Type { get; set; }
|
||||
public byte[]? Payload { get; set; }
|
||||
public ReadOnlySequence<byte>? Payload { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Buffers;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace Net.Sdk.Web.Websockets;
|
||||
|
||||
public sealed class WebSocketConverterResponse
|
||||
{
|
||||
public WebSocketMessageType Type { get; set; }
|
||||
public byte[]? Payload { get; set; }
|
||||
public ReadOnlySequence<byte>? Payload { get; set; }
|
||||
public bool EndOfMessage { get; set; }
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Net.Sdk.Web.Attributes;
|
||||
using System.Buffers;
|
||||
using System.Extensions;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
@@ -20,7 +21,7 @@ public abstract class WebSocketRouteBase
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public abstract Task ExecuteAsync(WebSocketMessageType type, byte[] data, CancellationToken cancellationToken);
|
||||
public abstract Task ExecuteAsync(WebSocketMessageType type, ReadOnlySequence<byte> data, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public abstract class WebSocketRouteBase<TReceiveType> : WebSocketRouteBase
|
||||
@@ -38,8 +39,13 @@ public abstract class WebSocketRouteBase<TReceiveType> : WebSocketRouteBase
|
||||
});
|
||||
}
|
||||
|
||||
public sealed override Task ExecuteAsync(WebSocketMessageType type, byte[] data, CancellationToken cancellationToken)
|
||||
public sealed override Task ExecuteAsync(WebSocketMessageType type, ReadOnlySequence<byte> data, CancellationToken cancellationToken)
|
||||
{
|
||||
if (this.Context is null)
|
||||
{
|
||||
throw new InvalidOperationException("Route HttpContext is null");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var objData = this.converter.Value.ConvertToObject(new WebSocketConverterRequest { Type = type, Payload = data }).Cast<TReceiveType>();
|
||||
@@ -47,7 +53,7 @@ public abstract class WebSocketRouteBase<TReceiveType> : WebSocketRouteBase
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var scoppedLogger = this.Context!.RequestServices.GetRequiredService<ILogger<WebSocketRouteBase<TReceiveType>>>().CreateScopedLogger(nameof(this.ExecuteAsync), string.Empty);
|
||||
var scoppedLogger = this.Context.RequestServices.GetRequiredService<ILogger<WebSocketRouteBase<TReceiveType>>>().CreateScopedLogger(nameof(this.ExecuteAsync), string.Empty);
|
||||
scoppedLogger.LogError(ex, "Failed to process data");
|
||||
throw;
|
||||
}
|
||||
@@ -99,16 +105,37 @@ public abstract class WebSocketRouteBase<TReceiveType, TSendType> : WebSocketRou
|
||||
});
|
||||
}
|
||||
|
||||
public Task SendMessage(TSendType sendType, CancellationToken cancellationToken)
|
||||
public async Task SendMessage(TSendType sendType, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = sendType ?? throw new ArgumentNullException(nameof(sendType));
|
||||
if (this.WebSocket is null)
|
||||
{
|
||||
throw new InvalidOperationException("Route WebSocket is null");
|
||||
}
|
||||
|
||||
if (this.Context is null)
|
||||
{
|
||||
throw new InvalidOperationException("Route HttpContext is null");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var response = this.converter.Value.ConvertFromObject(sendType!);
|
||||
return this.WebSocket!.SendAsync(response.Payload!, response.Type, response.EndOfMessage, cancellationToken);
|
||||
var response = this.converter.Value.ConvertFromObject(sendType);
|
||||
if (response.Payload is null)
|
||||
{
|
||||
throw new InvalidOperationException("Send payload is null");
|
||||
}
|
||||
|
||||
SequencePosition pos = response.Payload.Value.Start;
|
||||
while (response.Payload.Value.TryGet(ref pos, out var memory))
|
||||
{
|
||||
var endOfMessage = pos.Equals(response.Payload.Value.End);
|
||||
await this.WebSocket.SendAsync(memory, response.Type, endOfMessage && response.EndOfMessage, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var scoppedLogger = this.Context!.RequestServices.GetRequiredService<ILogger<WebSocketRouteBase<TReceiveType, TSendType>>>().CreateScopedLogger(nameof(this.SendMessage), string.Empty);
|
||||
var scoppedLogger = this.Context.RequestServices.GetRequiredService<ILogger<WebSocketRouteBase<TReceiveType, TSendType>>>().CreateScopedLogger(nameof(this.SendMessage), string.Empty);
|
||||
scoppedLogger.LogError(ex, "Failed to send data");
|
||||
throw;
|
||||
}
|
||||
|
||||
@@ -76,8 +76,9 @@ Net.Sdk.Web.Extensions provides a streamlined implementation of WebSockets to be
|
||||
### Integration into `WebApplication`
|
||||
```C#
|
||||
var builder = WebApplication.CreateSlimBuilder(args);
|
||||
builder.WithWebSocketRoute<TWebSocketRoute>();
|
||||
var app = builder.Build();
|
||||
app.MapWebSocket<CustomRoute>("custom-route");
|
||||
app.UseWebSocketRoute<TWebSocketRoute>("custom-route");
|
||||
```
|
||||
|
||||
### Message mapping
|
||||
|
||||
Reference in New Issue
Block a user