diff --git a/OverclockedApp/OverclockedApp.csproj b/OverclockedApp/OverclockedApp.csproj new file mode 100644 index 0000000..ae36db7 --- /dev/null +++ b/OverclockedApp/OverclockedApp.csproj @@ -0,0 +1,22 @@ + + + + Exe + net10.0 + enable + enable + true + true + true + true + + + + + + + + + + + diff --git a/OverclockedApp/Program.cs b/OverclockedApp/Program.cs new file mode 100644 index 0000000..65a90cf --- /dev/null +++ b/OverclockedApp/Program.cs @@ -0,0 +1,73 @@ +using System.Buffers; +using System.Text.Json; +using System.Text.Json.Serialization; +using Wired.IO.App; +using Wired.IO.Protocol.Response; + +namespace OverclockedApp; + +// dotnet publish -f net10.0 -c Release /p:PublishAot=true /p:OptimizationPreference=Speed + +[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization | JsonSourceGenerationMode.Metadata)] +[JsonSerializable(typeof(Program.JsonMessage))] +public partial class JsonContext : JsonSerializerContext { } + +public static class Program +{ + [ThreadStatic] private static Utf8JsonWriter? _tUtf8JsonWriter; + public static async Task Main(string[] args) + { + await WiredApp + .CreateOverclockedBuilder() // io_uring + .NoScopedEndpoints() + .UseRootEndpoints() + .Port(8080) + .MapGet("/json", context => + { + context.Respond() + .Status(ResponseStatus.Ok) + .Type("application/json"u8) + .Content(() => + { + var utf8JsonWriter = _tUtf8JsonWriter ??= new Utf8JsonWriter(context.Connection, new JsonWriterOptions { SkipValidation = true }); + utf8JsonWriter.Reset(context.Connection); + JsonSerializer.Serialize(utf8JsonWriter, new JsonMessage { Message = JsonBody }, SerializerContext.JsonMessage); + }, 27); + + }) + .Build() + .RunAsync(); + } + + private const string JsonBody = "Hello, World!"; + public static readonly JsonContext SerializerContext = JsonContext.Default; + public struct JsonMessage { public string Message { get; set; } } +} + + +/* +context.Connection.Write("HTTP/1.1 200 OK\r\n"u8 + + "Server: W\r\n"u8 + + "Content-Length: 27\r\n"u8 + + "Content-Type: application/json\r\n\r\n"u8 + + "{\"Message\":\"Hello, World!\"}"u8); +*/ + +/* +var headers = + "HTTP/1.1 200 OK\r\n"u8 + + "Server: W\r\n"u8 + + "Content-Length: 27\r\n"u8 + + "Content-Type: application/json\r\n"u8; + +context.Connection.Write(headers); +context.Connection.Write(DateHelper.HeaderBytes); + +_tUtf8JsonWriter ??= new Utf8JsonWriter(context.Connection, new JsonWriterOptions { SkipValidation = true }); +_tUtf8JsonWriter.Reset(context.Connection); + +// Creating(Allocating) a new JsonMessage every request +var message = new JsonMessage { Message = "Hello, World!" }; +// Serializing it every request +JsonSerializer.Serialize(_tUtf8JsonWriter, message, SerializerContext.JsonMessage); +*/ \ No newline at end of file diff --git a/Wired.IO.Playground/Program.cs b/Wired.IO.Playground/Program.cs index 3517c9d..916c443 100644 --- a/Wired.IO.Playground/Program.cs +++ b/Wired.IO.Playground/Program.cs @@ -1,26 +1,70 @@ -using System.Net; using System.Text.Json.Serialization; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Wired.IO.App; -using Wired.IO.Http11Express.Response.Content; +using Wired.IO.Handlers.Http11Express.Response.Content; using Wired.IO.Protocol.Response; +// dotnet publish -f net10.0 -c Release /p:PublishAot=true /p:OptimizationPreference=Speed + var services = new ServiceCollection(); +services.AddScoped(); + var builder = WiredApp - .CreateExpressBuilder() + //.CreateOverclockedBuilder() + .CreateRocketBuilder() + //.CreateExpressBuilder() + .NoScopedEndpoints() .Port(8080); builder.EmbedServices(services); - + builder .MapGroup("/") - .MapGet("/my-endpoint", context => + .MapGet("/route", context => { JsonContext SerializerContext = JsonContext.Default; - var ip = ((IPEndPoint)context.Inner.RemoteEndPoint!).Address; - Console.WriteLine($"Client: {ip.ToString()}"); + context + .Respond() + .Status(ResponseStatus.Ok) + .Type("application/json"u8) + .Content(new ExpressJsonAotContent(new JsonMessage + { + Message = "Hello World!" + }, SerializerContext.JsonMessage)); + }); + +builder + .MapGroup("/api") + .UseMiddleware(async (context, next) => + { + // logger or any dependencies can be resolved using scope + var logger = context.Services.GetRequiredService>(); + + try + { + Console.WriteLine("Executing Middleware"); + // Execute next in line, could be another middleware or the endpoint + await next(context); + } + + catch (Exception e) + { + logger.LogError(e.Message); + + context.Respond() + .Status(ResponseStatus.InternalServerError) + .Type("application/json"u8) + .Content(new ExpressJsonContent(new { Error = e.Message })); + } + }) + .MapGet("/my-endpoint", async context => + { + await context.Services.GetRequiredService().HandleAsync(); + + JsonContext SerializerContext = JsonContext.Default; context .Respond() @@ -43,3 +87,16 @@ public struct JsonMessage { public string Message { get; set; } } [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization | JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(JsonMessage))] public partial class JsonContext : JsonSerializerContext { } + +public class Service : IDisposable +{ + public Service() => Console.WriteLine("Created Service"); + + public async Task HandleAsync() + { + await Task.Delay(0); + Console.WriteLine("Handled Service"); + } + + public void Dispose() => Console.WriteLine("Disposed Service"); +} \ No newline at end of file diff --git a/Wired.IO.sln b/Wired.IO.sln index 7f4812a..1abe262 100755 --- a/Wired.IO.sln +++ b/Wired.IO.sln @@ -9,6 +9,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wired.IO.Playground", "Wire EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wired.IO.Tests", "Wired.IO.Tests\Wired.IO.Tests.csproj", "{21364EC0-8822-40F5-8EEB-339919ABD5C6}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{8BA4F484-5D28-4E2B-BE5A-BA62BD943FB7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OverclockedApp", "OverclockedApp\OverclockedApp.csproj", "{068FC1BE-C9B2-4C82-BACE-98547D68DDB5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +31,10 @@ Global {21364EC0-8822-40F5-8EEB-339919ABD5C6}.Debug|Any CPU.Build.0 = Debug|Any CPU {21364EC0-8822-40F5-8EEB-339919ABD5C6}.Release|Any CPU.ActiveCfg = Release|Any CPU {21364EC0-8822-40F5-8EEB-339919ABD5C6}.Release|Any CPU.Build.0 = Release|Any CPU + {068FC1BE-C9B2-4C82-BACE-98547D68DDB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {068FC1BE-C9B2-4C82-BACE-98547D68DDB5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {068FC1BE-C9B2-4C82-BACE-98547D68DDB5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {068FC1BE-C9B2-4C82-BACE-98547D68DDB5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -34,4 +42,7 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DC158062-AC46-467E-AFC4-2ED35B471D73} EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {068FC1BE-C9B2-4C82-BACE-98547D68DDB5} = {8BA4F484-5D28-4E2B-BE5A-BA62BD943FB7} + EndGlobalSection EndGlobal diff --git a/Wired.IO/App/App.Builder.cs b/Wired.IO/App/App.Builder.cs index 574d2a5..4a6387e 100644 --- a/Wired.IO/App/App.Builder.cs +++ b/Wired.IO/App/App.Builder.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Logging; using Wired.IO.Builder; using Wired.IO.Protocol; -using Wired.IO.Protocol.Handlers; using Wired.IO.Protocol.Request; using Wired.IO.Protocol.Response; @@ -125,11 +124,6 @@ public sealed partial class WiredApp /// internal bool UseRootOnlyEndpoints { get; set; } = false; - /// - /// Gets or sets the HTTP handler responsible for dispatching requests and handling routing. - /// - public IHttpHandler HttpHandler { get; set; } = null!; - #endregion } diff --git a/Wired.IO/App/App.ClientHandling.cs b/Wired.IO/App/App.ClientHandling.cs index ed7eb22..146f2ad 100644 --- a/Wired.IO/App/App.ClientHandling.cs +++ b/Wired.IO/App/App.ClientHandling.cs @@ -3,20 +3,21 @@ using System.Net.Sockets; using System.Security; using System.Security.Authentication; -using Wired.IO.MemoryBuffers; using Wired.IO.Protocol; using Wired.IO.Protocol.Request; using Wired.IO.Protocol.Response; +using Wired.IO.Transport.Socket; namespace Wired.IO.App; public sealed partial class WiredApp where TContext : IBaseContext { - private Func _pipeline = null!; + //private Func _pipeline = null!; - internal void SetPipeline(Func pipeline) => _pipeline = pipeline; + //internal void SetPipeline(Func pipeline) => _pipeline = pipeline; + /* /// /// Handles an incoming plain (non-TLS) TCP client connection. /// Wraps the client socket in a and delegates request handling to . @@ -26,7 +27,7 @@ public sealed partial class WiredApp private async Task HandlePlainClientAsync(Socket client, CancellationToken stoppingToken) { await using var networkStream = new PoolBufferedStream(new NetworkStream(client, ownsSocket: true), 65 * 1024); - await HttpHandler.HandleClientAsync( + await ((ISocketHttpHandler)HttpHandler).HandleClientAsync( client, networkStream, _pipeline, @@ -72,7 +73,7 @@ private async Task HandleTlsClientAsync(Socket client, CancellationToken stoppin // Handle the client connection securely // - await HttpHandler.HandleClientAsync( + await ((ISocketHttpHandler)HttpHandler).HandleClientAsync( client, sslStream, _pipeline, @@ -126,4 +127,5 @@ private static async Task SendTlsFailureMessageAsync(Socket client) client.Close(); } } + */ } \ No newline at end of file diff --git a/Wired.IO/App/App.Engine.cs b/Wired.IO/App/App.Engine.cs index c96ee19..575befc 100644 --- a/Wired.IO/App/App.Engine.cs +++ b/Wired.IO/App/App.Engine.cs @@ -10,6 +10,7 @@ namespace Wired.IO.App; public sealed partial class WiredApp where TContext : IBaseContext { + /* /// /// Creates an instance configured to accept and process plain (non-TLS) HTTP connections. /// @@ -126,4 +127,5 @@ private async Task HandleClientAsync(Socket client, Func : IDisposable /// Gets or sets the service collection used to configure application dependencies before building the provider. /// public IServiceCollection ServiceCollection { get; set; } = new ServiceCollection(); + + /// + /// Underlying Engine Ongoing Task + /// + public Task? TransportTask => _transportTask; + + internal ITransport Transport { get; set; } = null!; private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly TaskCompletionSource _startedTcs = new(); - private Task? _engineTask; private bool _disposed; - - /// - /// Encapsulates the server's execution logic and abstracts the engine behavior (e.g., plain or TLS). - /// - private sealed class Engine(Func action) - { - /// - /// Executes the engine logic using the provided cancellation token. - /// - /// Token used to signal cancellation of the server loop. - public async Task ExecuteAsync(CancellationToken stoppingToken) - { - await action(stoppingToken); - } - } + private Task? _transportTask; /// /// Asynchronously starts the application and begins listening for incoming requests. @@ -52,9 +45,14 @@ public async Task> StartAsync() { if (_disposed) throw new ObjectDisposedException(nameof(WiredApp)); - - var engine = TlsEnabled ? CreateTlsEnabledEngine() : CreatePlainEngine(); - _engineTask = engine.ExecuteAsync(_cancellationTokenSource.Token); + + Transport.IPAddress = IpAddress; + Transport.Port = Port; + Transport.Backlog = Backlog; + Transport.TlsEnabled = TlsEnabled; + Transport.Logger = Logger; + Transport.SslServerAuthenticationOptions = SslServerAuthenticationOptions; + _transportTask = Transport.ExecuteAsync(_cancellationTokenSource.Token); // Give the engine a moment to start listening await Task.Delay(10); @@ -73,10 +71,10 @@ public async Task RunAsync() if (_disposed) throw new ObjectDisposedException(nameof(WiredApp)); - if (_engineTask == null) + if (_transportTask == null) await StartAsync(); - await _engineTask!; + await _transportTask!; } /// @@ -88,9 +86,14 @@ public WiredApp Start() { if (_disposed) throw new ObjectDisposedException(nameof(WiredApp)); - - var engine = TlsEnabled ? CreateTlsEnabledEngine() : CreatePlainEngine(); - _engineTask = engine.ExecuteAsync(_cancellationTokenSource.Token); + + Transport.IPAddress = IpAddress; + Transport.Port = Port; + Transport.Backlog = Backlog; + Transport.TlsEnabled = TlsEnabled; + Transport.Logger = Logger; + Transport.SslServerAuthenticationOptions = SslServerAuthenticationOptions; + _transportTask = Transport.ExecuteAsync(_cancellationTokenSource.Token); // Brief delay to ensure listening starts Thread.Sleep(10); @@ -109,10 +112,10 @@ public void Run() if (_disposed) throw new ObjectDisposedException(nameof(WiredApp)); - if (_engineTask == null) + if (_transportTask == null) Start(); - _engineTask!.GetAwaiter().GetResult(); + _transportTask!.GetAwaiter().GetResult(); } /// @@ -125,11 +128,11 @@ public async Task StopAsync() await _cancellationTokenSource.CancelAsync(); - if (_engineTask != null) + if (_transportTask != null) { try { - await _engineTask; + await _transportTask; } catch (OperationCanceledException) { @@ -151,7 +154,9 @@ public void Dispose() _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); - _socket?.Dispose(); + + Transport.Dispose(); + //_socket?.Dispose(); if (Services is IDisposable disposableProvider) disposableProvider.Dispose(); diff --git a/Wired.IO/App/App.cs b/Wired.IO/App/App.cs index b62b997..21d15b0 100644 --- a/Wired.IO/App/App.cs +++ b/Wired.IO/App/App.cs @@ -1,12 +1,19 @@ using System.Net.Security; using Wired.IO.Builder; -using Wired.IO.Http11Express; -using Wired.IO.Http11Express.Context; -using Wired.IO.Http11Express.StaticHandlers; +using Wired.IO.Handlers.Http11Express; +using Wired.IO.Handlers.Http11Express.Context; +using Wired.IO.Handlers.Http11Express.StaticHandlers; +using Wired.IO.Handlers.Http11Overclocked; +using Wired.IO.Handlers.Http11Overclocked.Context; +using Wired.IO.Handlers.Http11Rocket; +using Wired.IO.Handlers.Http11Rocket.Context; using Wired.IO.Protocol; using Wired.IO.Protocol.Handlers; using Wired.IO.Protocol.Request; using Wired.IO.Protocol.Response; +using Wired.IO.Transport; +using Wired.IO.Transport.Rocket; +using Wired.IO.Transport.Socket; namespace Wired.IO.App; @@ -16,10 +23,26 @@ namespace Wired.IO.App; /// public sealed class WiredApp { + public static Builder CreateOverclockedBuilder() + { + var builder = new Builder(() => new WiredHttp11Overclocked(), + [SslApplicationProtocol.Http11], new RocketTransport()); + + return builder; + } + + public static Builder CreateRocketBuilder() + { + var builder = new Builder(() => new WiredHttp11Rocket(), + [SslApplicationProtocol.Http11], new RocketTransport()); + + return builder; + } + public static Builder CreateExpressBuilder() { - var builder = new Builder(() => - new WiredHttp11Express(), [SslApplicationProtocol.Http11]); + var builder = new Builder(() => new WiredHttp11Express(), + [SslApplicationProtocol.Http11], new SocketTransport()); return builder.MapFlowControl("NotFound", FlowControl.CreateEndpointNotFoundHandler()); } @@ -28,8 +51,8 @@ public static Builder CreateExpressBui public static Builder, TContext> CreateExpressBuilder() where TContext : Http11ExpressContext, new() { - var builder = new Builder, TContext>(() => - new WiredHttp11Express(), [SslApplicationProtocol.Http11]); + var builder = new Builder, TContext>(() => new WiredHttp11Express(), + [SslApplicationProtocol.Http11], new SocketTransport()); return builder.MapFlowControl("NotFound", FlowControl.CreateEndpointNotFoundHandler()); } @@ -37,34 +60,37 @@ public static Builder, TContext> CreateExpressBuild /// /// Creates a generic for a custom handler and context type. /// - /// The custom HTTP handler type implementing . + /// The custom HTTP handler type implementing . /// The request context type implementing . /// A factory delegate that produces an instance of . + /// /// A configured instance. - public static Builder CreateBuilder(Func handlerFactory) - where THandler : IHttpHandler + public static Builder CreateBuilder(Func handlerFactory, ITransport transport) + where THandler : IHttpHandler where TContext : IBaseContext { - return CreateBuilder(handlerFactory, [SslApplicationProtocol.Http11]); + return CreateBuilder(handlerFactory, [SslApplicationProtocol.Http11], transport); } /// /// Creates a generic for a custom handler and context type, /// using a custom list of supported ALPN protocols. /// - /// The custom HTTP handler type implementing . + /// The custom HTTP handler type implementing . /// The request context type implementing . /// A factory delegate that produces an instance of . /// /// A list of supported values for ALPN negotiation. /// + /// /// A configured instance. public static Builder CreateBuilder( Func handlerFactory, - List sslApplicationProtocols) - where THandler : IHttpHandler + List sslApplicationProtocols, + ITransport transport) + where THandler : IHttpHandler where TContext : IBaseContext { - return new Builder(handlerFactory, sslApplicationProtocols); + return new Builder(handlerFactory, sslApplicationProtocols, transport); } } \ No newline at end of file diff --git a/Wired.IO/Builder/Builder.GroupsMapping.cs b/Wired.IO/Builder/Builder.GroupsMapping.cs index 21a5c2e..1af12f0 100644 --- a/Wired.IO/Builder/Builder.GroupsMapping.cs +++ b/Wired.IO/Builder/Builder.GroupsMapping.cs @@ -14,7 +14,7 @@ namespace Wired.IO.Builder; public sealed partial class Builder where TContext : IBaseContext - where THandler : IHttpHandler + where THandler : IHttpHandler { // Public entry that creates/returns a top-level group public Group MapGroup(string groupRoute) diff --git a/Wired.IO/Builder/Builder.ManualPipeline.cs b/Wired.IO/Builder/Builder.ManualPipeline.cs index bf1b422..9cd7ee5 100644 --- a/Wired.IO/Builder/Builder.ManualPipeline.cs +++ b/Wired.IO/Builder/Builder.ManualPipeline.cs @@ -9,7 +9,7 @@ namespace Wired.IO.Builder; public sealed partial class Builder where TContext : IBaseContext - where THandler : IHttpHandler + where THandler : IHttpHandler { public Builder AddManualPipeline( string route, diff --git a/Wired.IO/Builder/Builder.RootEndpoints.cs b/Wired.IO/Builder/Builder.RootEndpoints.cs index 21dc5ab..59f7e21 100644 --- a/Wired.IO/Builder/Builder.RootEndpoints.cs +++ b/Wired.IO/Builder/Builder.RootEndpoints.cs @@ -9,7 +9,7 @@ namespace Wired.IO.Builder; public sealed partial class Builder where TContext : IBaseContext - where THandler : IHttpHandler + where THandler : IHttpHandler { // ======== FlowControl ========== diff --git a/Wired.IO/Builder/Builder.RootMiddleware.cs b/Wired.IO/Builder/Builder.RootMiddleware.cs index a2d9508..d4b43cf 100644 --- a/Wired.IO/Builder/Builder.RootMiddleware.cs +++ b/Wired.IO/Builder/Builder.RootMiddleware.cs @@ -8,7 +8,7 @@ namespace Wired.IO.Builder; public sealed partial class Builder where TContext : IBaseContext - where THandler : IHttpHandler + where THandler : IHttpHandler { /// /// Registers a middleware component that resolves dependencies per request scope diff --git a/Wired.IO/Builder/Builder.cs b/Wired.IO/Builder/Builder.cs index a00b576..286e7fd 100644 --- a/Wired.IO/Builder/Builder.cs +++ b/Wired.IO/Builder/Builder.cs @@ -2,12 +2,11 @@ using Microsoft.Extensions.Logging; using System.Net; using System.Net.Security; -using System.Reflection; using Wired.IO.App; -using Wired.IO.Http11Express.StaticHandlers; using Wired.IO.Protocol; using Wired.IO.Protocol.Handlers; using Wired.IO.Protocol.Response; +using Wired.IO.Transport; using IBaseRequest = Wired.IO.Protocol.Request.IBaseRequest; namespace Wired.IO.Builder; @@ -16,11 +15,11 @@ namespace Wired.IO.Builder; /// Provides a fluent configuration API to build and configure a instance. /// Supports setting up dependency injection, middleware, routes, TLS options, and runtime parameters. /// -/// The HTTP handler type implementing . +/// The HTTP handler type implementing . /// public sealed partial class Builder where TContext : IBaseContext - where THandler : IHttpHandler + where THandler : IHttpHandler { /// /// Gets the service collection used to register middleware, handlers, and services. @@ -36,9 +35,10 @@ public sealed partial class Builder /// Initializes the builder using a handler factory and defaults to HTTP/1.1 protocol. /// /// A delegate that creates the HTTP handler. - public Builder(Func handlerFactory) + /// + public Builder(Func handlerFactory, ITransport transport) { - Initialize(handlerFactory, [SslApplicationProtocol.Http11]); + Initialize(handlerFactory, [SslApplicationProtocol.Http11], transport); } /// @@ -46,9 +46,10 @@ public Builder(Func handlerFactory) /// /// A delegate that creates the HTTP handler. /// The list of supported ALPN protocols. - public Builder(Func handlerFactory, List sslApplicationProtocols) + /// + public Builder(Func handlerFactory, List sslApplicationProtocols, ITransport transport) { - Initialize(handlerFactory, sslApplicationProtocols); + Initialize(handlerFactory, sslApplicationProtocols, transport); } /// @@ -56,14 +57,17 @@ public Builder(Func handlerFactory, List sslAp /// /// The handler creation delegate. /// List of supported ALPN protocols. - private void Initialize(Func handlerFactory, List sslApplicationProtocols) + /// + private void Initialize(Func handlerFactory, List sslApplicationProtocols, ITransport transport) { _endpointDIRegister = new EndpointDIRegister(this); _root = new Group(prefix: "/", parent: null, diRegister: _endpointDIRegister); - - App.HttpHandler = handlerFactory(); + App.SslServerAuthenticationOptions.ApplicationProtocols = sslApplicationProtocols; + App.Transport = transport; + App.Transport.HttpHandler = handlerFactory(); + App.ServiceCollection.AddLogging(DefaultLoggingBuilder); } @@ -103,7 +107,7 @@ private void BuildRootOnly(IServiceProvider? serviceProvider = null!) App.RootMiddleware = App.Services.GetServices, Task>>().ToList(); App.BuildPipeline(App.RootMiddleware, App.EndpointInvoker); - App.SetPipeline(App.RootPipeline); + App.Transport.Pipeline = App.RootPipeline; App.RootEndpoints = []; @@ -141,7 +145,7 @@ private void BuildGroup(IServiceProvider? serviceProvider = null!) } App.CachePipelines(App.Services); - App.SetPipeline(App.GroupPipeline); + App.Transport.Pipeline = App.GroupPipeline; // Rearrange EmbeddedRoutes RearrangeEncodedRoutes(App.EncodedRoutes); diff --git a/Wired.IO/Handlers/CachedData.cs b/Wired.IO/Handlers/CachedData.cs new file mode 100644 index 0000000..e15d0ae --- /dev/null +++ b/Wired.IO/Handlers/CachedData.cs @@ -0,0 +1,44 @@ +using Wired.IO.Utilities.StringCache; + +namespace Wired.IO.Handlers; + +/// +/// Caches commonly-seen strings (routes, header keys/values, methods) to avoid repeated allocations +/// and string interning during hot paths. +/// +/// +/// Backed by custom fast hash caches sized for typical HTTP workloads. The pre-cached sets include common +/// request methods and frequently-present headers/values seen in benchmarks (e.g., TechEmpower Plaintext JSON). +/// +internal static class CachedData +{ + /// Cache of parsed routes (path components of the URL). + internal static readonly FastHashStringCache32 CachedRoutes = new FastHashStringCache32(); + /// Cache of query-string keys. + internal static readonly FastHashStringCache32 CachedQueryKeys = new FastHashStringCache32(); + /// Pre-cached HTTP methods (8 common verbs). + internal static readonly FastHashStringCache16 PreCachedHttpMethods = new FastHashStringCache16([ + "GET", + "POST", + "PUT", + "DELETE", + "PATCH", + "HEAD", + "OPTIONS", + "TRACE" + ], 8); + /// Pre-cached header keys commonly present in requests. + internal static readonly FastHashStringCache32 PreCachedHeaderKeys = new FastHashStringCache32([ + "Host", + "User-Agent", + "Cookie", + "Accept", + "Accept-Language", + "Connection" + ]); + /// Pre-cached header values commonly seen on the wire. + internal static readonly FastHashStringCache32 PreCachedHeaderValues = new FastHashStringCache32([ + "keep-alive", + "server", + ]); +} \ No newline at end of file diff --git a/Wired.IO/Http11Express/BuilderExtensions/StaticFiles.cs b/Wired.IO/Handlers/Http11Express/BuilderExtensions/StaticFiles.cs similarity index 98% rename from Wired.IO/Http11Express/BuilderExtensions/StaticFiles.cs rename to Wired.IO/Handlers/Http11Express/BuilderExtensions/StaticFiles.cs index 2c59467..cac70bf 100644 --- a/Wired.IO/Http11Express/BuilderExtensions/StaticFiles.cs +++ b/Wired.IO/Handlers/Http11Express/BuilderExtensions/StaticFiles.cs @@ -1,9 +1,8 @@ using Wired.IO.App; using Wired.IO.Builder; -using Wired.IO.Http11Express.Context; -using Wired.IO.Http11Express.StaticHandlers; +using Wired.IO.Handlers.Http11Express.Context; -namespace Wired.IO.Http11Express.BuilderExtensions; +namespace Wired.IO.Handlers.Http11Express.BuilderExtensions; /// /// Extension methods that wire up **static** and **SPA** file serving into diff --git a/Wired.IO/Http11Express/BuilderExtensions/StaticResources.cs b/Wired.IO/Handlers/Http11Express/BuilderExtensions/StaticResources.cs similarity index 99% rename from Wired.IO/Http11Express/BuilderExtensions/StaticResources.cs rename to Wired.IO/Handlers/Http11Express/BuilderExtensions/StaticResources.cs index 9636ea5..0f50cdb 100644 --- a/Wired.IO/Http11Express/BuilderExtensions/StaticResources.cs +++ b/Wired.IO/Handlers/Http11Express/BuilderExtensions/StaticResources.cs @@ -1,14 +1,13 @@ using System.Buffers; -using System.Diagnostics.Contracts; using System.IO.Pipelines; using System.Runtime.CompilerServices; using Wired.IO.App; using Wired.IO.Builder; -using Wired.IO.Http11Express.Context; +using Wired.IO.Handlers.Http11Express.Context; using Wired.IO.Protocol.Response; using Wired.IO.Utilities; -namespace Wired.IO.Http11Express.StaticHandlers; +namespace Wired.IO.Handlers.Http11Express.BuilderExtensions; public static class BuilderExtensions { diff --git a/Wired.IO/Http11Express/Context/Http11ExpressContext.cs b/Wired.IO/Handlers/Http11Express/Context/Http11ExpressContext.cs similarity index 86% rename from Wired.IO/Http11Express/Context/Http11ExpressContext.cs rename to Wired.IO/Handlers/Http11Express/Context/Http11ExpressContext.cs index efb5b89..3bc5d34 100644 --- a/Wired.IO/Http11Express/Context/Http11ExpressContext.cs +++ b/Wired.IO/Handlers/Http11Express/Context/Http11ExpressContext.cs @@ -1,19 +1,21 @@ using System.IO.Pipelines; -using System.Net.Sockets; -using Wired.IO.Http11Express.Request; -using Wired.IO.Http11Express.Response; +using Wired.IO.Handlers.Http11Express.Request; +using Wired.IO.Handlers.Http11Express.Response; using Wired.IO.Protocol; using Wired.IO.Protocol.Response; using Wired.IO.Utilities; -namespace Wired.IO.Http11Express.Context; +namespace Wired.IO.Handlers.Http11Express.Context; // This class cannot be sealed, might have super types public class Http11ExpressContext : IBaseContext { - public Socket Inner { get; internal set; } = null!; + public System.Net.Sockets.Socket Inner { get; internal set; } = null!; + public PipeReader Reader { get; set; } = null!; + public PipeWriter Writer { get; set; } = null!; + public IExpressRequest Request { get; set; } = new Http11ExpressRequest() { Headers = new PooledDictionary( @@ -23,9 +25,11 @@ public class Http11ExpressContext : IBaseContext _responseBuilder ??= new ExpressResponseBuilder(Response!); public ExpressResponseBuilder Respond() @@ -43,6 +47,7 @@ public ExpressResponseBuilder Respond() public void Clear() { + Inner = null!; Response?.Clear(); Request.Clear(); } diff --git a/Wired.IO/Http11Express/Context/Http11ExpressContextExtensions.cs b/Wired.IO/Handlers/Http11Express/Context/Http11ExpressContextExtensions.cs similarity index 96% rename from Wired.IO/Http11Express/Context/Http11ExpressContextExtensions.cs rename to Wired.IO/Handlers/Http11Express/Context/Http11ExpressContextExtensions.cs index 54bc85a..4d09192 100644 --- a/Wired.IO/Http11Express/Context/Http11ExpressContextExtensions.cs +++ b/Wired.IO/Handlers/Http11Express/Context/Http11ExpressContextExtensions.cs @@ -3,7 +3,7 @@ // ReSharper disable MemberCanBePrivate.Global -namespace Wired.IO.Http11Express.Context; +namespace Wired.IO.Handlers.Http11Express.Context; public static class Http11ExpressContextExtensions { diff --git a/Wired.IO/Http11Express/Request/Http11ExpressRequest.cs b/Wired.IO/Handlers/Http11Express/Request/Http11ExpressRequest.cs similarity index 96% rename from Wired.IO/Http11Express/Request/Http11ExpressRequest.cs rename to Wired.IO/Handlers/Http11Express/Request/Http11ExpressRequest.cs index 34945d1..7710623 100644 --- a/Wired.IO/Http11Express/Request/Http11ExpressRequest.cs +++ b/Wired.IO/Handlers/Http11Express/Request/Http11ExpressRequest.cs @@ -3,7 +3,7 @@ using Wired.IO.Protocol.Request; using Wired.IO.Utilities; -namespace Wired.IO.Http11Express.Request; +namespace Wired.IO.Handlers.Http11Express.Request; [SkipLocalsInit] public class Http11ExpressRequest : IExpressRequest diff --git a/Wired.IO/Http11Express/Request/IExpressRequest.cs b/Wired.IO/Handlers/Http11Express/Request/IExpressRequest.cs similarity index 96% rename from Wired.IO/Http11Express/Request/IExpressRequest.cs rename to Wired.IO/Handlers/Http11Express/Request/IExpressRequest.cs index d89fd51..4eb4dd2 100644 --- a/Wired.IO/Http11Express/Request/IExpressRequest.cs +++ b/Wired.IO/Handlers/Http11Express/Request/IExpressRequest.cs @@ -1,7 +1,7 @@ using Wired.IO.Protocol.Request; using Wired.IO.Utilities; -namespace Wired.IO.Http11Express.Request; +namespace Wired.IO.Handlers.Http11Express.Request; public interface IExpressRequest : IBaseRequest { diff --git a/Wired.IO/Http11Express/Response/Content/ExpressJsonContent.cs b/Wired.IO/Handlers/Http11Express/Response/Content/ExpressJsonContent.cs similarity index 97% rename from Wired.IO/Http11Express/Response/Content/ExpressJsonContent.cs rename to Wired.IO/Handlers/Http11Express/Response/Content/ExpressJsonContent.cs index 48809c6..c67000f 100644 --- a/Wired.IO/Http11Express/Response/Content/ExpressJsonContent.cs +++ b/Wired.IO/Handlers/Http11Express/Response/Content/ExpressJsonContent.cs @@ -1,12 +1,11 @@ -using Microsoft.Extensions.ObjectPool; -using System.IO.Pipelines; +using System.IO.Pipelines; using System.Runtime.CompilerServices; using System.Text.Json; -using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; +using Microsoft.Extensions.ObjectPool; using Wired.IO.Protocol.Writers; -namespace Wired.IO.Http11Express.Response.Content; +namespace Wired.IO.Handlers.Http11Express.Response.Content; /// /// Provides pooled writers and utilities for JSON serialization. diff --git a/Wired.IO/Http11Express/Response/Content/ExpressRawContent.cs b/Wired.IO/Handlers/Http11Express/Response/Content/ExpressRawContent.cs similarity index 88% rename from Wired.IO/Http11Express/Response/Content/ExpressRawContent.cs rename to Wired.IO/Handlers/Http11Express/Response/Content/ExpressRawContent.cs index 819f07e..8e16e98 100644 --- a/Wired.IO/Http11Express/Response/Content/ExpressRawContent.cs +++ b/Wired.IO/Handlers/Http11Express/Response/Content/ExpressRawContent.cs @@ -2,7 +2,7 @@ using System.IO.Pipelines; using System.Runtime.CompilerServices; -namespace Wired.IO.Http11Express.Response.Content; +namespace Wired.IO.Handlers.Http11Express.Response.Content; [SkipLocalsInit] public class ExpressRawContent : IExpressResponseContent diff --git a/Wired.IO/Http11Express/Response/Content/ExpressStringContent.cs b/Wired.IO/Handlers/Http11Express/Response/Content/ExpressStringContent.cs similarity index 92% rename from Wired.IO/Http11Express/Response/Content/ExpressStringContent.cs rename to Wired.IO/Handlers/Http11Express/Response/Content/ExpressStringContent.cs index 08fa4fc..9ccb55c 100644 --- a/Wired.IO/Http11Express/Response/Content/ExpressStringContent.cs +++ b/Wired.IO/Handlers/Http11Express/Response/Content/ExpressStringContent.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; using Wired.IO.Utilities; -namespace Wired.IO.Http11Express.Response.Content; +namespace Wired.IO.Handlers.Http11Express.Response.Content; [SkipLocalsInit] public class ExpressStringContent : IExpressResponseContent diff --git a/Wired.IO/Http11Express/Response/Content/IExpressResponseContent.cs b/Wired.IO/Handlers/Http11Express/Response/Content/IExpressResponseContent.cs similarity index 86% rename from Wired.IO/Http11Express/Response/Content/IExpressResponseContent.cs rename to Wired.IO/Handlers/Http11Express/Response/Content/IExpressResponseContent.cs index e334eef..473b048 100644 --- a/Wired.IO/Http11Express/Response/Content/IExpressResponseContent.cs +++ b/Wired.IO/Handlers/Http11Express/Response/Content/IExpressResponseContent.cs @@ -1,7 +1,7 @@ using System.IO.Pipelines; using System.Text.Json.Serialization.Metadata; -namespace Wired.IO.Http11Express.Response.Content; +namespace Wired.IO.Handlers.Http11Express.Response.Content; public interface IExpressResponseContent { diff --git a/Wired.IO/Protocol/Response/ExpressResponseBuilder.cs b/Wired.IO/Handlers/Http11Express/Response/ExpressResponseBuilder.cs similarity index 97% rename from Wired.IO/Protocol/Response/ExpressResponseBuilder.cs rename to Wired.IO/Handlers/Http11Express/Response/ExpressResponseBuilder.cs index ba08419..a73c7fb 100644 --- a/Wired.IO/Protocol/Response/ExpressResponseBuilder.cs +++ b/Wired.IO/Handlers/Http11Express/Response/ExpressResponseBuilder.cs @@ -1,10 +1,10 @@ using System.Runtime.CompilerServices; using System.Text.Json.Serialization.Metadata; -using Wired.IO.Http11Express.Response; -using Wired.IO.Http11Express.Response.Content; +using Wired.IO.Handlers.Http11Express.Response.Content; +using Wired.IO.Protocol.Response; using Wired.IO.Utilities; -namespace Wired.IO.Protocol.Response; +namespace Wired.IO.Handlers.Http11Express.Response; public enum ContentLengthStrategy { diff --git a/Wired.IO/Http11Express/Response/Http11ExpressResponse.cs b/Wired.IO/Handlers/Http11Express/Response/Http11ExpressResponse.cs similarity index 94% rename from Wired.IO/Http11Express/Response/Http11ExpressResponse.cs rename to Wired.IO/Handlers/Http11Express/Response/Http11ExpressResponse.cs index e81c96c..ec45bad 100644 --- a/Wired.IO/Http11Express/Response/Http11ExpressResponse.cs +++ b/Wired.IO/Handlers/Http11Express/Response/Http11ExpressResponse.cs @@ -1,9 +1,9 @@ -using Wired.IO.Http11Express.Response.Content; +using Wired.IO.Handlers.Http11Express.Response.Content; using Wired.IO.Protocol.Response; using Wired.IO.Protocol.Response.Headers; using Wired.IO.Utilities; -namespace Wired.IO.Http11Express.Response; +namespace Wired.IO.Handlers.Http11Express.Response; public class Http11ExpressResponse : IExpressResponse { diff --git a/Wired.IO/Http11Express/Response/IExpressResponse.cs b/Wired.IO/Handlers/Http11Express/Response/IExpressResponse.cs similarity index 94% rename from Wired.IO/Http11Express/Response/IExpressResponse.cs rename to Wired.IO/Handlers/Http11Express/Response/IExpressResponse.cs index 679d257..e9bbe17 100644 --- a/Wired.IO/Http11Express/Response/IExpressResponse.cs +++ b/Wired.IO/Handlers/Http11Express/Response/IExpressResponse.cs @@ -1,9 +1,9 @@ -using Wired.IO.Http11Express.Response.Content; +using Wired.IO.Handlers.Http11Express.Response.Content; using Wired.IO.Protocol.Response; using Wired.IO.Protocol.Response.Headers; using Wired.IO.Utilities; -namespace Wired.IO.Http11Express.Response; +namespace Wired.IO.Handlers.Http11Express.Response; public interface IExpressResponse : IBaseResponse { diff --git a/Wired.IO/Http11Express/StaticHandlers/FlowControl.cs b/Wired.IO/Handlers/Http11Express/StaticHandlers/FlowControl.cs similarity index 95% rename from Wired.IO/Http11Express/StaticHandlers/FlowControl.cs rename to Wired.IO/Handlers/Http11Express/StaticHandlers/FlowControl.cs index 4fd5d86..33c79de 100644 --- a/Wired.IO/Http11Express/StaticHandlers/FlowControl.cs +++ b/Wired.IO/Handlers/Http11Express/StaticHandlers/FlowControl.cs @@ -1,12 +1,10 @@ using System.Buffers; using System.IO.Pipelines; using System.Runtime.CompilerServices; -using Wired.IO.App; -using Wired.IO.Http11Express.Context; +using Wired.IO.Handlers.Http11Express.Context; using Wired.IO.Protocol.Response; -using Wired.IO.Utilities; -namespace Wired.IO.Http11Express.StaticHandlers; +namespace Wired.IO.Handlers.Http11Express.StaticHandlers; /// /// Provides prebuilt lightweight response handlers for exceptional or control flow diff --git a/Wired.IO/Http11Express/WiredHttp11Express.Handler.cs b/Wired.IO/Handlers/Http11Express/WiredHttp11Express.Handler.cs similarity index 94% rename from Wired.IO/Http11Express/WiredHttp11Express.Handler.cs rename to Wired.IO/Handlers/Http11Express/WiredHttp11Express.Handler.cs index 7f2760f..bcdfad9 100644 --- a/Wired.IO/Http11Express/WiredHttp11Express.Handler.cs +++ b/Wired.IO/Handlers/Http11Express/WiredHttp11Express.Handler.cs @@ -1,21 +1,16 @@ -using Microsoft.Extensions.ObjectPool; -using System.Buffers; -using System.Diagnostics; +using System.Buffers; using System.Globalization; using System.IO.Pipelines; -using System.Net.Security; -using System.Net.Sockets; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices.Marshalling; using System.Text; -using Wired.IO.Http11Express.Context; -using Wired.IO.Http11Express.Request; -using Wired.IO.Protocol.Handlers; +using Microsoft.Extensions.ObjectPool; +using Wired.IO.Handlers.Http11Express.Context; +using Wired.IO.Handlers.Http11Express.Request; using Wired.IO.Protocol.Writers; +using Wired.IO.Transport.Socket; using Wired.IO.Utilities; -using Wired.IO.Utilities.StringCache; -namespace Wired.IO.Http11Express; +namespace Wired.IO.Handlers.Http11Express; #pragma warning disable CS1591 // (We document via XML comments below; suppress warnings if any members stay undocumented.) @@ -40,7 +35,7 @@ public sealed class WiredHttp11Express : WiredHttp11Express /// -public partial class WiredHttp11Express : IHttpHandler +public partial class WiredHttp11Express : ISocketHttpHandler // TContext can be a super type of Http11ExpressContext where TContext : Http11ExpressContext, new() { @@ -97,7 +92,7 @@ public override bool Return(TContext context) /// /// The connection is half-managed here: exceptions are swallowed to avoid noisy logs in benchmark scenarios; the stream is closed when the reader/writer complete. /// - public async Task HandleClientAsync(Socket inner, Stream stream, Func pipeline, CancellationToken stoppingToken) + public async Task HandleClientAsync(System.Net.Sockets.Socket inner, Stream stream, Func pipeline, CancellationToken stoppingToken) { // Rent a context object from the pool var context = ContextPool.Get(); @@ -918,44 +913,4 @@ private static void ParseHeaderLine(in ReadOnlySequence headerLine, IExpre private const byte Equal = 0x3D; // '=' private const byte Colon = 0x3A; // ':' private const byte SemiColon = 0x3B; // ';' -} -/// -/// Caches commonly-seen strings (routes, header keys/values, methods) to avoid repeated allocations -/// and string interning during hot paths. -/// -/// -/// Backed by custom fast hash caches sized for typical HTTP workloads. The pre-cached sets include common -/// request methods and frequently-present headers/values seen in benchmarks (e.g., TechEmpower Plaintext JSON). -/// -internal static class CachedData -{ - /// Cache of parsed routes (path components of the URL). - internal static readonly FastHashStringCache32 CachedRoutes = new FastHashStringCache32(); - /// Cache of query-string keys. - internal static readonly FastHashStringCache32 CachedQueryKeys = new FastHashStringCache32(); - /// Pre-cached HTTP methods (8 common verbs). - internal static readonly FastHashStringCache16 PreCachedHttpMethods = new FastHashStringCache16([ - "GET", - "POST", - "PUT", - "DELETE", - "PATCH", - "HEAD", - "OPTIONS", - "TRACE" - ], 8); - /// Pre-cached header keys commonly present in requests. - internal static readonly FastHashStringCache32 PreCachedHeaderKeys = new FastHashStringCache32([ - "Host", - "User-Agent", - "Cookie", - "Accept", - "Accept-Language", - "Connection" - ]); - /// Pre-cached header values commonly seen on the wire. - internal static readonly FastHashStringCache32 PreCachedHeaderValues = new FastHashStringCache32([ - "keep-alive", - "server", - ]); } \ No newline at end of file diff --git a/Wired.IO/Http11Express/WiredHttp11Express.Response.cs b/Wired.IO/Handlers/Http11Express/WiredHttp11Express.Response.cs similarity index 97% rename from Wired.IO/Http11Express/WiredHttp11Express.Response.cs rename to Wired.IO/Handlers/Http11Express/WiredHttp11Express.Response.cs index 77f9cfc..768e87f 100644 --- a/Wired.IO/Http11Express/WiredHttp11Express.Response.cs +++ b/Wired.IO/Handlers/Http11Express/WiredHttp11Express.Response.cs @@ -1,12 +1,12 @@ using System.Buffers; using System.Buffers.Text; -using System.ComponentModel.DataAnnotations; using System.IO.Pipelines; using System.Runtime.CompilerServices; +using Wired.IO.Handlers.Http11Express.Response; using Wired.IO.Protocol.Response; using Wired.IO.Utilities; -namespace Wired.IO.Http11Express; +namespace Wired.IO.Handlers.Http11Express; public partial class WiredHttp11Express { diff --git a/Wired.IO/Http11Express/_README.txt b/Wired.IO/Handlers/Http11Express/_README.txt similarity index 100% rename from Wired.IO/Http11Express/_README.txt rename to Wired.IO/Handlers/Http11Express/_README.txt diff --git a/Wired.IO/Handlers/Http11Overclocked/Context/Http11OverclockedContext.cs b/Wired.IO/Handlers/Http11Overclocked/Context/Http11OverclockedContext.cs new file mode 100644 index 0000000..1749a08 --- /dev/null +++ b/Wired.IO/Handlers/Http11Overclocked/Context/Http11OverclockedContext.cs @@ -0,0 +1,40 @@ +using URocket.Connection; +using Wired.IO.Handlers.Http11Overclocked.Request; +using Wired.IO.Handlers.Http11Overclocked.Response; +using Wired.IO.Protocol; +using Wired.IO.Protocol.Request; + +namespace Wired.IO.Handlers.Http11Overclocked.Context; + +public class Http11OverclockedContext : IBaseContext +{ + public Connection Connection { get; internal set; } = null!; + + public IBaseRequest Request { get; } = new Http11OverclockedRequest(); + + public IOverclockedResponse? Response { get; private set; } + + private OverclockedResponseBuilder? _responseBuilder; + + private OverclockedResponseBuilder ResponseBuilder => _responseBuilder ??= new OverclockedResponseBuilder(Response!); + + public OverclockedResponseBuilder Respond() + { + Response ??= new Http11OverclockedResponse(); + Response.Activate(); + + return ResponseBuilder; + } + + public IServiceProvider Services { get; set; } = null!; + + public CancellationToken CancellationToken { get; set; } + + public void Clear() + { + Request.Clear(); + Response?.Clear(); + } + + public void Dispose() { } +} \ No newline at end of file diff --git a/Wired.IO/Handlers/Http11Overclocked/Request/Http11OverclockedRequest.cs b/Wired.IO/Handlers/Http11Overclocked/Request/Http11OverclockedRequest.cs new file mode 100644 index 0000000..3097728 --- /dev/null +++ b/Wired.IO/Handlers/Http11Overclocked/Request/Http11OverclockedRequest.cs @@ -0,0 +1,17 @@ +using Wired.IO.Protocol.Request; + +namespace Wired.IO.Handlers.Http11Overclocked.Request; + +public class Http11OverclockedRequest : IBaseRequest +{ + public string Route { get; set; } = null!; + + public string HttpMethod { get; set; } = null!; + + public void Clear() + { + + } + + public void Dispose() { } +} \ No newline at end of file diff --git a/Wired.IO/Handlers/Http11Overclocked/Response/Http11OverclockedResponse.cs b/Wired.IO/Handlers/Http11Overclocked/Response/Http11OverclockedResponse.cs new file mode 100644 index 0000000..1483913 --- /dev/null +++ b/Wired.IO/Handlers/Http11Overclocked/Response/Http11OverclockedResponse.cs @@ -0,0 +1,36 @@ +using Wired.IO.Protocol.Response; +using Wired.IO.Utilities; + +namespace Wired.IO.Handlers.Http11Overclocked.Response; + +public class Http11OverclockedResponse : IOverclockedResponse +{ + private bool _active; + + public void Activate() => _active = true; + + public bool IsActive() => _active; + + public ResponseStatus Status { get; set; } = ResponseStatus.Ok; + + public Action ContentHandler { get; set; } = null!; + + public Func AsyncContentHandler { get; set; } = null!; + + public Utf8View ContentType { get; set; } + + public ulong? ContentLength { get; set; } + + public void Clear() + { + _active = false; + + ContentType = default; + ContentLength = null; + + } + + public void Dispose() + { + } +} \ No newline at end of file diff --git a/Wired.IO/Handlers/Http11Overclocked/Response/IOverclockedResponse.cs b/Wired.IO/Handlers/Http11Overclocked/Response/IOverclockedResponse.cs new file mode 100644 index 0000000..c7edfab --- /dev/null +++ b/Wired.IO/Handlers/Http11Overclocked/Response/IOverclockedResponse.cs @@ -0,0 +1,26 @@ +using Wired.IO.Protocol.Response; +using Wired.IO.Utilities; + +namespace Wired.IO.Handlers.Http11Overclocked.Response; + +public interface IOverclockedResponse : IBaseResponse +{ + void Activate(); + + bool IsActive(); + + /// + /// The type of the content. + /// + Utf8View ContentType { get; set; } + + ulong? ContentLength { get; set; } + + ResponseStatus Status { get; set; } + + Action ContentHandler { get; set; } + + Func AsyncContentHandler { get; set; } + + void Clear(); +} \ No newline at end of file diff --git a/Wired.IO/Handlers/Http11Overclocked/Response/OverclockedResponseBuilder.cs b/Wired.IO/Handlers/Http11Overclocked/Response/OverclockedResponseBuilder.cs new file mode 100644 index 0000000..2629ac2 --- /dev/null +++ b/Wired.IO/Handlers/Http11Overclocked/Response/OverclockedResponseBuilder.cs @@ -0,0 +1,43 @@ +using Wired.IO.Protocol.Response; +using Wired.IO.Utilities; + +namespace Wired.IO.Handlers.Http11Overclocked.Response; + +public enum ContentStrategy +{ + Utf8JsonWriter +} + +public class OverclockedResponseBuilder(IOverclockedResponse response) +{ + + + public OverclockedResponseBuilder Content(Action contentHandler, ulong? length = null) + { + response.ContentLength = length; + response.ContentHandler = contentHandler; + + return this; + } + + public OverclockedResponseBuilder Type(ReadOnlySpan contentType) + { + response.ContentType = Utf8View.FromLiteral(contentType); + + return this; + } + + public OverclockedResponseBuilder Status(ResponseStatus status) + { + response.Status = status; + + return this; + } + + public OverclockedResponseBuilder Length(ulong length) + { + response.ContentLength = length; + + return this; + } +} \ No newline at end of file diff --git a/Wired.IO/Handlers/Http11Overclocked/WiredHttp11Overclocked.Handler.cs b/Wired.IO/Handlers/Http11Overclocked/WiredHttp11Overclocked.Handler.cs new file mode 100644 index 0000000..5a37a5b --- /dev/null +++ b/Wired.IO/Handlers/Http11Overclocked/WiredHttp11Overclocked.Handler.cs @@ -0,0 +1,169 @@ +using System.Buffers; +using Microsoft.Extensions.ObjectPool; +using URocket.Connection; +using URocket.Utils.UnmanagedMemoryManager; +using Wired.IO.Handlers.Http11Overclocked.Context; +using Wired.IO.Protocol.Writers; +using Wired.IO.Transport.Rocket; + +namespace Wired.IO.Handlers.Http11Overclocked; + +// This Handler does not support pipelined requests + +public sealed class WiredHttp11Overclocked : WiredHttp11Rocket { } + +public partial class WiredHttp11Rocket : IRocketHttpHandler + where TContext : Http11OverclockedContext, new() +{ + /// + /// Object pool used to recycle request contexts for reduced allocations and improved performance. + /// + private static readonly ObjectPool ContextPool = + new DefaultObjectPool(new PipelinedContextPoolPolicy(), 4096 * 4); + + /// + /// Pool policy that defines how to create and reset pooled instances. + /// + private class PipelinedContextPoolPolicy : PooledObjectPolicy + { + /// + /// Creates a new instance of . + /// + public override TContext Create() => new(); + + /// + /// Resets the context before returning it to the pool. + /// + /// The context instance to return. + /// true if the context can be reused; otherwise, false. + public override bool Return(TContext context) + { + // Overclocked context does not need to be cleared + //context.Clear(); // User-defined reset method to clean internal state. + return true; + } + } + + public async Task HandleClientAsync(Connection connection, Func pipeline, CancellationToken stoppingToken) + { + // Rent a context object from the pool + var context = ContextPool.Get(); + context.Connection = connection; + + try + { + await ProcessRequestsAsync(context, pipeline); + } + catch(Exception e) + { + Console.WriteLine(e.Message); + // TODO Check the CancellationTokenSource Impl @ Express Handler + } + finally + { + // Return context to pool for reuse + ContextPool.Return(context); + } + } + + private async Task ProcessRequestsAsync(TContext context, Func pipeline) + { + while (true) + { + var result = await context.Connection.ReadAsync(); // Read data from the wire + if (result.IsClosed) + break; + + var rings = context.Connection.PeekAllSnapshotRingsAsUnmanagedMemory(result); + var sequence = rings.ToReadOnlySequence(); + + if (sequence.IsSingleSegment) + { + // Single segment, fast path, use span based operations + var span = sequence.FirstSpan; + + var idx = span.IndexOf(CrlfCrlf); + if (idx == -1) /* Not enough data */ continue; + + // Full header is present + var firstLineEnd = span.IndexOf(Crlf); + var firstHeader = span[..firstLineEnd]; + + var firstSpace = firstHeader.IndexOf(Space); + if (firstSpace == -1) throw new InvalidOperationException("Invalid request line"); + + context.Request.HttpMethod = CachedData.PreCachedHttpMethods.GetOrAdd(firstHeader[..firstSpace]); + + var secondSpaceRelative = firstHeader[(firstSpace + 1)..].IndexOf(Space); + if (secondSpaceRelative == -1) throw new InvalidOperationException("Invalid request line"); + + var secondSpace = firstSpace + secondSpaceRelative + 1; + var url = firstHeader[(firstSpace + 1)..secondSpace]; + + // Url is same as route + context.Request.Route = CachedData.CachedRoutes.GetOrAdd(url); + + // No more parsing, this handler now delegates parsing control to the request pipeline. + // We could also add logic to check whether a body is present and wait for more data if body is not fully received. - Not needed for this benchmark + } + else + { + var reader = new SequenceReader(sequence); + if (!reader.TryReadTo(out ReadOnlySequence headers, CrlfCrlf)) /* Not enough data */ continue; + + // Full header is present + + // Extract the Http method + if(!reader.TryReadTo(out ReadOnlySequence methodSequence, Space)) /* Not enough data */ continue; + + context.Request.HttpMethod = CachedData.PreCachedHttpMethods.GetOrAdd(methodSequence.ToSpan()); + + // Read URL/path + if (!reader.TryReadTo(out ReadOnlySequence urlSequence, Space)) /* Not enough data */ continue; + + // Url is same as route + context.Request.Route = CachedData.CachedRoutes.GetOrAdd(urlSequence.ToSpan()); + + // No more parsing, this handler now delegates parsing control to the request pipeline. + // We could also add logic to check whether a body is present and wait for more data if body is not fully received. - Not needed for this benchmark + } + + // Execute request pipeline + await pipeline(context); + + if (context.Response is not null && context.Response.IsActive()) + { + WriteStatusLine(context.Connection, context.Response.Status); + WriteHeaders(context); + context.Response.ContentHandler(); + } + + await context.Connection.FlushAsync(); + + // Dequeue and return rings to the kernel + for(int i = 0; i < rings.Length; i++) + context.Connection.GetRing(); + foreach (var ring in rings) + context.Connection.ReturnRing(ring.BufferId); + + // Signal we are ready for a new read + context.Connection.ResetRead(); + } + } + + // ---- Constants & literals ---- + + /// CRLF delimiter used for line termination. + private static ReadOnlySpan Crlf => "\r\n"u8; + private static ReadOnlySpan CrlfCrlf => "\r\n\r\n"u8; + + private const string ContentLength = "Content-Length"; + private const string TransferEncoding = "Transfer-Encoding"; + + private const byte Space = 0x20; // ' ' + private const byte Question = 0x3F; // '?' + private const byte QuerySeparator = 0x26; // '&' + private const byte Equal = 0x3D; // '=' + private const byte Colon = 0x3A; // ':' + private const byte SemiColon = 0x3B; // ';' +} \ No newline at end of file diff --git a/Wired.IO/Handlers/Http11Overclocked/WiredHttp11Overclocked.Response.cs b/Wired.IO/Handlers/Http11Overclocked/WiredHttp11Overclocked.Response.cs new file mode 100644 index 0000000..01b0d1c --- /dev/null +++ b/Wired.IO/Handlers/Http11Overclocked/WiredHttp11Overclocked.Response.cs @@ -0,0 +1,57 @@ +using System.Buffers; +using System.Buffers.Text; +using System.Runtime.CompilerServices; +using Wired.IO.Protocol.Response; +using Wired.IO.Utilities; + +namespace Wired.IO.Handlers.Http11Overclocked; + +public partial class WiredHttp11Rocket +{ + private static ReadOnlySpan ServerHeaderName => "Server: "u8; + private static ReadOnlySpan ContentTypeHeader => "Content-Type: "u8; + private static ReadOnlySpan ContentLengthHeader => "Content-Length: "u8; + private static ReadOnlySpan ContentEncodingHeader => "Content-Encoding: "u8; + private static ReadOnlySpan TransferEncodingHeader => "Transfer-Encoding: "u8; + private static ReadOnlySpan TransferEncodingChunkedHeader => "Transfer-Encoding: chunked\r\n"u8; + private static ReadOnlySpan LastModifiedHeader => "Last-Modified: "u8; + private static ReadOnlySpan ExpiresHeader => "Expires: "u8; + private static ReadOnlySpan ConnectionHeader => "Connection: "u8; + private static ReadOnlySpan DateHeader => "Date: "u8; + + [SkipLocalsInit] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteStatusLine(IBufferWriter writer, ResponseStatus statusCode) + => writer.Write(HttpStatusLines.Lines[(int)statusCode]); + + [SkipLocalsInit] + private static void WriteHeaders(TContext context) + { + var writer = context.Connection; + + writer.Write("Server: W\r\n"u8); + + if (!context.Response!.ContentType.IsEmpty) + { + writer.Write(ContentTypeHeader); + writer.Write(context.Response.ContentType.AsSpan()); + writer.Write("\r\n"u8); + } + + // If ContentLength is not zero, its length is known and is valid to use Content-Length header + if (context.Response.ContentLength is not null) + { + writer.Write(ContentLengthHeader); + + var buffer = writer.GetSpan(16); // 16 is enough for any int in UTF-8 + if (!Utf8Formatter.TryFormat((ulong)context.Response.ContentLength, buffer, out var written)) + throw new InvalidOperationException("Failed to format int"); + + writer.Advance(written); + + writer.Write("\r\n"u8); + } + + writer.Write(DateHelper.HeaderBytes); + } +} \ No newline at end of file diff --git a/Wired.IO/Handlers/Http11Rocket/Context/Http11RocketContext.cs b/Wired.IO/Handlers/Http11Rocket/Context/Http11RocketContext.cs new file mode 100644 index 0000000..8b0c033 --- /dev/null +++ b/Wired.IO/Handlers/Http11Rocket/Context/Http11RocketContext.cs @@ -0,0 +1,58 @@ +using System.IO.Pipelines; +using URocket.Connection; +using Wired.IO.Handlers.Http11Express.Request; +using Wired.IO.Handlers.Http11Express.Response; +using Wired.IO.Protocol; +using Wired.IO.Utilities; + +namespace Wired.IO.Handlers.Http11Rocket.Context; + +// This class cannot be sealed, might have super types +public class Http11RocketContext : IBaseContext +{ + public Connection Connection { get; internal set; } = null!; + + public PipeReader Reader { get; set; } = null!; + + public PipeWriter Writer { get; set; } = null!; + + public IExpressRequest Request { get; } = new Http11ExpressRequest() + { + Headers = new PooledDictionary( + capacity: 8, + comparer: StringComparer.OrdinalIgnoreCase), + QueryParameters = new PooledDictionary( + capacity: 8, + comparer: StringComparer.OrdinalIgnoreCase) + }; + + public IExpressResponse? Response { get; private set; } + + private ExpressResponseBuilder? _responseBuilder; + private ExpressResponseBuilder ResponseBuilder => _responseBuilder ??= new ExpressResponseBuilder(Response!); + + public ExpressResponseBuilder Respond() + { + Response ??= new Http11ExpressResponse(); + Response.Activate(); + + return ResponseBuilder; + } + + public IServiceProvider Services { get; set; } = null!; + + public CancellationToken CancellationToken { get; set; } + + public void Clear() + { + Connection = null!; + Response?.Clear(); + Request.Clear(); + } + + public void Dispose() + { + Response?.Dispose(); + Request.Dispose(); + } +} \ No newline at end of file diff --git a/Wired.IO/Handlers/Http11Rocket/WiredHttp11Rocket.Handler.cs b/Wired.IO/Handlers/Http11Rocket/WiredHttp11Rocket.Handler.cs new file mode 100644 index 0000000..06a89e1 --- /dev/null +++ b/Wired.IO/Handlers/Http11Rocket/WiredHttp11Rocket.Handler.cs @@ -0,0 +1,788 @@ +using System.Buffers; +using System.Globalization; +using System.IO.Pipelines; +using System.Runtime.CompilerServices; +using System.Text; +using Microsoft.Extensions.ObjectPool; +using URocket.Connection; +using Wired.IO.Handlers.Http11Express.Request; +using Wired.IO.Handlers.Http11Rocket.Context; +using Wired.IO.Protocol.Writers; +using Wired.IO.Transport.Rocket; +using Wired.IO.Utilities; + +namespace Wired.IO.Handlers.Http11Rocket; + +// Public non-generic facade +public sealed class WiredHttp11Rocket : WiredHttp11Rocket { } + +public partial class WiredHttp11Rocket : IRocketHttpHandler + where TContext : Http11RocketContext, new() +{ + /// + /// Parser state machine for the multi-segment path. + /// + private enum State + { + /// Expecting the request line: METHOD SP PATH SP HTTP/VERSION CRLF + StartLine, + /// Reading header lines until a blank line (CRLF) is found. + Headers, + /// Headers complete; body (if any) would be processed next. + Body + } + + /// + /// Object pool used to recycle request contexts for reduced allocations and improved performance. + /// + private static readonly ObjectPool ContextPool = + new DefaultObjectPool(new PipelinedContextPoolPolicy(), 4096 * 4); + + /// + /// Pool policy that defines how to create and reset pooled instances. + /// + private class PipelinedContextPoolPolicy : PooledObjectPolicy + { + /// + /// Creates a new instance of . + /// + public override TContext Create() => new(); + + /// + /// Resets the context before returning it to the pool. + /// + /// The context instance to return. + /// true if the context can be reused; otherwise, false. + public override bool Return(TContext context) + { + context.Clear(); // User-defined reset method to clean internal state. + return true; + } + } + + /// + /// Handles a client connection using a , wiring it to / + /// and processing zero or more HTTP/1.1 requests in sequence. + /// + /// + /// The application pipeline delegate that processes a fully-parsed request. + /// Cancellation token signaling server shutdown. + /// A task that completes when the connection closes or parsing finishes. + /// + /// The connection is half-managed here: exceptions are swallowed to avoid noisy logs in benchmark scenarios; the stream is closed when the reader/writer complete. + /// + public async Task HandleClientAsync(Connection connection, Func pipeline, CancellationToken stoppingToken) + { + // Rent a context object from the pool + var context = ContextPool.Get(); + + var stream = new ConnectionStream(connection); + + // Assign a new CancellationTokenSource to support per-request cancellation + var cts = new CancellationTokenSource(); + context.CancellationToken = cts.Token; + + // Wrap the stream in a PipeReader and PipeWriter for efficient buffered reads/writes + context.Reader = PipeReader.Create(stream, + new StreamPipeReaderOptions( + MemoryPool.Shared, + leaveOpen: false, + bufferSize: 4096 * 4, + minimumReadSize: 1024)); + + context.Writer = PipeWriter.Create(stream, + new StreamPipeWriterOptions( + MemoryPool.Shared, + leaveOpen: false, + minimumBufferSize: 512)); + + try + { + await ProcessRequestsAsync(context, pipeline); + } + catch + { + // Swallow all exceptions; connection will be closed silently + await cts.CancelAsync(); + } + finally + { + // Gracefully complete the reader/writer to release underlying resources + await context.Reader.CompleteAsync(); + await context.Writer.CompleteAsync(); + + // Return context to pool for reuse + ContextPool.Return(context); + } + } + + /// + /// Reads from the and parses as many complete HTTP requests as are currently available, + /// dispatching each to . + /// + /// The pooled per-connection context. + /// Application delegate to invoke once a request line and headers are parsed. + /// + /// Uses a fast path for single-segment buffers to minimize copying and branching. For multi-segment buffers, + /// a is used to parse across segments safely. + /// + [SkipLocalsInit] + private static async Task ProcessRequestsAsync(TContext context, Func pipeline) + { + var state = State.StartLine; + + while (true) + { + var readResult = await context.Reader.ReadAsync(); + var buffer = readResult.Buffer; + + var isCompleted = readResult.IsCompleted; + + if (buffer.IsEmpty && isCompleted) + return; + + var flush = false; + + // Hot path: A new request is starting, and the buffer is a single segment + // If some of the request is already read, always fall back to multi-segment path + // This avoids complex state management in the single-segment path + // This optimizes for the common case of small requests that fit in one segment (vast majority of cases) + if (buffer.IsSingleSegment && state == State.StartLine) + { + var currentPosition = 0; + + while (true) + { + if (buffer.Length == 0 || isCompleted) + break; + + var requestReceived = ExtractHeaderFromSingleSegment(context, ref buffer, ref currentPosition); + + if (!requestReceived) + { + context.Reader.AdvanceTo(buffer.GetPosition(0), buffer.GetPosition(buffer.FirstSpan.Length)); + break; + } + + context.Reader.AdvanceTo(buffer.GetPosition(currentPosition)); + + state = State.Body; + + var bodyReceived = TryExtractBodyFromSingleSegment(context, ref buffer, ref currentPosition, out var bodyEmpty); + if (!bodyReceived) + break; + + if (!bodyEmpty) + context.Reader.AdvanceTo(buffer.GetPosition(currentPosition)); + + // Handle the request pipeline + await pipeline(context); + + // Respond + if(context.Response is not null && context.Response.IsActive()) + await WriteResponse(context); + + // Clear context for next request + context.Clear(); + + // Signal that there is something to flush + flush = true; + + state = State.StartLine; + + if (currentPosition == buffer.Length) // There is no more data, need to ReadAsync() + break; + } + } + // Slower path: multi-segment buffer or already partway through a request + // This handles cases where the request line or headers span multiple segments + else + { + var currentPosition = buffer.Start; + + while (true) + { + if (buffer.Length == 0 || isCompleted) + break; + + if (state != State.Body) + { + var requestReceived = + ExtractHeaderFromMultipleSegment(context, ref buffer, ref currentPosition, ref state); + + context.Reader.AdvanceTo(currentPosition, buffer.End); + + if (!requestReceived) + break; + + state = State.Body; + } + + //Try to get the body, if the full body isn't read, break + var bodyReceived = TryExtractBodyFromMultipleSegment(context, ref buffer, ref currentPosition, ref state, out var bodyExists); + + if (bodyExists) + { + if (!bodyReceived) + { + break; + } + + context.Reader.AdvanceTo(currentPosition, buffer.End); + } + + await pipeline(context); + + // Respond + if(context.Response is not null && context.Response.IsActive()) + await WriteResponse(context); + + context.Clear(); + flush = true; + + state = State.StartLine; + + if (buffer.Slice(currentPosition).IsEmpty) // There is no more data, need to ReadAsync() + break; + } + } + + if (flush) + await context.Writer.FlushAsync(); + } + } + + /// + /// Attempts to extract the HTTP request body from a single-segment buffer. + /// + /// The HTTP request context to populate with body data. + /// The input buffer containing the body. + /// + /// The current offset in the single segment span. + /// Updated to point just after the body if extraction succeeds. + /// + /// + /// Output flag indicating whether the request body was empty or absent. + /// true if no body is present, otherwise false. + /// + /// + /// true if the body was fully extracted or no body is required; + /// false if more data is needed from the transport. + /// + /// + /// Thrown if the Content-Length or chunked encoding format is invalid. + /// + [SkipLocalsInit] + private static bool TryExtractBodyFromSingleSegment( + TContext context, + ref ReadOnlySequence buffer, + ref int position, + out bool bodyEmpty) + { + bodyEmpty = true; + + // Content-Length header present + // TODO: Test if this works, it's odd that the position is not used by method caller + var contentLengthAvailable = context.Request.Headers.TryGetValue(ContentLength, out var contentLengthValue); + if (contentLengthAvailable) + { + var validContentLength = int.TryParse(contentLengthValue, out var contentLength); + if (contentLength <= 0 || !validContentLength) + { + return true; // Invalid Content-Length header + } + + bodyEmpty = false; + + var remainingBytes = buffer.FirstSpan.Length - position; + + if(remainingBytes < contentLength) + return false; // Not enough bytes yet + + context.Request.ContentLength = contentLength; + context.Request.Content = ArrayPool.Shared.Rent(contentLength); + buffer.FirstSpan.Slice(position, contentLength).CopyTo(context.Request.Content); + + position += contentLength; + + return true; + } + + // Transfer-Encoding header present + var transferEncodingAvailable = context.Request.Headers.TryGetValue(TransferEncoding, out var transferEncodingValue); + if (transferEncodingAvailable && transferEncodingValue.Equals("chunked", StringComparison.OrdinalIgnoreCase)) + { + var currentOffset = 0; + var bufferSpan = buffer.FirstSpan[position..]; + + var bodyEndIndex = bufferSpan.IndexOf("0\r\n\r\n"u8); + if (bodyEndIndex == -1) + return false; // Not enough bytes yet + + context.Request.Content = ArrayPool.Shared.Rent(bodyEndIndex); + + while (true) + { + var chunkSizeEnd = bufferSpan[currentOffset..].IndexOf(Crlf); + var validChunkSize = int.TryParse(bufferSpan[currentOffset..(currentOffset + chunkSizeEnd)], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var currentChunkSize); + if (!validChunkSize) + throw new InvalidOperationException("Invalid chunk size"); + + if (currentChunkSize == 0) // End of body detected + { + if (chunkSizeEnd + currentOffset != bodyEndIndex + 1) + throw new InvalidOperationException("Invalid chunked body termination"); + + position += 5; // Move past "0\r\n\r\n" + + bodyEmpty = false; + return true; + } + + bodyEmpty = false; + + var destinationSpan = context.Request.Content.AsSpan().Slice(currentOffset, currentChunkSize); + bufferSpan.Slice(chunkSizeEnd + 2, currentChunkSize).CopyTo(destinationSpan); + + context.Request.ContentLength += currentChunkSize; + currentOffset += chunkSizeEnd + 2 + currentChunkSize + 2; // Move past chunk size line, chunk data, and trailing CRLF + position += currentOffset; + } + } + + return true; + } + + + /// + /// Attempts to extract the HTTP request body from a multi-segment buffer. + /// + /// The HTTP request context to populate with body data. + /// The input buffer containing the body. + /// + /// The current position in the buffer. + /// Updated to point just after the body if extraction succeeds. + /// + /// Parser state, may be advanced after body extraction. + /// + /// + /// true if the body was fully extracted or no body is required; + /// false if more data is needed from the transport. + /// + /// + /// Thrown if the Content-Length or chunked encoding format is invalid. + /// + private static bool TryExtractBodyFromMultipleSegment( + TContext context, + ref ReadOnlySequence buffer, + ref SequencePosition position, + ref State state, + out bool bodyExists) + { + bodyExists = false; + + // Content-Length header present + var contentLengthAvailable = context.Request.Headers.TryGetValue(ContentLength, out var contentLengthValue); + // TODO: Test if this works, it's odd that the position is not used by method caller + if (contentLengthAvailable) + { + bodyExists = true; + + var validContentLength = int.TryParse(contentLengthValue, out var contentLength); + + if (!validContentLength || contentLength < 0) + return false; // Invalid Content-Length header + + if (buffer.Slice(position).Length < contentLength) + return false; // Not enough bytes yet + + context.Request.Content = ArrayPool.Shared.Rent(contentLength); + buffer.Slice(position, contentLength).CopyTo(context.Request.Content); + + context.Request.ContentLength = contentLength; + + position = buffer.GetPosition(contentLength, position); + + return true; + } + + var transferEncodingAvailable = context.Request.Headers.TryGetValue(TransferEncoding, out var transferEncodingValue); + if (transferEncodingAvailable && transferEncodingValue.Equals("chunked", StringComparison.OrdinalIgnoreCase)) + { + bodyExists = true; + + var reader = new SequenceReader(buffer.Slice(position)); + var currentOffset = context.Request.Content != null ? context.Request.ContentLength : 0; + + while (true) + { + if (!reader.TryReadTo(out ReadOnlySequence chunkSizeSequence, Crlf)) + return false; // Not enough bytes yet + + var chunkSizeSpan = chunkSizeSequence.ToSpan(); + if (!TryParseChunkSize(chunkSizeSpan, out var chunkSize)) + throw new InvalidOperationException("Invalid chunk size"); + + if (chunkSize == 0) // End of body detected + { + if (!reader.TryReadTo(out ReadOnlySequence _, Crlf)) + return false; // Not enough bytes yet + + position = reader.Position; + break; + } + + if (reader.Remaining < chunkSize + 2) // +2 for trailing CRLF + return false; // Not enough bytes yet + + if (context.Request.Content == null) + { + // Estimate a reasonable buffer size for the first chunk, will be re-rented if more chunks arrive + context.Request.Content = ArrayPool.Shared.Rent(chunkSize); + } + else if (context.Request.Content.Length < currentOffset + chunkSize) + { + // Grow the buffer if needed + var newBuffer = ArrayPool.Shared.Rent(currentOffset + chunkSize); + context.Request.Content.AsSpan(0, currentOffset).CopyTo(newBuffer); + ArrayPool.Shared.Return(context.Request.Content); + context.Request.Content = newBuffer; + } + + var destinationSpan = context.Request.Content.AsSpan(currentOffset, chunkSize); + if (!reader.TryCopyTo(destinationSpan)) + return false; // Not enough bytes yet + + currentOffset += chunkSize; + + // Advance past chunk data and trailing CRLF + reader.Advance(chunkSize); + if (!reader.TryReadTo(out ReadOnlySequence _, Crlf)) + return false; // Not enough bytes yet + } + + context.Request.ContentLength = currentOffset; + position = reader.Position; + } + + return true; + } + + // Helper for parsing chunk size (hex) + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryParseChunkSize(ReadOnlySpan span, out int value) + { + value = 0; + int i = 0; + for (; i < span.Length; i++) + { + byte b = span[i]; + if (b >= '0' && b <= '9') + value = (value << 4) + (b - '0'); + else if (b >= 'A' && b <= 'F') + value = (value << 4) + (b - 'A' + 10); + else if (b >= 'a' && b <= 'f') + value = (value << 4) + (b - 'a' + 10); + else + return false; + } + return i > 0; + } + + /// + /// Parses the request line and headers from a single-segment buffer. + /// + /// Target request context to populate. + /// The read-only buffer (single segment). + /// Current integer offset in the first segment; updated to point just after the header terminator. + /// if a complete header block was parsed; otherwise . + /// Thrown when the request line is malformed. + [SkipLocalsInit] + private static bool ExtractHeaderFromSingleSegment( + TContext context, + ref ReadOnlySequence buffer, + ref int position) + { + // Hot path, single segment buffer + var bufferSpan = buffer.FirstSpan[position..]; + var fullHeaderIndex = bufferSpan.IndexOf("\r\n\r\n"u8); + + if (fullHeaderIndex == -1) + return false; + + // Whole headers are present for the request + // Parse first header + + var lineEnd = bufferSpan.IndexOf("\r\n"u8); + var firstHeader = bufferSpan[..lineEnd]; + + var firstSpace = firstHeader.IndexOf(Space); + if (firstSpace == -1) + throw new InvalidOperationException("Invalid request line"); + + context.Request.HttpMethod = CachedData.PreCachedHttpMethods.GetOrAdd(firstHeader[..firstSpace]); + + var secondSpaceRelative = firstHeader[(firstSpace + 1)..].IndexOf(Space); + if (secondSpaceRelative == -1) + throw new InvalidOperationException("Invalid request line"); + + var secondSpace = firstSpace + secondSpaceRelative + 1; + var url = firstHeader[(firstSpace + 1)..secondSpace]; + var queryStart = url.IndexOf(Question); // (byte)'?' + + if (queryStart != -1) + { + // Route has params + context.Request.Route = CachedData.CachedRoutes.GetOrAdd(url[..queryStart]); + var querySpan = url[(queryStart + 1)..]; + var current = 0; + while (current < querySpan.Length) + { + var separator = querySpan[current..].IndexOf(QuerySeparator); // (byte)'&' + ReadOnlySpan pair; + + if (separator == -1) + { + pair = querySpan[current..]; + current = querySpan.Length; + } + else + { + pair = querySpan.Slice(current, separator); + current += separator + 1; + } + + var equalsIndex = pair.IndexOf(Equal); // (byte)'=' + if (equalsIndex == -1) + break; + + context.Request.QueryParameters? + .TryAdd(CachedData.CachedQueryKeys.GetOrAdd(pair[..equalsIndex]), + Encoders.Utf8Encoder.GetString(pair[(equalsIndex + 1)..])); + } + } + else + { + // Url is same as route + context.Request.Route = CachedData.CachedRoutes.GetOrAdd(url); + } + + // Parse remaining headers + var lineStart = 0; + while (true) + { + lineStart += lineEnd + 2; + + lineEnd = bufferSpan[lineStart..].IndexOf("\r\n"u8); + if (lineEnd == 0) + { + // All Headers read + break; + } + + var header = bufferSpan.Slice(lineStart, lineEnd); + var colonIndex = header.IndexOf(Colon); + + if (colonIndex == -1) + { + // Malformed header + continue; + } + + var headerKey = header[..colonIndex]; + var headerValue = header[(colonIndex + 2)..]; + + context.Request.Headers + .TryAdd(CachedData.PreCachedHeaderKeys.GetOrAdd(headerKey), CachedData.PreCachedHeaderValues.GetOrAdd(headerValue)); + } + + position += fullHeaderIndex + 4; + //context.Reader.AdvanceTo(buffer.GetPosition(position)); + + return true; + } + + /// + /// Parses the request line and headers from a multi-segment buffer using . + /// + /// Target request context to populate. + /// The read-only buffer (may span multiple segments). + /// The current sequence position; updated as bytes are consumed. + /// State machine indicating where to resume parsing. + /// if a complete header block was parsed; otherwise . + [SkipLocalsInit] + private static bool ExtractHeaderFromMultipleSegment( + TContext context, + ref ReadOnlySequence buffer, + ref SequencePosition position, + ref State state) + { + // Parse the complete headers, taking off from where it left off + var headerReader = new SequenceReader(buffer.Slice(position)); + + if (state == State.StartLine) + { + if (!TryParseRequestLine(ref headerReader, context.Request)) + return false; + } + + // Header route was read, update state + position = headerReader.Position; + state = State.Headers; + + // Parse remaining headers + while (true) + { + if (!headerReader.TryReadTo(out ReadOnlySequence headerLine, Crlf)) + return false; + + position = headerReader.Position; + + if (headerLine.Length == 0) + break; // Empty line indicates end of headers + + ParseHeaderLine(in headerLine, context.Request); + + if (headerReader.End) + return false; + + } + // All headers were read + //state = State.Body; + position = headerReader.Position; + return true; + } + + // ---- Parsing helpers ---- + /// + /// Parses the request line (method, URL, version) from the reader position and advances past CRLF. + /// + /// Sequence reader positioned at the start of the request line. + /// Target request object to populate. + /// if the full request line was parsed; otherwise . + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + [SkipLocalsInit] + private static bool TryParseRequestLine(ref SequenceReader reader, IExpressRequest request) + { + // Read method + if (!reader.TryReadTo(out ReadOnlySequence methodSequence, (byte)' ')) + return false; + + request.HttpMethod = CachedData.PreCachedHttpMethods.GetOrAdd(methodSequence.ToSpan()); + + // Read URL/path + if (!reader.TryReadTo(out ReadOnlySequence urlSequence, (byte)' ')) + return false; + + ParseUrl(in urlSequence, request); + + // Skip HTTP version (read to end of line) + return reader.TryReadTo(out ReadOnlySequence _, Crlf); + } + + /// + /// Parses the URL and optional query string into the request route and query dictionary. + /// + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + [SkipLocalsInit] + private static void ParseUrl(in ReadOnlySequence urlSequence, IExpressRequest request) + { + var urlSpan = urlSequence.ToSpan(); + var queryStart = urlSpan.IndexOf((byte)'?'); + + if (queryStart != -1) + { + // URL has query parameters + var routeSpan = urlSpan[..queryStart]; + request.Route = CachedData.CachedRoutes.GetOrAdd(routeSpan); + + // Parse query parameters + ParseQueryParameters(urlSpan[(queryStart + 1)..], request); + } + else + { + // Simple URL without query parameters + request.Route = CachedData.CachedRoutes.GetOrAdd(urlSpan); + } + } + + /// + /// Parses a query string in key=value&key2=value2 form into . + /// + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + [SkipLocalsInit] + private static void ParseQueryParameters(in ReadOnlySpan querySpan, IExpressRequest request) + { + var current = 0; + while (current < querySpan.Length) + { + var separator = querySpan[current..].IndexOf((byte)'&'); + ReadOnlySpan pair; + + if (separator == -1) + { + pair = querySpan[current..]; + current = querySpan.Length; + } + else + { + pair = querySpan.Slice(current, separator); + current += separator + 1; + } + + var equalsIndex = pair.IndexOf((byte)'='); + if (equalsIndex == -1) + continue; + + var key = CachedData.CachedQueryKeys.GetOrAdd(pair[..equalsIndex]); + var value = Encoding.UTF8.GetString(pair[(equalsIndex + 1)..]); + + request.QueryParameters?.TryAdd(key, value); + } + } + + /// + /// Parses a single header line and adds it to . + /// + /// A buffer containing a single header line with trailing CRLF removed. + /// Target request. + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + [SkipLocalsInit] + private static void ParseHeaderLine(in ReadOnlySequence headerLine, IExpressRequest request) + { + var headerSpan = headerLine.ToSpan(); + var colonIndex = headerSpan.IndexOf((byte)':'); + + if (colonIndex == -1) + return; // Malformed header, skip + + var headerKey = headerSpan[..colonIndex]; + + // Skip colon and optional whitespace + var valueStart = colonIndex + 1; + while (valueStart < headerSpan.Length && headerSpan[valueStart] == (byte)' ') + valueStart++; + + var headerValue = headerSpan[valueStart..]; + + request.Headers?.TryAdd( + CachedData.PreCachedHeaderKeys.GetOrAdd(headerKey), + CachedData.PreCachedHeaderValues.GetOrAdd(headerValue)); + } + + // ---- Constants & literals ---- + + /// CRLF delimiter used for line termination. + private static ReadOnlySpan Crlf => "\r\n"u8; + private static ReadOnlySpan CrlfCrlf => "\r\n\r\n"u8; + + private const string ContentLength = "Content-Length"; + private const string TransferEncoding = "Transfer-Encoding"; + + private const byte Space = 0x20; // ' ' + private const byte Question = 0x3F; // '?' + private const byte QuerySeparator = 0x26; // '&' + private const byte Equal = 0x3D; // '=' + private const byte Colon = 0x3A; // ':' + private const byte SemiColon = 0x3B; // ';' +} \ No newline at end of file diff --git a/Wired.IO/Handlers/Http11Rocket/WiredHttp11Rocket.Response.cs b/Wired.IO/Handlers/Http11Rocket/WiredHttp11Rocket.Response.cs new file mode 100644 index 0000000..ed156eb --- /dev/null +++ b/Wired.IO/Handlers/Http11Rocket/WiredHttp11Rocket.Response.cs @@ -0,0 +1,119 @@ +using System.Buffers; +using System.Buffers.Text; +using System.IO.Pipelines; +using System.Runtime.CompilerServices; +using Wired.IO.Handlers.Http11Express.Response; +using Wired.IO.Protocol.Response; +using Wired.IO.Utilities; + +namespace Wired.IO.Handlers.Http11Rocket; + +public partial class WiredHttp11Rocket +{ + private static ReadOnlySpan ServerHeaderName => "Server: "u8; + private static ReadOnlySpan ContentTypeHeader => "Content-Type: "u8; + private static ReadOnlySpan ContentLengthHeader => "Content-Length: "u8; + private static ReadOnlySpan ContentEncodingHeader => "Content-Encoding: "u8; + private static ReadOnlySpan TransferEncodingHeader => "Transfer-Encoding: "u8; + private static ReadOnlySpan TransferEncodingChunkedHeader => "Transfer-Encoding: chunked\r\n"u8; + private static ReadOnlySpan LastModifiedHeader => "Last-Modified: "u8; + private static ReadOnlySpan ExpiresHeader => "Expires: "u8; + private static ReadOnlySpan ConnectionHeader => "Connection: "u8; + private static ReadOnlySpan DateHeader => "Date: "u8; + + private static async Task WriteResponse(TContext context) + { + WriteStatusLine(context.Writer, context.Response!.Status); + + WriteHeaders(context); + + await WriteBody(context); + } + + [SkipLocalsInit] + private static async ValueTask WriteBody(TContext context) + { + if (context.Response!.ContentLengthStrategy is ContentLengthStrategy.Action) + { + context.Response.Handler(); + return; + } + else if (context.Response!.ContentLengthStrategy is ContentLengthStrategy.AsyncTask) + { + await context.Response.AsyncHandler(); + return; + } + + if (context.Response!.ContentLengthStrategy is ContentLengthStrategy.Utf8View) + { + context.Writer.Write(context.Response.Utf8Content.AsSpan()); + return; + } + + context.Response.Content?.Write(context.Writer); + } + + [SkipLocalsInit] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteStatusLine(PipeWriter writer, ResponseStatus statusCode) + => writer.Write(HttpStatusLines.Lines[(int)statusCode]); + + [SkipLocalsInit] + private static void WriteHeaders(TContext context) + { + var writer = context.Writer; + + writer.Write("Server: W\r\n"u8); + + if (!context.Response!.ContentType.IsEmpty) + { + writer.Write(ContentTypeHeader); + writer.Write(context.Response.ContentType.AsSpan()); + writer.Write("\r\n"u8); + } + + // If ContentLength is not zero, its length is known and is valid to use Content-Length header + if (context.Response.ContentLength is not null) + { + writer.Write(ContentLengthHeader); + + var buffer = writer.GetSpan(16); // 16 is enough for any int in UTF-8 + if (!Utf8Formatter.TryFormat((ulong)context.Response.ContentLength, buffer, out var written)) + throw new InvalidOperationException("Failed to format int"); + + writer.Advance(written); + + writer.Write("\r\n"u8); + } + else if (context.Response.ContentLengthStrategy is ContentLengthStrategy.Chunked or ContentLengthStrategy.Action) + { + writer.Write(TransferEncodingChunkedHeader); + } + + if (context.Response.Utf8Headers is not null) + { + foreach (var header in context.Response.Utf8Headers) + { + writer.Write(header.Key.AsSpan()); + writer.Write(": "u8); + writer.Write(header.Value.AsSpan()); + writer.Write("\r\n"u8); + } + } + + if (context.Response.Headers.Count > 0) + { + foreach (var header in context.Response.Headers) + { + writer.WriteString(header.Key); + writer.Write(": "u8); + writer.WriteString(header.Value); + writer.Write("\r\n"u8); + } + } + + // TODO: Add Modified and Expires headers + + writer.Write(DateHelper.HeaderBytes); + } +} \ No newline at end of file diff --git a/Wired.IO/Protocol/Handlers/IHttpHandler.cs b/Wired.IO/Protocol/Handlers/IHttpHandler.cs index ca5cc6e..7b0184d 100644 --- a/Wired.IO/Protocol/Handlers/IHttpHandler.cs +++ b/Wired.IO/Protocol/Handlers/IHttpHandler.cs @@ -1,32 +1,9 @@ -using System.Net.Sockets; -using Wired.IO.Protocol.Request; -using Wired.IO.Protocol.Response; - -namespace Wired.IO.Protocol.Handlers; +namespace Wired.IO.Protocol.Handlers; /// /// Defines a contract for handling client connections using a custom or HTTP-based protocol. /// -public interface IHttpHandler - where TContext : IBaseContext +public interface IHttpHandler { - /// - /// Processes a client connection and dispatches one or more protocol-compliant requests. - /// - /// - /// The representing the client connection. - /// - /// A delegate that executes the application's request-handling pipeline, typically consisting of middleware and endpoint logic. - /// - /// - /// A used to signal cancellation, such as during server shutdown. - /// - /// - /// A that represents the asynchronous operation of handling the client session. - /// - Task HandleClientAsync( - Socket inner, - Stream stream, - Func pipeline, - CancellationToken stoppingToken); + } \ No newline at end of file diff --git a/Wired.IO/Protocol/IBaseContext.cs b/Wired.IO/Protocol/IBaseContext.cs index 235f435..b573456 100644 --- a/Wired.IO/Protocol/IBaseContext.cs +++ b/Wired.IO/Protocol/IBaseContext.cs @@ -9,24 +9,6 @@ public interface IBaseContext : IDisposable where TRequest : IBaseRequest where TResponse : IBaseResponse { - /// - /// Gets or sets the used to read incoming data from the client connection. - /// - /// - /// This reader is typically bound to the network stream and used to consume HTTP request data - /// such as headers, body content, or raw bytes from the client. - /// - PipeReader Reader { get; set; } - - /// - /// Gets or sets the used to write outgoing data to the client connection. - /// - /// - /// This writer is used to serialize the HTTP response, including headers and body, and flush it - /// to the client. It can be wrapped with encoders like chunked or plain writers. - /// - PipeWriter Writer { get; set; } - /// /// Gets or sets the HTTP request for the current connection. /// This property contains all the details of the incoming HTTP request, diff --git a/Wired.IO/Protocol/Response/ResponseStatus.cs b/Wired.IO/Protocol/Response/ResponseStatus.cs index b817d17..8d514d5 100644 --- a/Wired.IO/Protocol/Response/ResponseStatus.cs +++ b/Wired.IO/Protocol/Response/ResponseStatus.cs @@ -1,6 +1,4 @@ -using Wired.IO.Http11Express; - -namespace Wired.IO.Protocol.Response; +namespace Wired.IO.Protocol.Response; public enum ResponseStatus { diff --git a/Wired.IO/Transport/ITransport.cs b/Wired.IO/Transport/ITransport.cs new file mode 100644 index 0000000..cf19d4c --- /dev/null +++ b/Wired.IO/Transport/ITransport.cs @@ -0,0 +1,31 @@ +using System.Net; +using System.Net.Security; +using Microsoft.Extensions.Logging; +using Wired.IO.Protocol; +using Wired.IO.Protocol.Handlers; +using Wired.IO.Protocol.Request; +using Wired.IO.Protocol.Response; + +namespace Wired.IO.Transport; + +public interface ITransport : IDisposable + where TContext : IBaseContext +{ + IPAddress IPAddress { get; set; } + + int Port { get; set; } + + int Backlog { get; set; } + + IHttpHandler HttpHandler { get; set; } + + ILogger? Logger { get; set; } + + bool TlsEnabled { get; set; } + + SslServerAuthenticationOptions SslServerAuthenticationOptions { get; set; } + + Func Pipeline { get; set; } + + Task ExecuteAsync(CancellationToken stoppingToken); +} \ No newline at end of file diff --git a/Wired.IO/Transport/NamedPipes/INamedPipesHttpHandler.cs b/Wired.IO/Transport/NamedPipes/INamedPipesHttpHandler.cs new file mode 100644 index 0000000..67300ff --- /dev/null +++ b/Wired.IO/Transport/NamedPipes/INamedPipesHttpHandler.cs @@ -0,0 +1,12 @@ +using Wired.IO.Protocol; +using Wired.IO.Protocol.Handlers; +using Wired.IO.Protocol.Request; +using Wired.IO.Protocol.Response; + +namespace Wired.IO.Transport.NamedPipes; + +public interface INamedPipesHttpHandler : IHttpHandler + where TContext : IBaseContext +{ + +} \ No newline at end of file diff --git a/Wired.IO/Transport/NamedPipes/NamedPipesTransport.cs b/Wired.IO/Transport/NamedPipes/NamedPipesTransport.cs new file mode 100644 index 0000000..8158beb --- /dev/null +++ b/Wired.IO/Transport/NamedPipes/NamedPipesTransport.cs @@ -0,0 +1,39 @@ +using System.Net; +using System.Net.Security; +using Microsoft.Extensions.Logging; +using Wired.IO.Protocol; +using Wired.IO.Protocol.Handlers; +using Wired.IO.Protocol.Request; +using Wired.IO.Protocol.Response; + +namespace Wired.IO.Transport.NamedPipes; + +public class NamedPipesTransport : ITransport + where TContext : IBaseContext +{ + public IPAddress IPAddress { get; set; } = null!; + + public int Port { get; set; } + + public int Backlog { get; set; } + + public IHttpHandler HttpHandler { get; set; } = null!; + + public ILogger? Logger { get; set; } + + public bool TlsEnabled { get; set; } + + public SslServerAuthenticationOptions SslServerAuthenticationOptions { get; set; } = null!; + + public Func Pipeline { get; set; } = null!; + + public Task ExecuteAsync(CancellationToken stoppingToken) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + // TODO release managed resources here + } +} \ No newline at end of file diff --git a/Wired.IO/Transport/Rocket/IRocketHttpHandler.cs b/Wired.IO/Transport/Rocket/IRocketHttpHandler.cs new file mode 100644 index 0000000..4b57dd4 --- /dev/null +++ b/Wired.IO/Transport/Rocket/IRocketHttpHandler.cs @@ -0,0 +1,16 @@ +using URocket.Connection; +using Wired.IO.Protocol; +using Wired.IO.Protocol.Handlers; +using Wired.IO.Protocol.Request; +using Wired.IO.Protocol.Response; + +namespace Wired.IO.Transport.Rocket; + +public interface IRocketHttpHandler : IHttpHandler + where TContext : IBaseContext +{ + Task HandleClientAsync( + Connection connection, + Func pipeline, + CancellationToken stoppingToken); +} \ No newline at end of file diff --git a/Wired.IO/Transport/Rocket/RocketTransport.cs b/Wired.IO/Transport/Rocket/RocketTransport.cs new file mode 100644 index 0000000..017e105 --- /dev/null +++ b/Wired.IO/Transport/Rocket/RocketTransport.cs @@ -0,0 +1,73 @@ +using System.Net; +using System.Net.Security; +using Microsoft.Extensions.Logging; +using URocket.Engine; +using URocket.Engine.Configs; +using Wired.IO.Protocol; +using Wired.IO.Protocol.Handlers; +using Wired.IO.Protocol.Request; +using Wired.IO.Protocol.Response; + +namespace Wired.IO.Transport.Rocket; + +public class RocketTransport : ITransport + where TContext : IBaseContext +{ + private Engine _engine = null!; + + private EngineOptions _options; + + public IPAddress IPAddress { get; set; } = null!; + + public int Port { get; set; } + + public int Backlog { get; set; } + + public IHttpHandler HttpHandler { get; set; } = null!; + + public ILogger? Logger { get; set; } + + public bool TlsEnabled { get; set; } // Not Supported yet by uRocket + + public SslServerAuthenticationOptions SslServerAuthenticationOptions { get; set; } = null!; // Not Supported yet by uRocket + + public Func Pipeline { get; set; } = null!; + + public RocketTransport(EngineOptions? options = null) + { + _options = options ?? new EngineOptions(); + } + + public async Task ExecuteAsync(CancellationToken stoppingToken) + { + CreateEngine(); + + while(_engine.ServerRunning) + { + var connection = await _engine.AcceptAsync(stoppingToken); + + if(connection == null) + continue; + + _ = ((IRocketHttpHandler)HttpHandler).HandleClientAsync(connection, Pipeline, stoppingToken); + } + } + + private void CreateEngine() + { + _engine= new Engine(new EngineOptions + { + Port = (ushort)Port, + Ip = "0.0.0.0", + Backlog = Backlog, + ReactorCount = 32 + }); + + _engine.Listen(); + } + + public void Dispose() + { + _engine.Stop(); + } +} \ No newline at end of file diff --git a/Wired.IO/Transport/Socket/ISocketHttpHandler.cs b/Wired.IO/Transport/Socket/ISocketHttpHandler.cs new file mode 100644 index 0000000..a31e117 --- /dev/null +++ b/Wired.IO/Transport/Socket/ISocketHttpHandler.cs @@ -0,0 +1,33 @@ +using Wired.IO.Protocol; +using Wired.IO.Protocol.Handlers; +using Wired.IO.Protocol.Request; +using Wired.IO.Protocol.Response; + +namespace Wired.IO.Transport.Socket; + +/// +/// Defines a contract for handling client connections using a custom or HTTP-based protocol. +/// +public interface ISocketHttpHandler : IHttpHandler + where TContext : IBaseContext +{ + /// + /// Processes a client connection and dispatches one or more protocol-compliant requests. + /// + /// + /// The representing the client connection. + /// + /// A delegate that executes the application's request-handling pipeline, typically consisting of middleware and endpoint logic. + /// + /// + /// A used to signal cancellation, such as during server shutdown. + /// + /// + /// A that represents the asynchronous operation of handling the client session. + /// + Task HandleClientAsync( + System.Net.Sockets.Socket inner, + Stream stream, + Func pipeline, + CancellationToken stoppingToken); +} \ No newline at end of file diff --git a/Wired.IO/Transport/Socket/SocketTransport.cs b/Wired.IO/Transport/Socket/SocketTransport.cs new file mode 100644 index 0000000..b78ed46 --- /dev/null +++ b/Wired.IO/Transport/Socket/SocketTransport.cs @@ -0,0 +1,276 @@ +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Security; +using System.Security.Authentication; +using Microsoft.Extensions.Logging; +using Wired.IO.Protocol; +using Wired.IO.Protocol.Handlers; +using Wired.IO.Protocol.Request; +using Wired.IO.Protocol.Response; +using Wired.IO.Utilities.MemoryBuffers; + +namespace Wired.IO.Transport.Socket; + +/// +/// Encapsulates the server's execution logic and abstracts the engine behavior (e.g., plain or TLS). +/// +public sealed class SocketTransport : ITransport + where TContext : IBaseContext +{ + private System.Net.Sockets.Socket? _socket; + + public IPAddress IPAddress { get; set; } = null!; + + public int Port { get; set; } + + public int Backlog { get; set; } + + public IHttpHandler HttpHandler { get; set; } = null!; + + public ILogger? Logger { get; set; } + + public bool TlsEnabled { get; set; } + + public Func Pipeline { get; set; } + + /// + /// Gets or sets the TLS configuration for the server. + /// Defaults to , indicating no TLS enabled unless explicitly configured. + /// + public SslServerAuthenticationOptions SslServerAuthenticationOptions { get; set; } = + new SslServerAuthenticationOptions + { + EnabledSslProtocols = SslProtocols.None + }; + + public SocketTransport() { } + + /// + /// Executes the engine logic using the provided cancellation token. + /// + /// Token used to signal cancellation of the server loop. + public async Task ExecuteAsync(CancellationToken stoppingToken) + { + CreateListeningSocket(); + if (TlsEnabled) + { + await RunAcceptLoopAsync(HandleTlsClientAsync, stoppingToken); + } + else + { + await RunAcceptLoopAsync(HandlePlainClientAsync, stoppingToken); + } + } + + /// + /// Initializes the TCP listening socket and binds it to the configured IP address and port. + /// Uses dual-stack IPv6 socket with IPv4 support enabled. + /// + private void CreateListeningSocket() + { + //IPv4 + //_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + + //IPv6 DualStack + _socket = new System.Net.Sockets.Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp); + _socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false); + _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); + _socket.NoDelay = true; + + _socket.Bind(new IPEndPoint(IPAddress, Port)); + _socket.Listen(Backlog); + } + + /// + /// Launches parallel accept loops, one per processor core, to maximize concurrent connection handling throughput. + /// + /// A delegate that handles a connected . + /// A token used to cancel the accept loops. + /// A task that completes when all accept loops have terminated. + private async Task RunAcceptLoopAsync(Func clientHandler, CancellationToken stoppingToken) + { + // Multiple concurrent accept loops for maximum throughput + //var acceptTasks = new Task[Environment.ProcessorCount/2]; + var acceptTasks = new Task[4]; + for (var i = 0; i < acceptTasks.Length; i++) + { + acceptTasks[i] = AcceptLoopAsync(clientHandler, stoppingToken); + } + + await Task.WhenAll(acceptTasks); + } + + /// + /// Continuously accepts incoming connections and dispatches them to the provided client handler delegate. + /// Each accepted client is processed in the background to avoid blocking the accept loop. + /// + /// A delegate that handles the accepted . + /// A token that cancels the accept loop when requested. + /// A task that completes when the loop is canceled or terminated due to error. + private async Task AcceptLoopAsync(Func clientHandler, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + var client = await _socket!.AcceptAsync(cancellationToken); + + client.NoDelay = true; // Disable Nagle's algorithm for low-latency communication + + // Handle client without blocking accept loop + _ = HandleClientAsync(client, clientHandler, cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + break; + } + catch (Exception ex) + { + Console.WriteLine($"Accept error: {ex.Message}"); + await Task.Delay(100, cancellationToken); // Brief delay on error + } + } + } + + /// + /// Invokes the specified handler for a connected client socket, ensuring proper error logging and disposal. + /// + /// The connected to handle. + /// The delegate responsible for processing the client connection. + /// The token used to cancel the operation. + /// A task that completes when client handling is finished. + private async Task HandleClientAsync(System.Net.Sockets.Socket client, Func clientHandler, CancellationToken stoppingToken) + { + try + { + await clientHandler(client, stoppingToken); + } + catch (Exception ex) + { + Logger?.LogTrace("Client could not be handled: {Exception}", ex); + } + finally + { + client.Dispose(); + } + } + + // ------------------------------------- + // ------------------------------------- + + /// + /// Handles an incoming plain (non-TLS) TCP client connection. + /// Wraps the client socket in a and delegates request handling to . + /// + /// The connected TCP . + /// The used to cancel the operation. + private async Task HandlePlainClientAsync(System.Net.Sockets.Socket client, CancellationToken stoppingToken) + { + await using var networkStream = new PoolBufferedStream(new NetworkStream(client, ownsSocket: true), 65 * 1024); + await ((ISocketHttpHandler)HttpHandler).HandleClientAsync( + client, + networkStream, + Pipeline, + stoppingToken); + } + + /// + /// Handles an incoming TLS (SSL) client connection. + /// Performs a TLS handshake using the configured and delegates secure stream handling to . + /// + /// The connected TCP . + /// The used to cancel the operation. + /// + /// Thrown if is not set. + /// + private async Task HandleTlsClientAsync(System.Net.Sockets.Socket client, CancellationToken stoppingToken) + { + if (SslServerAuthenticationOptions.ServerCertificate is null) + { + throw new SecurityException("SecurityOptions.ServerCertificate is null"); + } + + // Create and configure the SSL stream for the client connection + // + // TODO: Investigate leaveInnerStreamOpen flag + await using var stream = new PoolBufferedStream(new NetworkStream(client, ownsSocket: true), 65 * 1024); + await using var sslStream = new SslStream(stream, + false, + SslServerAuthenticationOptions.RemoteCertificateValidationCallback); + + try + { + // Perform the TLS handshake + // + await sslStream.AuthenticateAsServerAsync(SslServerAuthenticationOptions, stoppingToken); + } + catch (Exception ex) when (HandleTlsException(ex)) + { + // Unified handling of TLS failure message. + // + await SendTlsFailureMessageAsync(client); + } + + // Handle the client connection securely + // + await ((ISocketHttpHandler)HttpHandler).HandleClientAsync( + client, + sslStream, + Pipeline, + stoppingToken); + } + + /// + /// Handles and logs exceptions that occur during a TLS handshake. + /// Always returns true to allow usage in a catch when filter. + /// + /// The exception that was thrown during TLS negotiation. + /// true, indicating the exception should be handled. + private bool HandleTlsException(Exception ex) + { + switch (ex) + { + case AuthenticationException authEx: + Logger?.LogTrace("TLS Handshake failed due to authentication error: {Message}", authEx.Message); + break; + case InvalidOperationException invalidOpEx: + Logger?.LogTrace("TLS Handshake failed due to socket error: {Message}", invalidOpEx.Message); + break; + default: + Logger?.LogTrace("Unexpected error during TLS Handshake: {Message}", ex.Message); + break; + } + + return true; // Ensure the exception is always caught + } + + /// + /// Sends a simple plaintext error message to the client if the TLS handshake fails, + /// then closes the underlying socket. + /// + /// The TCP to notify and close. + /// A task representing the asynchronous operation. + private static async Task SendTlsFailureMessageAsync(System.Net.Sockets.Socket client) + { + var messageBytes = "TLS Handshake failed. Closing connection."u8.ToArray(); + + try + { + await client.SendAsync(messageBytes, SocketFlags.None); + } + catch + { + // Ignore errors while sending failure response + } + finally + { + client.Close(); + } + } + + public void Dispose() + { + _socket?.Dispose(); + } +} \ No newline at end of file diff --git a/Wired.IO/Common/Extensions/SpanExtensions.cs b/Wired.IO/Utilities/Extensions/SpanExtensions.cs similarity index 94% rename from Wired.IO/Common/Extensions/SpanExtensions.cs rename to Wired.IO/Utilities/Extensions/SpanExtensions.cs index d98584c..e1389c2 100644 --- a/Wired.IO/Common/Extensions/SpanExtensions.cs +++ b/Wired.IO/Utilities/Extensions/SpanExtensions.cs @@ -1,7 +1,4 @@ -using System.Text; -using Wired.IO.Utilities; - -namespace Wired.IO.Common.Extensions; +namespace Wired.IO.Utilities.Extensions; /// /// Provides extension methods for efficiently writing UTF-8 and ASCII text into a buffer. diff --git a/Wired.IO/MemoryBuffers/PoolBufferedStream.cs b/Wired.IO/Utilities/MemoryBuffers/PoolBufferedStream.cs similarity index 99% rename from Wired.IO/MemoryBuffers/PoolBufferedStream.cs rename to Wired.IO/Utilities/MemoryBuffers/PoolBufferedStream.cs index 6675ca0..1c0ba82 100644 --- a/Wired.IO/MemoryBuffers/PoolBufferedStream.cs +++ b/Wired.IO/Utilities/MemoryBuffers/PoolBufferedStream.cs @@ -1,6 +1,6 @@ using System.Buffers; -namespace Wired.IO.MemoryBuffers; +namespace Wired.IO.Utilities.MemoryBuffers; /// /// An output stream using a pooled array to buffer small writes. diff --git a/Wired.IO/MemoryBuffers/PooledMemoryOwner.cs b/Wired.IO/Utilities/MemoryBuffers/PooledMemoryOwner.cs similarity index 98% rename from Wired.IO/MemoryBuffers/PooledMemoryOwner.cs rename to Wired.IO/Utilities/MemoryBuffers/PooledMemoryOwner.cs index 1c024b7..005d5ca 100644 --- a/Wired.IO/MemoryBuffers/PooledMemoryOwner.cs +++ b/Wired.IO/Utilities/MemoryBuffers/PooledMemoryOwner.cs @@ -1,6 +1,6 @@ using System.Buffers; -namespace Wired.IO.MemoryBuffers; +namespace Wired.IO.Utilities.MemoryBuffers; /// /// A custom implementation of that uses an diff --git a/Wired.IO/Wired.IO.csproj b/Wired.IO/Wired.IO.csproj index 6d22fd0..14b316b 100644 --- a/Wired.IO/Wired.IO.csproj +++ b/Wired.IO/Wired.IO.csproj @@ -41,11 +41,12 @@ - - - - - + + + + + +