Back to Quiche

Tokio Quiche

tokio-quiche/README.md

0.28.05.9 KB
Original Source

Tokio Quiche

Bridging the gap between quiche and tokio.

tokio-quiche connects quiche::Connections and quiche::h3::Connections to tokio's event loop. Users have the choice between implementing their own, custom <code>ApplicationOverQuic</code> or using the ready-made <code>H3Driver</code> for HTTP/3 clients and servers.

Starting an HTTP/3 Server

A server listens on a UDP socket for QUIC connections and spawns a new tokio task to handle each individual connection.

rust
use bytes::Bytes;
use foundations::telemetry::log;
use tokio_quiche::http3::driver::{H3Event, IncomingH3Headers, OutboundFrame, ServerH3Event};
use tokio_quiche::http3::settings::Http3Settings;
use tokio_quiche::listen;
use tokio_quiche::metrics::DefaultMetrics;
use tokio_quiche::quic::SimpleConnectionIdGenerator;
use tokio_quiche::quiche::h3;
use tokio_quiche::{ConnectionParams, ServerH3Controller, ServerH3Driver};

let socket = tokio::net::UdpSocket::bind("0.0.0.0:4043").await?;
let mut listeners = listen(
    [socket],
    ConnectionParams::new_server(
        Default::default(),
        tokio_quiche::settings::TlsCertificatePaths {
            cert: "/path/to/cert.pem",
            private_key: "/path/to/key.pem",
            kind: tokio_quiche::settings::CertificateKind::X509,
        },
        Default::default(),
    ),
    SimpleConnectionIdGenerator,
    DefaultMetrics,
)?;
let accept_stream = &mut listeners[0];

while let Some(conn) = accept_stream.next().await {
    let (driver, controller) = ServerH3Driver::new(Http3Settings::default());
    conn?.start(driver);
    tokio::spawn(handle_connection(controller));
}

async fn handle_connection(mut controller: ServerH3Controller) {
    while let Some(ServerH3Event::Core(event)) = controller.event_receiver_mut().recv().await {
        match event {
            H3Event::IncomingHeaders(IncomingH3Headers {
                mut send, headers, ..
            }) => {
                log::info!("incoming headers"; "headers" => ?headers);
                send.send(OutboundFrame::Headers(
                    vec![h3::Header::new(b":status", b"200")],
                    None,
                ))
                .await
                .unwrap();

                send.send(OutboundFrame::Body(
                    Bytes::copy_from_slice(b"hello from TQ!"),
                    true,
                ))
                .await
                .unwrap();
            }
            event => {
                log::info!("event: {event:?}");
            }
        }
    }
}

Sending an HTTP/3 request

rust
use foundations::telemetry::log;
use tokio_quiche::http3::driver::{ClientH3Event, H3Event, IncomingH3Headers};
use tokio_quiche::quiche::h3;

let socket = tokio::net::UdpSocket::bind("0.0.0.0:0").await?;
socket.connect("127.0.0.1:4043").await?;
let (_, mut controller) = tokio_quiche::quic::connect(socket, None).await?;

controller
    .request_sender()
    .send(tokio_quiche::http3::driver::NewClientRequest {
        request_id: 0,
        headers: vec![h3::Header::new(b":method", b"GET")],
        body_writer: None,
    })
    .unwrap();

while let Some(event) = controller.event_receiver_mut().recv().await {
    match event {
        ClientH3Event::Core(H3Event::IncomingHeaders(IncomingH3Headers {
            stream_id,
            headers,
            ..
        })) => {
            log::info!("incoming headers"; "stream_id" => stream_id, "headers" => ?headers);
        }
        ClientH3Event::Core(H3Event::BodyBytesReceived { stream_id, data, fin }) => {
            log::info!("inbound body";
                "stream_id" => stream_id,
                "len" => data.len(),
                "fin" => fin
            );
            if fin {
                log::info!("received full body, exiting");
                break;
            }
        }
        ClientH3Event::Core(event) => log::info!("received event: {event:?}"),
        ClientH3Event::NewOutboundRequest {
            stream_id,
            request_id,
        } => log::info!(
            "sending outbound request";
            "stream_id" => stream_id,
            "request_id" => request_id
        ),
    }
}

Note: Omitted in these two examples is the use of stream_id to track multiplexed requests within the same connection.

Feature Flags

tokio-quiche supports a number of feature flags to enable experimental features, performance enhancements, and additional telemetry. By default, no feature flags are enabled.

  • rpk: Support for raw public keys (RPK) in QUIC handshakes (via boring).
  • gcongestion: Replace quiche's original congestion control implementation with one adapted from google/quiche.
  • zero-copy: Use zero-copy sends with quiche (implies gcongestion).
  • perf-quic-listener-metrics: Extra telemetry for QUIC handshake durations, including protocol overhead and network delays.
  • tokio-task-metrics: Scheduling & poll duration histograms for tokio tasks.

Other parts of the crate are enabled by separate build flags instead, to be controlled by the final binary:

  • --cfg capture_keylogs: Optional SSLKEYLOGFILE capturing for QUIC connections.

Server usage architecture

Client usage architecture