We’re excited to announce experimental support for the WebTransport over HTTP/3 protocol as part of .NET 7 RC1.
This feature and blog post were written by our excellent intern Daniel Genkin!
What is WebTransport
WebTransport is a new draft specification for a transport protocol similar to WebSockets that allows the usage of multiple streams per connection.
WebSockets allowed upgrading a whole HTTP TCP/TLS connection to a bidirectional data stream. If you needed to open more streams you’d spend additional time and resources establishing new TCP and TLS sessions. WebSockets over HTTP/2 streamlined this by allowing multiple WebSocket streams to be established over one HTTP/2 TCP/TLS session. The downside here is that because this was still based on TCP, any packets lost from one stream would cause delays for every stream on the connection.
With the introduction of HTTP/3 and QUIC, which uses UDP rather than TCP, WebTransport can be used to establish multiple streams on one connection without them blocking each other. For example, consider an online game where the game state is transmitted on one bidirectional stream, the players’ voices for the game’s voice chat feature on another bidirectional stream, and the player’s controls are transmitted on a unidirectional stream. With WebSockets, these would all need to be put on separate connections or squashed into a single stream. With WebTransport, you can keep all the traffic on one connection but separate them into their own streams and, if one stream were to drop network packets, the others could continue uninterrupted.
Enabling WebTransport
You can enable WebTransport support in ASP.NET Core by setting EnablePreviewFeatures
to True
and adding the following RuntimeHostConfigurationOption
item in your project’s .csproj
file:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<EnablePreviewFeatures>True</EnablePreviewFeatures>
</PropertyGroup>
<ItemGroup>
<RuntimeHostConfigurationOption Include="Microsoft.AspNetCore.Server.Kestrel.Experimental.WebTransportAndH3Datagrams" Value="true" />
</ItemGroup>
</Project>
Setting up a Server
To setup a WebTransport connection, you first need to configure a web host to listen on a port over HTTP/3:
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel((context, options) =>
{
// Port configured for WebTransport
options.ListenAnyIP([SOME PORT], listenOptions =>
{
listenOptions.UseHttps(GenerateManualCertificate());
listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
});
});
var app = builder.Build();
WebTransport uses HTTP/3, so you must select the listenOptions.UseHttps
setting as well as set the listenOptions.Protocols
to include HTTP/3.
The default Kestrel development certificate cannot be used for WebTransport connections. For local testing you can use the workaround described in the Obtaining a test certificate section.
Next, we define the code that will run when Kestrel receives a connection.
app.Run(async (context) =>
{
var feature = context.Features.GetRequiredFeature<IHttpWebTransportFeature>();
if (!feature.IsWebTransportRequest)
{
return;
}
var session = await feature.AcceptAsync(CancellationToken.None);
// Use WebTransport via the newly established session.
});
await app.RunAsync();
The Run
method is triggered every time there is a connection request. The IsWebTransportRequest
property on the IHttpWebTransportFeature
indicates if the current request is a WebTransport request. Once a WebTransport request is received, calling IHttpWebTransportFeature.AcceptAsync()
accepts the WebTransport session so you can interact with the client. This original request must be kept alive for the durration of the session or else all streams associated with that session will be closed.
Calling await app.RunAsync()
starts the server, which can then start accepting connections.
Interacting with a WebTransport Session
Once the session has been established both the client and server can create streams for that session.
This example accepts a session, waits for a bidirectional stream, reads some data, reverse it, and then writes it back to the stream.
app.Run(async (context) =>
{
var feature = context.Features.GetRequiredFeature<IHttpWebTransportFeature>();
if (!feature.IsWebTransportRequest)
{
return;
}
var session = await feature.AcceptAsync(CancellationToken.None);
ConnectionContext? stream = null;
IStreamDirectionFeature? direction = null;
while (true)
{
// wait until we get a stream
stream = await session.AcceptStreamAsync(CancellationToken.None);
if (stream is null)
{
// if a stream is null, this means that the session failed to get the next one.
// Thus, the session has ended, or some other issue has occurred. We end the
// connection in this case.
return;
}
// check that the stream is bidirectional. If yes, keep going, otherwise
// dispose its resources and keep waiting.
direction = stream.Features.GetRequiredFeature<IStreamDirectionFeature>();
if (direction.CanRead && direction.CanWrite)
{
break;
}
else
{
await stream.DisposeAsync();
}
}
var inputPipe = stream!.Transport.Input;
var outputPipe = stream!.Transport.Output;
// read some data from the stream into the memory
var length = await inputPipe.AsStream().ReadAsync(memory);
// slice to only keep the relevant parts of the memory
var outputMemory = memory[..length];
// do some operations on the contents of the data
outputMemory.Span.Reverse();
// write back the data to the stream
await outputPipe.WriteAsync(outputMemory);
});
await app.RunAsync();
This example opens a new stream from the server side and then sends data.
app.Run(async (context) =>
{
var feature = context.Features.GetRequiredFeature<IHttpWebTransportFeature>();
if (!feature.IsWebTransportRequest)
{
return;
}
var session = await feature.AcceptAsync(CancellationToken.None);
// open a new stream from the server to the client
var stream = await session.OpenUnidirectionalStreamAsync(CancellationToken.None);
if (stream is not null)
{
// write data to the stream
var outputPipe = stream.Transport.Output;
await outputPipe.WriteAsync(new Memory<byte>(new byte[] { 65, 66, 67, 68, 69 }), CancellationToken.None);
await outputPipe.FlushAsync(CancellationToken.None);
}
});
await app.RunAsync();
Sample Apps
To showcase the functionality of the WebTransport support in Kestrel, we also created two sample apps: an interactive one and a console-based one. You can run them in Visual Studio via the F5 run options.
Obtaining a test certificate
The current Kestrel development certificate cannot be used for WebTransport connections as it does not meet the requirements needed for WebTransport over HTTP/3. You can generate a new certificate for testing via the following C# (this function will also automatically handle certificate rotation every time one expires):
static X509Certificate2 GenerateManualCertificate()
{
X509Certificate2 cert = null;
var store = new X509Store("KestrelWebTransportCertificates", StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite);
if (store.Certificates.Count > 0)
{
cert = store.Certificates[^1];
// rotate key after it expires
if (DateTime.Parse(cert.GetExpirationDateString(), null) < DateTimeOffset.UtcNow)
{
cert = null;
}
}
if (cert == null)
{
// generate a new cert
var now = DateTimeOffset.UtcNow;
SubjectAlternativeNameBuilder sanBuilder = new();
sanBuilder.AddDnsName("localhost");
using var ec = ECDsa.Create(ECCurve.NamedCurves.nistP256);
CertificateRequest req = new("CN=localhost", ec, HashAlgorithmName.SHA256);
// Adds purpose
req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection
{
new("1.3.6.1.5.5.7.3.1") // serverAuth
}, false));
// Adds usage
req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false));
// Adds subject alternate names
req.CertificateExtensions.Add(sanBuilder.Build());
// Sign
using var crt = req.CreateSelfSigned(now, now.AddDays(14)); // 14 days is the max duration of a certificate for this
cert = new(crt.Export(X509ContentType.Pfx));
// Save
store.Add(cert);
}
store.Close();
var hash = SHA256.HashData(cert.RawData);
var certStr = Convert.ToBase64String(hash);
Console.WriteLine($"\n\n\n\n\nCertificate: {certStr}\n\n\n\n"); // <-- you will need to put this output into the JS API call to allow the connection
return cert;
}
// Adapted from: https://github.com/wegylexy/webtransport
Going Forward
WebTransport offers exciting new possibilities for real-time app developers, making it easier and more efficient to stream multiple kinds of data back and forth with the client.
Please send us feedback about how this works for your scenarios. We’ll use that feedback to continue the development of this feature in .NET 8, explore integrations with frameworks like SignalR and determine when to take it out of preview.
Can this be used to enable a single signalR socket connection for multiple hub connections?
It will still be a single connection to a single hub.
In theory, the .NET client should be able to share the connection if you pass the same HttpMessageHandler around (assuming it’s similar to the WebSockets over HTTP/2 support).
And the browser would ideally share the connection automatically.
But the user would still need to create a separate HubConnection per SignalR connection, even if it uses the same socket underneath.
Am I right in thinking that if this requires HTTP3 that it can’t be used from Blazor WASM? Bidirectional streaming would be great to have in the browser but without SignalR it’s not something we’ve been able to do
I don’t see why not, though you’d need a fallback plan when the client or network doesn’t support HTTP/3.
You mentioned an example of using WebTransport for voice, I’m wondering if it would allow to keep UDP behavior, and allow to bypass any retransmission handled by http3 layer.
The current scenarios use QUIC streams. QUIC is the layer under HTTP/3 that provides reliable retransmission over UDP. There is talk of an un-reliable extension for QUIC that might help with this in the future: https://datatracker.ietf.org/doc/html/draft-ietf-quic-datagram
Will this make it possibly to use SignInManager in a Blazor component?
So we can get rid of this error in future? 🥳
Not directly 😁
But will it be possible to implement SignInManager and it’s extension methods to work on the HttpContext for SignIn? And use it in a Blazor component, by using WebTransport?
I don’t think WebTransport will change that situation.