This project contains networking primitives for use with .NET. It can be obtained from NuGet.
This is a bit of a prototyping playground right now and APIs will not be perfectly stable.
NetworkToolkit provides a lower-level HTTP client that prioritizes performance and flexibility. Once warmed up, it processes requests using zero allocations.
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:
Method | RequestHeaders | ResponseBytes | Mean | Error | StdDev | Median | Ratio | RatioSD |
---|---|---|---|---|---|---|---|---|
Primitive | Minimal | Minimal | 10.806 us | 0.3304 us | 0.9741 us | 10.985 us | 0.51 | 0.05 |
PrimitivePrepared | Minimal | Minimal | 9.278 us | 0.2091 us | 0.6066 us | 9.298 us | 0.44 | 0.04 |
SocketsHandler | Minimal | Minimal | 21.323 us | 0.4380 us | 1.2136 us | 21.442 us | 1.00 | 0.00 |
Primitive | Minimal | StackOverflow | 13.665 us | 0.6509 us | 1.9089 us | 13.187 us | 0.16 | 0.02 |
PrimitivePrepared | Minimal | StackOverflow | 14.108 us | 0.6328 us | 1.8559 us | 13.432 us | 0.17 | 0.03 |
SocketsHandler | Minimal | StackOverflow | 86.356 us | 1.7149 us | 4.2707 us | 87.476 us | 1.00 | 0.00 |
Primitive | Normal | Minimal | 11.053 us | 0.2498 us | 0.7366 us | 11.149 us | 0.32 | 0.03 |
PrimitivePrepared | Normal | Minimal | 10.636 us | 0.2867 us | 0.8455 us | 10.701 us | 0.31 | 0.03 |
SocketsHandler | Normal | Minimal | 34.775 us | 0.6940 us | 1.9231 us | 35.172 us | 1.00 | 0.00 |
Primitive | Normal | StackOverflow | 15.080 us | 0.6158 us | 1.7866 us | 14.874 us | 0.16 | 0.03 |
PrimitivePrepared | Normal | StackOverflow | 13.964 us | 0.5963 us | 1.7490 us | 13.257 us | 0.15 | 0.02 |
SocketsHandler | Normal | StackOverflow | 94.221 us | 2.4801 us | 7.3127 us | 96.337 us | 1.00 | 0.00 |
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();
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;
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 PreparedHeaderSetBuilder()
.AddHeader("User-Agent", "NetworkToolkit")
.AddHeader("Accept", "text/html")
.Build();
await using ValueHttpRequest request = ...;
request.ConfigureRequest(contentLength: 0, hasTrailingHeaders: false);
request.WriteRequest(HttpMethod.Get, new Uri("http://microsoft.com"));
request.WriteHeader(preparedHeaders);
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);
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 used to abstract establishment of Stream
-based connections.