ioxide
io_uring which is a capability of the Linux kernel, it cannot
run on other platforms, such as Windows or macOS. Kernel version 6.1 or higher is required.io_uring might be blocked by security
mechanisms, so you may need to unblock them explicitly.An engine built on top of the ioxide io_uring runtime:
one reactor runs per CPU core,accepting and serving connections independently. According to the
HTTP Arena benchmark, this engine provides the fastest webserver on the whole list.
using GenHTTP.Engine.Ioxide;
await Host.Create()
.Handler(...)
.RunAsync();Serving async Payloads
ioxide achieves its performance by keeping a request on a single CPU core from
start to finish. Standard async code continues to work: Npgsql, HttpClient,
and any other .NET library can be used without modification. Such libraries
dispatch their work to the shared .NET thread pool, so a request may resume on
a different core than the one it started on. ioxide detects this and returns
the work to the originating core. This is correct, but not free: the cost is
incurred on every affected call and, under load, reduces the very advantage
ioxide was adopted for.
To avoid this, ioxide provides drivers that remain on the same core:
ioxide.pg for PostgreSQL,
ioxide.redis for Redis,
ioxide.file for file access,
and a built-in HTTP client. Applications that query a database or read files
on most requests benefit the most from these drivers. In all other cases, the
standard libraries remain a suitable choice.
.Result or .Wait() on a
task does not merely slow the core down, it halts it: the thread waits for
work that only it can process and will not recover until the process is
restarted. Always use await.Tuning the io_uring Runtime
The defaults - one reactor per CPU core, with sensible ring and buffer sizes - work for most
deployments. If you need finer control, Host.Create(...) takes an EngineOptions, grouped by the
part of the runtime each setting belongs to: the reactors (Reactor), the TCP transport and its TLS
(Tcp), the QUIC transport (Quic), and HTTP/3 above it (Http3).
using GenHTTP.Engine.Ioxide;
await Host.Create(options: new EngineOptions
{
Reactor = new ReactorOptions
{
ReactorCount = 16,
RingEntries = 16384,
RecvBufferSize = 64 * 1024,
RecvSlots = 8192,
},
})
.Handler(...)
.Bind(...)
.RunAsync();Host.Create(...) also takes a hook that runs once per reactor, on that reactor’s own thread, before
it starts serving - useful to register per-reactor, ring-native services.
await Host.Create(onReactorStart: reactor => ...)
.Handler(...)
.Bind(...)
.RunAsync();TLS
TLS is configured through the .Bind(...) overload that takes a certificate provider. It is
terminated in OpenSSL on the TCP transports, and the handshake rides the same io_uring reads and
writes as everything else. Where a port serves both HTTP/1.1 and HTTP/2, ALPN decides which one a
connection speaks - the server offers HTTP/2 first, so a client that speaks it is served HTTP/2 and
everyone else falls back to HTTP/1.1.
using System.Net;
using System.Security.Authentication;
using GenHTTP.Api.Infrastructure;
using GenHTTP.Engine.Ioxide;
await Host.Create()
.Handler(...)
.Bind(IPAddress.Any, 8443, new FileCertificateProvider("./cert.pem", "./key.pem"),
sslProtocols: SslProtocols.Tls12 | SslProtocols.Tls13,
httpProtocols: HttpProtocols.Http1AndHttp2)
.RunAsync();A FileCertificateProvider names the certificate and its key as PEM files. Files are preferred:
OpenSSL reads the chain from the file, so intermediates come from there rather than the machine
store, and the private key never enters managed memory. On the TCP transports an already loaded
X509Certificate2 can be passed instead; HTTP/3 cannot take one.
Server Name Indication
If you would like to serve several host names from one port, a HostCertificateProvider holds one
certificate per name alongside a default:
var certificates = new HostCertificateProvider("./localhost.pem", "./localhost.key");
certificates.Add("alpha.example", "./alpha.pem", "./alpha.key");
certificates.Add("beta.example", "./beta.pem", "./beta.key");
await Host.Create()
.Handler(...)
.Bind(IPAddress.Any, 8443, certificates, httpProtocols: HttpProtocols.Http1AndHttp2)
.RunAsync();The client sends the name it wants during the handshake, and the server answers with that name’s certificate. Names are matched case-insensitively and exactly - a wildcard certificate covers its names through the certificate itself, not by being registered here. The default is not optional, as it answers a client that sent no name or asked for one this port does not hold, which is what a bare IP address does, since an IP is not a legal SNI value.
HTTP/3
An HTTP/3 port always needs a certificate, and it needs it as files: ngtcp2 loads PEM by path and
takes nothing else, so an in-memory X509Certificate2 is refused when the server starts. Bind such
a port with an IFileCertificateProvider - both FileCertificateProvider and
HostCertificateProvider are such. Only one endpoint may serve HTTP/3, as the engine binds a single
QUIC listener. Browsers reach HTTP/3 only after an Alt-Svc header points them there from a TCP
port, so a browser-facing deployment binds HTTP/3 alongside HTTP/1.1 or HTTP/2 rather than on its own.
Client Certificates
If you would like to require a client certificate, pass an IMutualTlsValidator as the
certificateValidator, naming the anchors the client’s certificate is checked against:
public sealed class RequireClientCertificate(string clientCaPath) : IMutualTlsValidator
{
public bool RequireCertificate => true;
public string? ClientCaPath => clientCaPath;
}.Bind(IPAddress.Any, 8444, new FileCertificateProvider("./cert.pem", "./key.pem"),
certificateValidator: new RequireClientCertificate("./client-ca.pem"),
httpProtocols: HttpProtocols.Http1)With RequireCertificate left false the connection is let in and the decision is left to the
handler. A binding that requires a certificate but names nothing to validate it against is refused
when the server starts.
Certificate Rotation
A renewed certificate does not need a restart. ReloadCertificates asks each bound provider again
and installs what it answers with, across both transports, while the server keeps serving:
using IoxideServer = GenHTTP.Engine.Ioxide.Infrastructure.Server;
var host = Host.Create()
.Handler(...)
.Bind(IPAddress.Any, 8443, certificates, httpProtocols: HttpProtocols.Http1AndHttp2);
// after the PEM files the providers name have been rewritten, e.g. by an ACME hook
(host.Instance as IoxideServer)?.ReloadCertificates();Connections already established keep the certificate they authenticated with. Only the certificate
material changes: trust anchors, RequireCertificate, ALPN and the TLS floor stay as the binding
set them, and no name can be added, since both stacks settle their SNI tables at startup. Everything
is resolved and checked before anything is published, so a provider that throws, or a path an ACME
client has not finished writing, leaves the server exactly as it was.
Kernel TLS
Kernel TLS moves the record layer into the kernel, so plaintext lands directly in ring memory. It is
off by default and is not a free win: it needs the Linux tls module, pins TLS 1.3 and a single
ciphersuite, and disables session resumption. RxKernelTls needs TxKernelTls, as inbound shares
the TCP_ULP the outbound side installs, so Tls13 is the only floor that is consistent with it.
await Host.Create(options: new EngineOptions
{
Tcp = new TcpTransportOptions
{
TxKernelTls = true,
RxKernelTls = true,
},
})
.Handler(...)
.Bind(IPAddress.Any, 8443, new FileCertificateProvider("./cert.pem", "./key.pem"),
sslProtocols: SslProtocols.Tls13,
httpProtocols: HttpProtocols.Http1AndHttp2)
.RunAsync();