Home

Awesome

NetworkToolkit

This project contains networking primitives for use with .NET. It can be obtained from NuGet. Nuget

This is a bit of a prototyping playground right now and APIs will not be perfectly stable.

HTTP Primitives

NetworkToolkit provides a lower-level HTTP client that prioritizes performance and flexibility. Once warmed up, it processes requests using zero allocations.

Performance

In the great majority of cases, HttpClient will be fast enough. Generally I/O will dominate a request's total time. However, if peak perf is needed, this library will be faster, especially as the number of headers in the request/response increase.

Benchmarks are incomplete, so this should not be viewed as an exhaustive comparison:

MethodRequestHeadersResponseBytesMeanErrorStdDevMedianRatioRatioSD
PrimitiveMinimalMinimal10.806 us0.3304 us0.9741 us10.985 us0.510.05
PrimitivePreparedMinimalMinimal9.278 us0.2091 us0.6066 us9.298 us0.440.04
SocketsHandlerMinimalMinimal21.323 us0.4380 us1.2136 us21.442 us1.000.00
PrimitiveMinimalStackOverflow13.665 us0.6509 us1.9089 us13.187 us0.160.02
PrimitivePreparedMinimalStackOverflow14.108 us0.6328 us1.8559 us13.432 us0.170.03
SocketsHandlerMinimalStackOverflow86.356 us1.7149 us4.2707 us87.476 us1.000.00
PrimitiveNormalMinimal11.053 us0.2498 us0.7366 us11.149 us0.320.03
PrimitivePreparedNormalMinimal10.636 us0.2867 us0.8455 us10.701 us0.310.03
SocketsHandlerNormalMinimal34.775 us0.6940 us1.9231 us35.172 us1.000.00
PrimitiveNormalStackOverflow15.080 us0.6158 us1.7866 us14.874 us0.160.03
PrimitivePreparedNormalStackOverflow13.964 us0.5963 us1.7490 us13.257 us0.150.02
SocketsHandlerNormalStackOverflow94.221 us2.4801 us7.3127 us96.337 us1.000.00

A simple GET request using exactly one HTTP/1 connection, no pooling

Avoid a connection pool to have exact control over connections.

await using ConnectionFactory connectionFactory = new SocketConnectionFactory();
await using Connection connection = await connectionFactory.ConnectAsync(new DnsEndPoint("microsoft.com", 80));
await using HttpConnection httpConnection = new Http1Connection(connection);
await using ValueHttpRequest request = (await httpConnection.CreateNewRequestAsync(HttpPrimitiveVersion.Version11, HttpVersionPolicy.RequestVersionExact)).Value;

request.ConfigureRequest(contentLength: 0, hasTrailingHeaders: false);
request.WriteRequest(HttpMethod.Get, new Uri("http://microsoft.com"));
request.WriteHeader("Accept", "text/html");
await request.CompleteRequestAsync();

await request.ReadToFinalResponseAsync();
Console.WriteLine($"Got response {request.StatusCode}.");

if(await request.ReadToHeadersAsync())
{
    await request.ReadHeadersAsync(...);
}

if(await request.ReadToContentAsync())
{
    int len;
    byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);

    do
    {
        while((len = await request.ReadContentAsync(buffer)) != 0)
        {
            ForwardData(buffer[..len]);
        }
    }
    while(await request.ReadToNextContentAsync());

    ArrayPool<byte>.Shared.Return(buffer);
}

await request.DrainAsync();

Using the opt-in connection pool

A single-host connection pool handles concurrent HTTP/1 requests, H2C upgrade, ALPN negotiation for HTTP/2, etc.

await using ConnectionFactory connectionFactory = new SocketConnectionFactory();
await using HttpConnection httpConnection = new PooledHttpConnection(connectionFactory, new DnsEndPoint("microsoft.com", 80), sslTargetHost: null);
await using ValueHttpRequest request = (await httpConnection.CreateNewRequestAsync(HttpPrimitiveVersion.Version11, HttpVersionPolicy.RequestVersionExact)).Value;

Optimize frequently-used headers

Prepare frequently-used headers to reduce CPU costs by pre-validating and caching protocol encoding. In the future, this will light up dynamic table compression in HTTP/2 and HTTP/3.

PreparedHeaderSet preparedHeaders = new PreparedHeaderSet()
{
    { "User-Agent", "NetworkToolkit" },
    { "Accept", "text/html" }
};

await using ValueHttpRequest request = ...;

request.ConfigureRequest(contentLength: 0, hasTrailingHeaders: false);
request.WriteRequest(HttpMethod.Get, new Uri("http://microsoft.com"));
request.WriteHeader(preparedHeaders);

Avoiding strings

Avoid string and Uri allocations, optimize away some related processing, and get tight control over encoding by passing in ReadOnlySpan<byte>:

ReadOnlySpan<byte> method = HttpRequest.GetMethod; // "GET"
ReadOnlySpan<byte> authority = ...; // "microsoft.com:80"
ReadOnlySpan<byte> pathAndQuery = ...; // "/"

ReadOnlySpan<byte> headerName = ...; // "Accept"
ReadOnlySpan<byte> headerValue = ...; // "text/html"

await using ValueHttpRequest request = ...;
request.ConfigureRequest(contentLength: 0, hasTrailingHeaders: false);
request.WriteRequest(method, authority, pathAndQuery);
request.WriteHeader(headerName, headerValue);

Advanced processing

Access extensions like HTTP/2 ALT-SVC frames, or efficiently forward requests (reverse proxy) by processing requests as a loosely structured stream:

await using ValueHttpRequest request = ...;
while(await request.ReadAsync() != HttpReadType.EndOfStream)
{
    switch(request.ReadType)
    {
    case HttpReadType.InformationalResponse:
        ProcessInformationalResponse(request.StatusCode, request.Version);
        break;
    case HttpReadType.FinalResponse:
        ProcessResponse(request.StatusCode, request.Version);
        break;
    case HttpReadType.Headers:
        await request.ReadHeadersAsync(...);
        break;
    case HttpReadType.Content:
        int len;
        byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);

        while((len = await request.ReadContentAsync(buffer)) != 0)
        {
            ForwardData(buffer[..len]);
        }

        ArrayPool<byte>.Shared.Return(buffer);
        break;
    case HttpReadType.TrailingHeaders:
        await request.ReadHeadersAsync(...);
        break;
    case HttpReadType.AltSvc:
        ProcessAltSvc(request.AltSvc);
        break;
    }
}

Connection Abstractions

Connection abstractions used to abstract establishment of Stream-based connections.