diff --git a/Engine/Internal/Infrastructure/Endpoints/EndPoint.cs b/Engine/Internal/Infrastructure/Endpoints/EndPoint.cs index 59f467999..287c78c8b 100644 --- a/Engine/Internal/Infrastructure/Endpoints/EndPoint.cs +++ b/Engine/Internal/Infrastructure/Endpoints/EndPoint.cs @@ -5,6 +5,7 @@ using GenHTTP.Api.Infrastructure; using GenHTTP.Engine.Internal.Protocol; +using GenHTTP.Engine.Internal.Utilities; using GenHTTP.Engine.Shared.Infrastructure; namespace GenHTTP.Engine.Internal.Infrastructure.Endpoints; @@ -98,7 +99,7 @@ private void Handle(Socket client) protected abstract ValueTask Accept(Socket client); - protected ValueTask Handle(Socket client, Stream inputStream, X509Certificate? clientCertificate = null) + protected ValueTask Handle(Socket client, PoolBufferedStream inputStream, X509Certificate? clientCertificate = null) { client.NoDelay = true; diff --git a/Engine/Internal/Protocol/ChunkedStream.cs b/Engine/Internal/Protocol/ChunkedStream.cs index 8a5ffd887..1fd3d7e78 100644 --- a/Engine/Internal/Protocol/ChunkedStream.cs +++ b/Engine/Internal/Protocol/ChunkedStream.cs @@ -1,4 +1,4 @@ -using GenHTTP.Modules.IO.Streaming; +using GenHTTP.Engine.Internal.Utilities; namespace GenHTTP.Engine.Internal.Protocol; @@ -11,18 +11,8 @@ namespace GenHTTP.Engine.Internal.Protocol; /// soon as there is no known content length. To avoid this overhead, /// specify the length of your content whenever possible. /// -public sealed class ChunkedStream : Stream +public sealed class ChunkedStream(PoolBufferedStream target) : Stream { - private static readonly string NL = "\r\n"; - - #region Initialization - - public ChunkedStream(Stream target) - { - Target = target; - } - - #endregion #region Get-/Setters @@ -36,7 +26,7 @@ public ChunkedStream(Stream target) public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } - private Stream Target { get; } + private Stream Target { get; } = target; #endregion @@ -59,7 +49,7 @@ public override void Write(byte[] buffer, int offset, int count) Target.Write(buffer, offset, count); - NL.Write(Target); + Target.Write("\r\n"u8); } } @@ -67,11 +57,11 @@ public override async Task WriteAsync(byte[] buffer, int offset, int count, Canc { if (count > 0) { - await WriteAsync(count); + Write(count); await Target.WriteAsync(buffer.AsMemory(offset, count), cancellationToken); - await WriteAsync(NL); + Target.Write("\r\n"u8); } } @@ -79,17 +69,17 @@ public override async ValueTask WriteAsync(ReadOnlyMemory buffer, Cancella { if (!buffer.IsEmpty) { - await WriteAsync(buffer.Length); + Write(buffer.Length); await Target.WriteAsync(buffer, cancellationToken); - await WriteAsync(NL); + Target.Write("\r\n"u8); } } - public async ValueTask FinishAsync() + public void Finish() { - await WriteAsync("0\r\n\r\n"); + Target.Write("0\r\n\r\n"u8); } public override void Flush() @@ -99,11 +89,22 @@ public override void Flush() public override Task FlushAsync(CancellationToken cancellationToken) => Target.FlushAsync(cancellationToken); - private void Write(int value) => $"{value:X}\r\n".Write(Target); + private void Write(int value) + { + Span buffer = stackalloc byte[8 + 2]; - private ValueTask WriteAsync(string text) => text.WriteAsync(Target); + if (value.TryFormat(buffer, out var written, "X")) + { + buffer[written++] = (byte)'\r'; + buffer[written++] = (byte)'\n'; - private ValueTask WriteAsync(int value) => $"{value:X}\r\n".WriteAsync(Target); + Target.Write(buffer[..written]); + } + else + { + throw new InvalidOperationException("Failed to format chunk size"); + } + } #endregion diff --git a/Engine/Internal/Protocol/ClientHandler.cs b/Engine/Internal/Protocol/ClientHandler.cs index b8174de2d..ccab78c74 100644 --- a/Engine/Internal/Protocol/ClientHandler.cs +++ b/Engine/Internal/Protocol/ClientHandler.cs @@ -7,6 +7,7 @@ using GenHTTP.Api.Protocol; using GenHTTP.Engine.Internal.Protocol.Parser; +using GenHTTP.Engine.Internal.Utilities; using GenHTTP.Engine.Shared.Infrastructure; using GenHTTP.Engine.Shared.Types; @@ -38,7 +39,7 @@ internal sealed class ClientHandler internal X509Certificate? ClientCertificate { get; set; } - internal Stream Stream { get; } + internal PoolBufferedStream Stream { get; } private bool? KeepAlive { get; set; } @@ -48,7 +49,7 @@ internal sealed class ClientHandler #region Initialization - internal ClientHandler(Socket socket, Stream stream, X509Certificate? clientCertificate, IServer server, IEndPoint endPoint, NetworkConfiguration config) + internal ClientHandler(Socket socket, PoolBufferedStream stream, X509Certificate? clientCertificate, IServer server, IEndPoint endPoint, NetworkConfiguration config) { Server = server; EndPoint = endPoint; diff --git a/Engine/Internal/Protocol/ResponseHandler.cs b/Engine/Internal/Protocol/ResponseHandler.cs index f4718a5b1..4efae0b3c 100644 --- a/Engine/Internal/Protocol/ResponseHandler.cs +++ b/Engine/Internal/Protocol/ResponseHandler.cs @@ -1,6 +1,4 @@ -using System.Buffers; -using System.Net.Sockets; -using System.Text; +using System.Net.Sockets; using GenHTTP.Api.Infrastructure; using GenHTTP.Api.Protocol; @@ -12,13 +10,6 @@ namespace GenHTTP.Engine.Internal.Protocol; internal sealed class ResponseHandler { - private const string ServerHeader = "Server"; - - private const string NL = "\r\n"; - - private static readonly Encoding Ascii = Encoding.ASCII; - - private static readonly ArrayPool Pool = ArrayPool.Shared; #region Get-/Setters @@ -26,7 +17,7 @@ internal sealed class ResponseHandler private Socket Socket { get; } - private Stream OutputStream { get; } + private PoolBufferedStream Output { get; } private NetworkConfiguration Configuration { get; } @@ -34,12 +25,12 @@ internal sealed class ResponseHandler #region Initialization - internal ResponseHandler(IServer server, Socket socket, Stream outputStream, NetworkConfiguration configuration) + internal ResponseHandler(IServer server, Socket socket, PoolBufferedStream output, NetworkConfiguration configuration) { Server = server; Socket = socket; - OutputStream = outputStream; + Output = output; Configuration = configuration; } @@ -52,11 +43,11 @@ internal async ValueTask Handle(IRequest? request, IResponse response, Htt { try { - await WriteStatus(request, response); + WriteStatus(request, response); - await WriteHeader(response, version, keepAlive); + WriteHeader(response, version, keepAlive); - await Write(NL); + Output.Write("\r\n"u8); if (ShouldSendBody(request, response)) { @@ -69,7 +60,7 @@ internal async ValueTask Handle(IRequest? request, IResponse response, Htt // otherwise save flushes for improved performance when pipelining if (!dataRemaining && connected) { - await OutputStream.FlushAsync(); + await Output.FlushAsync(); } if (request != null) @@ -92,84 +83,104 @@ private static bool ShouldSendBody(IRequest? request, IResponse response) => (re response.ContentType is not null || response.ContentEncoding is not null ); - private ValueTask WriteStatus(IRequest? request, IResponse response) + private void WriteStatus(IRequest? request, IResponse response) { - var version = request?.ProtocolType == HttpProtocol.Http11 ? "1.1" : "1.0"; + Output.Write((request?.ProtocolType == HttpProtocol.Http11) ? "HTTP/1.1 "u8 : "HTTP/1.0 "u8); + Output.Write(response.Status.RawStatus); + Output.Write(" "u8); - return Write("HTTP/", version, " ", NumberStringCache.Convert(response.Status.RawStatus), " ", response.Status.Phrase, NL); + Output.Write(response.Status.Phrase); + + Output.Write("\r\n"u8); } - private async ValueTask WriteHeader(IResponse response, HttpProtocol version, bool keepAlive) + private void WriteHeader(IResponse response, HttpProtocol version, bool keepAlive) { - if (response.Headers.TryGetValue(ServerHeader, out var server)) + if (response.Headers.TryGetValue("Server", out var server)) { - await WriteHeaderLine(ServerHeader, server); + Output.Write("Server: "u8); + Output.Write(server); + Output.Write("\r\n"u8); } else { - await Write("Server: GenHTTP/", Server.Version, NL); + Output.Write("Server: "u8); + Output.Write(Server.Version); + Output.Write("\r\n"u8); } - await WriteHeaderLine("Date", DateHeader.GetValue()); + Output.Write("Date: "u8); + Output.Write(DateHeader.GetValue()); + Output.Write("\r\n"u8); if (version == HttpProtocol.Http10) { - await WriteHeaderLine("Connection", keepAlive ? "Keep-Alive" : "Close"); + Output.Write(keepAlive ? "Connection: Keep-Alive\r\n"u8 : "Connection: Close\r\n"u8); } else if (!keepAlive) { // HTTP/1.1 connections are persistent by default so we do not need to send a Keep-Alive header - await WriteHeaderLine("Connection", "Close"); + Output.Write("Connection: Close\r\n"u8); } if (response.ContentType is not null) { + Output.Write("Content-Type: "u8); + Output.Write(response.ContentType.RawType); + if (response.ContentType.Charset is not null) { - await Write("Content-Type: ", response.ContentType.RawType, "; charset=", response.ContentType.Charset, NL); - } - else - { - await WriteHeaderLine("Content-Type", response.ContentType.RawType); + Output.Write("; charset="u8); + Output.Write(response.ContentType.Charset); } + + Output.Write("\r\n"u8); } if (response.ContentEncoding is not null) { - await WriteHeaderLine("Content-Encoding", response.ContentEncoding!); + Output.Write("Content-Encoding: "u8); + Output.Write(response.ContentEncoding!); + Output.Write("\r\n"u8); } if (response.ContentLength is not null) { - await WriteHeaderLine("Content-Length", NumberStringCache.Convert(response.ContentLength.Value)); + Output.Write("Content-Length: "u8); + Output.Write(response.ContentLength.Value); + Output.Write("\r\n"u8); } else { - if (response.Content is not null) - { - await WriteHeaderLine("Transfer-Encoding", "chunked"); - } - else - { - await WriteHeaderLine("Content-Length", "0"); - } + Output.Write(response.Content is not null ? "Transfer-Encoding: chunked\r\n"u8 : "Content-Length: 0\r\n"u8); } if (response.Modified is not null) { - await WriteHeaderLine("Last-Modified", (DateTime)response.Modified); + Output.Write("Last-Modified: "u8); + Output.Write(response.Modified.Value); + Output.Write("\r\n"u8); } if (response.Expires is not null) { - await WriteHeaderLine("Expires", (DateTime)response.Expires); + Output.Write("Expires: "u8); + Output.Write(response.Expires.Value); + Output.Write("\r\n"u8); } + var serverSpan = "Server".AsSpan(); + foreach (var header in response.Headers) { - if (!header.Key.Equals(ServerHeader, StringComparison.OrdinalIgnoreCase)) + var keySpan = header.Key.AsSpan(); + + if (!keySpan.Equals(serverSpan, StringComparison.OrdinalIgnoreCase)) { - await WriteHeaderLine(header.Key, header.Value); + Output.Write(header.Key); + Output.Write(": "u8); + Output.Write(header.Value); + Output.Write("\r\n"u8); } } @@ -177,7 +188,7 @@ private async ValueTask WriteHeader(IResponse response, HttpProtocol version, bo { foreach (var cookie in response.Cookies) { - await WriteCookie(cookie.Value); + WriteCookie(cookie.Value); } } } @@ -188,15 +199,15 @@ private async ValueTask WriteBody(IResponse response) { if (response.ContentLength is null) { - await using var chunked = new ChunkedStream(OutputStream); + await using var chunked = new ChunkedStream(Output); await response.Content.WriteAsync(chunked, Configuration.TransferBufferSize); - await chunked.FinishAsync(); + chunked.Finish(); } else { - await response.Content.WriteAsync(OutputStream, Configuration.TransferBufferSize); + await response.Content.WriteAsync(Output, Configuration.TransferBufferSize); } } } @@ -205,80 +216,23 @@ private async ValueTask WriteBody(IResponse response) #region Helpers - private ValueTask WriteHeaderLine(string key, string value) => Write(key, ": ", value, NL); - - private ValueTask WriteHeaderLine(string key, DateTime value) => WriteHeaderLine(key, value.ToUniversalTime().ToString("r")); - - private async ValueTask WriteCookie(Cookie cookie) + private void WriteCookie(Cookie cookie) { - await Write("Set-Cookie: ", cookie.Name, "=", cookie.Value); + Output.Write("Set-Cookie: "u8); + Output.Write(cookie.Name); + Output.Write("="u8); + Output.Write(cookie.Value); if (cookie.MaxAge is not null) { - await Write("; Max-Age=", NumberStringCache.Convert(cookie.MaxAge.Value)); + Output.Write("; Max-Age="u8); + Output.Write(cookie.MaxAge.Value); } - await Write("; Path=/", NL); - } - - /// - /// Writes the given parts to the output stream. - /// - /// - /// Reduces the number of writes to the output stream by collecting - /// data to be written. Cannot use params keyword because it allocates - /// an array. - /// - private async ValueTask Write(string part1, string? part2 = null, string? part3 = null, - string? part4 = null, string? part5 = null, string? part6 = null, string? part7 = null) - { - var length = part1.Length + (part2?.Length ?? 0) + (part3?.Length ?? 0) + (part4?.Length ?? 0) - + (part5?.Length ?? 0) + (part6?.Length ?? 0) + (part7?.Length ?? 0); - - var buffer = Pool.Rent(length); - - try - { - var index = Ascii.GetBytes(part1, 0, part1.Length, buffer, 0); - - if (part2 is not null) - { - index += Ascii.GetBytes(part2, 0, part2.Length, buffer, index); - } - - if (part3 is not null) - { - index += Ascii.GetBytes(part3, 0, part3.Length, buffer, index); - } - - if (part4 is not null) - { - index += Ascii.GetBytes(part4, 0, part4.Length, buffer, index); - } - - if (part5 is not null) - { - index += Ascii.GetBytes(part5, 0, part5.Length, buffer, index); - } - - if (part6 is not null) - { - index += Ascii.GetBytes(part6, 0, part6.Length, buffer, index); - } - - if (part7 is not null) - { - Ascii.GetBytes(part7, 0, part7.Length, buffer, index); - } - - await OutputStream.WriteAsync(buffer.AsMemory(0, length)); - } - finally - { - Pool.Return(buffer); - } + Output.Write("; Path=/\r\n"u8); } #endregion } + diff --git a/Engine/Internal/Protocol/SocketExtensions.cs b/Engine/Internal/Protocol/SocketExtensions.cs index dd07727d9..d72627de4 100644 --- a/Engine/Internal/Protocol/SocketExtensions.cs +++ b/Engine/Internal/Protocol/SocketExtensions.cs @@ -7,4 +7,5 @@ internal static class SocketExtensions { public static IPAddress? GetAddress(this Socket socket) => (socket.RemoteEndPoint as IPEndPoint)?.Address; + } diff --git a/Engine/Internal/Protocol/StreamExtensions.cs b/Engine/Internal/Protocol/StreamExtensions.cs new file mode 100644 index 000000000..0ea96d78f --- /dev/null +++ b/Engine/Internal/Protocol/StreamExtensions.cs @@ -0,0 +1,75 @@ +using System.Runtime.CompilerServices; + +using GenHTTP.Engine.Internal.Utilities; + +namespace GenHTTP.Engine.Internal.Protocol; + +internal static class StreamExtensions +{ + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Write(this PoolBufferedStream stream, string value) + { + Span buffer = stackalloc byte[value.Length]; + + for (var i = 0; i < value.Length; i++) + { + buffer[i] = (byte)value[i]; + } + + stream.Write(buffer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Write(this PoolBufferedStream stream, long number) + { + Span buffer = stackalloc byte[20]; + + if (number.TryFormat(buffer, out var written)) + { + stream.Write(buffer[..written]); + } + else + { + throw new InvalidOperationException("Unable to write number to stream"); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Write(this PoolBufferedStream stream, ulong number) + { + Span buffer = stackalloc byte[20]; + + if (number.TryFormat(buffer, out var written)) + { + stream.Write(buffer[..written]); + } + else + { + throw new InvalidOperationException("Unable to write number to stream"); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Write(this PoolBufferedStream stream, DateTime time) + { + Span charBuffer = stackalloc char[29]; // RFC1123 format is 29 chars + + if (time.ToUniversalTime().TryFormat(charBuffer, out var written, "r")) + { + Span byteBuffer = stackalloc byte[written]; + + for (var i = 0; i < written; i++) + { + byteBuffer[i] = (byte)charBuffer[i]; + } + + stream.Write(byteBuffer); + } + else + { + throw new InvalidOperationException("Unable to write date time to stream"); + } + } + +} diff --git a/Engine/Internal/Utilities/NumberStringCache.cs b/Engine/Internal/Utilities/NumberStringCache.cs deleted file mode 100644 index 11f115da4..000000000 --- a/Engine/Internal/Utilities/NumberStringCache.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace GenHTTP.Engine.Internal.Utilities; - -/// -/// Caches the string representation of small numbers, -/// reducing the amount of string allocations needed -/// by the engine when writing HTTP responses. -/// -public static class NumberStringCache -{ - private const int Limit = 1024; - - private static readonly Dictionary Cache = new - ( - Enumerable.Range(0, Limit + 1).Select(i => new KeyValuePair((ulong)i, $"{i}")) - ); - - #region Functionality - - public static string Convert(int number) - { - if (number < 0) - { - throw new ArgumentOutOfRangeException(nameof(number), "Only positive numbers are supported"); - } - - return Convert((ulong)number); - } - - public static string Convert(ulong number) => number <= Limit ? Cache[number] : $"{number}"; - - #endregion - -} diff --git a/Engine/Internal/Utilities/PoolBufferedStream.cs b/Engine/Internal/Utilities/PoolBufferedStream.cs index 9805af589..72be8d141 100644 --- a/Engine/Internal/Utilities/PoolBufferedStream.cs +++ b/Engine/Internal/Utilities/PoolBufferedStream.cs @@ -1,4 +1,5 @@ using System.Buffers; +using System.Runtime.CompilerServices; namespace GenHTTP.Engine.Internal.Utilities; @@ -113,6 +114,33 @@ public override void Write(byte[] buffer, int offset, int count) } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Write(ReadOnlySpan buffer) + { + var toWrite = buffer.Length; + + if (toWrite <= Buffer.Length - Current) + { + buffer.CopyTo(Buffer.AsSpan(Current)); + Current += toWrite; + return; + } + + if (Current > 0) + { + WriteBuffer(); + } + + if (toWrite > Buffer.Length) + { + Stream.Write(buffer); + return; + } + + buffer.CopyTo(Buffer); + Current = toWrite; + } + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { await WriteAsync(buffer.AsMemory(offset, count - offset), cancellationToken); diff --git a/Modules/IO/Streaming/StreamExtensions.cs b/Modules/IO/Streaming/StreamExtensions.cs index 66a151a3e..579515113 100644 --- a/Modules/IO/Streaming/StreamExtensions.cs +++ b/Modules/IO/Streaming/StreamExtensions.cs @@ -1,5 +1,4 @@ using System.Buffers; -using System.Text; namespace GenHTTP.Modules.IO.Streaming; @@ -7,10 +6,6 @@ public static class StreamExtensions { private static readonly ArrayPool Pool = ArrayPool.Shared; - private static readonly Encoding Utf8 = Encoding.UTF8; - - private static readonly Encoder Encoder = Utf8.GetEncoder(); - public static async ValueTask CopyPooledAsync(this Stream source, Stream target, uint bufferSize) { if (source.CanSeek && source.Position != 0) @@ -43,42 +38,6 @@ public static async ValueTask CopyPooledAsync(this Stream source, Stream target, } } - public static async ValueTask WriteAsync(this string content, Stream target) - { - var bytes = Encoder.GetByteCount(content, false); - - var buffer = Pool.Rent(bytes); - - try - { - Encoder.GetBytes(content.AsSpan(), buffer.AsSpan(), true); - - await target.WriteAsync(buffer.AsMemory(0, bytes)); - } - finally - { - Pool.Return(buffer); - } - } - - public static void Write(this string content, Stream target) - { - var length = Encoder.GetByteCount(content, false); - - var buffer = Pool.Rent(length); - - try - { - Encoder.GetBytes(content.AsSpan(), buffer.AsSpan(), true); - - target.Write(buffer, 0, length); - } - finally - { - Pool.Return(buffer); - } - } - /// /// Efficiently calculates the checksum of the stream, beginning /// from the current position. Resets the position to the previous @@ -130,4 +89,5 @@ public static void Write(this string content, Stream target) return null; } + }