From 20862d42f50ed6982907c50d4da41313ad7938bd Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Thu, 23 Nov 2023 13:39:01 +0100 Subject: [PATCH] Add example showing how to use hyper's low level API (#2338) --- axum/src/extract/connect_info.rs | 6 ++ examples/serve-with-hyper/Cargo.toml | 12 +++ examples/serve-with-hyper/src/main.rs | 124 ++++++++++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 examples/serve-with-hyper/Cargo.toml create mode 100644 examples/serve-with-hyper/src/main.rs diff --git a/axum/src/extract/connect_info.rs b/axum/src/extract/connect_info.rs index 0085b3f8..3036d375 100644 --- a/axum/src/extract/connect_info.rs +++ b/axum/src/extract/connect_info.rs @@ -94,6 +94,12 @@ const _: () = { } }; +impl Connected for SocketAddr { + fn connect_info(remote_addr: SocketAddr) -> Self { + remote_addr + } +} + impl Service for IntoMakeServiceWithConnectInfo where S: Clone, diff --git a/examples/serve-with-hyper/Cargo.toml b/examples/serve-with-hyper/Cargo.toml new file mode 100644 index 00000000..06f46070 --- /dev/null +++ b/examples/serve-with-hyper/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "example-serve-with-hyper" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +axum = { path = "../../axum" } +hyper = { version = "1.0", features = [] } +hyper-util = { version = "0.1", features = ["tokio", "server-auto", "http1"] } +tokio = { version = "1.0", features = ["full"] } +tower = { version = "0.4", features = ["util"] } diff --git a/examples/serve-with-hyper/src/main.rs b/examples/serve-with-hyper/src/main.rs new file mode 100644 index 00000000..0b9c93d2 --- /dev/null +++ b/examples/serve-with-hyper/src/main.rs @@ -0,0 +1,124 @@ +//! Run with +//! +//! ```not_rust +//! cargo run -p example-serve-with-hyper +//! ``` +//! +//! This example shows how to run axum using hyper's low level API. +//! +//! The [hyper-util] crate exists to provide high level utilities but its still in early stages of +//! development. +//! +//! [hyper-util]: https://crates.io/crates/hyper-util + +use std::convert::Infallible; +use std::net::SocketAddr; + +use axum::extract::ConnectInfo; +use axum::{extract::Request, routing::get, Router}; +use hyper::body::Incoming; +use hyper_util::rt::{TokioExecutor, TokioIo}; +use hyper_util::server; +use tokio::net::TcpListener; +use tower::{Service, ServiceExt}; + +#[tokio::main] +async fn main() { + tokio::join!(serve_plain(), serve_with_connect_info()); +} + +async fn serve_plain() { + // Create a regular axum app. + let app = Router::new().route("/", get(|| async { "Hello!" })); + + // Create a `TcpListener` using tokio. + let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap(); + + // Continuously accept new connections. + loop { + // In this example we discard the remote address. See `fn serve_with_connect_info` for how + // to expose that. + let (socket, _remote_addr) = listener.accept().await.unwrap(); + + // We don't need to call `poll_ready` because `Router` is always ready. + let tower_service = app.clone(); + + // Spawn a task to handle the connection. That way we can multiple connections + // concurrently. + tokio::spawn(async move { + // Hyper has its own `AsyncRead` and `AsyncWrite` traits and doesn't use tokio. + // `TokioIo` converts between them. + let socket = TokioIo::new(socket); + + // Hyper also has its own `Service` trait and doesn't use tower. We can use + // `hyper::service::service_fn` to create a hyper `Service` that calls our app through + // `tower::Service::call`. + let hyper_service = hyper::service::service_fn(move |request: Request| { + // We have to clone `tower_service` because hyper's `Service` uses `&self` whereas + // tower's `Service` requires `&mut self`. + // + // We don't need to call `poll_ready` since `Router` is always ready. + tower_service.clone().call(request) + }); + + // `server::conn::auto::Builder` supports both http1 and http2. + // + // `TokioExecutor` tells hyper to use `tokio::spawn` to spawn tasks. + if let Err(err) = server::conn::auto::Builder::new(TokioExecutor::new()) + // `serve_connection_with_upgrades` is required for websockets. If you don't need + // that you can use `serve_connection` instead. + .serve_connection_with_upgrades(socket, hyper_service) + .await + { + eprintln!("failed to serve connection: {err:#}"); + } + }); + } +} + +// Similar setup to `serve_plain` but captures the remote address and exposes it through the +// `ConnectInfo` extractor +async fn serve_with_connect_info() { + let app = Router::new().route( + "/", + get( + |ConnectInfo(remote_addr): ConnectInfo| async move { + format!("Hello {remote_addr}") + }, + ), + ); + + let mut make_service = app.into_make_service_with_connect_info::(); + + let listener = TcpListener::bind("0.0.0.0:3001").await.unwrap(); + + loop { + let (socket, remote_addr) = listener.accept().await.unwrap(); + + // We don't need to call `poll_ready` because `IntoMakeServiceWithConnectInfo` is always + // ready. + let tower_service = unwrap_infallible(make_service.call(remote_addr).await); + + tokio::spawn(async move { + let socket = TokioIo::new(socket); + + let hyper_service = hyper::service::service_fn(move |request: Request| { + tower_service.clone().oneshot(request) + }); + + if let Err(err) = server::conn::auto::Builder::new(TokioExecutor::new()) + .serve_connection_with_upgrades(socket, hyper_service) + .await + { + eprintln!("failed to serve connection: {err:#}"); + } + }); + } +} + +fn unwrap_infallible(result: Result) -> T { + match result { + Ok(value) => value, + Err(err) => match err {}, + } +}