Cómo construir un servidor HTTP asíncrono en Rust con Tokio y Hyper

rust

Cómo construir un servidor HTTP asíncrono en Rust con Tokio y Hyper

En este tutorial práctico crearás un servidor HTTP asíncrono minimalista y robusto en Rust usando tokio y hyper. Verás la estructura del proyecto, el código completo, pruebas básicas y por qué tomamos cada decisión.

Por qué Tokio + Hyper

  • Tokio es el runtime asíncrono de facto en Rust: alto rendimiento y ecosistema maduro.
  • Hyper es una librería HTTP de bajo nivel, muy eficiente y usada como base por frameworks como warp o axum.
  • Combinados, permiten un control fino sobre rendimiento y diseño arquitectural.

Requisitos

  • Rust (stable)
  • cargo

Estructura de carpetas

async-server/
├─ Cargo.toml
└─ src/
   └─ main.rs

Cargo.toml

[package]
name = "async-server"
edition = "2021"

[dependencies]
tokio = { version = "1", features = ["full"] }
hyper = "0.14"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Explicación rápida: tokio para runtime async; hyper como servidor HTTP; tracing para logs estructurados; serde/serde_json para JSON.

src/main.rs — Código completo

use std::convert::Infallible;
use hyper::{Body, Request, Response, Server, Method, StatusCode};
use hyper::service::{make_service_fn, service_fn};
use serde::Serialize;
use tracing::{info, error};

async fn handle(req: Request) -> Result, Infallible> {
    match (req.method(), req.uri().path()) {
        (&Method::GET, "/") => {
            let body = "Hello from Rust + Hyper!";
            Ok(Response::new(Body::from(body)))
        }

        (&Method::GET, "/json") => {
            #[derive(Serialize)]
            struct Msg { msg: &'static str }
            let m = Msg { msg: "Hello JSON" };
            let json = serde_json::to_string(&m).unwrap();

            let mut res = Response::new(Body::from(json));
            res.headers_mut().insert(
                hyper::header::CONTENT_TYPE,
                hyper::header::HeaderValue::from_static("application/json"),
            );
            Ok(res)
        }

        _ => {
            let mut not_found = Response::new(Body::from("Not Found"));
            *not_found.status_mut() = StatusCode::NOT_FOUND;
            Ok(not_found)
        }
    }
}

#[tokio::main]
async fn main() -> Result<(), Box> {
    // Inicializa logging con nivel configurable vía RUST_LOG
    tracing_subscriber::fmt::init();

    let addr = ([127, 0, 0, 1], 3000).into();

    // make_service crea una instancia de servicio por conexión
    let make_svc = make_service_fn(|_conn| async {
        Ok::<_, Infallible>(service_fn(handle))
    });

    let server = Server::bind(&addr).serve(make_svc);

    // Graceful shutdown al recibir Ctrl+C
    let graceful = server.with_graceful_shutdown(async {
        tokio::signal::ctrl_c()
            .await
            .expect("failed to listen for ctrl_c");
        info!("Señal recibida, iniciando apagado ordenado");
    });

    info!("Escuchando en http://{}", addr);

    if let Err(e) = graceful.await {
        error!("error del servidor: {}", e);
    }

    Ok(())
}


Por qué este diseño

  • service_fn: permite convertir una función async en un Service compatible con Hyper sin dependencias externas.
  • make_service_fn: crea un servicio por conexión, útil si necesitas estado por conexión (ej. métricas, auth).
  • with_graceful_shutdown: evita cortar conexiones abruptamente y permite liberar recursos.
  • tracing en lugar de println: logs estructurados, filtrado por RUST_LOG y mejor para producción.

Ejecutar y probar

  1. Construir y ejecutar: cargo run --release
  2. Probando con curl:
curl http://127.0.0.1:3000/
curl http://127.0.0.1:3000/json
curl -i http://127.0.0.1:3000/unknown

Buenas prácticas y errores comunes

  • No hagas blocking I/O en handlers; si necesitas bloquear, muévelo a un spawn_blocking.
  • Maneja correctamente errores: evita unwrap() en producción; usa tipos de error y mapea a respuestas HTTP.
  • Para rutas y parsing complejo usa marcos de routing (axum, warp, tide) o implementa un router claro.
  • Si necesitas middleware (autenticación, rate-limiting), considera tower o migrar a frameworks basados en Tower.

Escalado y optimización

  • Deploy: ejecuta con múltiples workers detrás de un load balancer o usa systemd/containers para instancias.
  • HTTPS: agrega TLS con rustls + hyper o usa un proxy inverso (NGINX, Caddy).
  • Métricas: instrumenta con Prometheus (exportador) y usa tracing + opentelemetry para trazas distribuidas.
  • Conexiones y keep-alive: tunear timeouts y límites de conexión según tu carga.

Extensiones sugeridas

  • Agrega un router con axum para handlers más expresivos y extracción automática de parámetros.
  • Integra autenticación JWT como middleware con tower.
  • Soporte HTTP/2 y gRPC mediante tonic si necesitas RPC de alto rendimiento.

Consejo avanzado: para producción, reemplaza la función handler plana por una pila de middlewares (tower) que permita añadir logging correlacionado, timeouts y retries de forma composable. Como siguiente paso práctico integra TLS con rustls y añade pruebas de integración automatizadas que arranquen el servidor en background y realicen solicitudes reales.

Comentarios
¿Quieres comentar?

Inicia sesión con Telegram para participar en la conversación


Comentarios (0)

Aún no hay comentarios. ¡Sé el primero en comentar!

Iniciar Sesión