← Índice

6.2 Un puerto en el mundo

Esta traducción fue asistida por IA y revisada superficialmente. Si encuentras errores o algo no suena natural, escríbeme — lo agradezco.

La diferencia entre “escuchando” y “respondiendo” es más o menos la misma que entre “la puerta está abierta” y “hay alguien en casa”. Lo primero es un hecho. Lo segundo es una pregunta. Un visitante con asuntos quiere la respuesta a lo segundo pero comprueba lo primero. El puerto está abierto.

Lo que necesitas

tq-cli responde a comandos. tq-api responderá peticiones HTTP - desde cualquier cliente que conozca la dirección. Ambos puntos de entrada usan el mismo almacén en tq-core. Este capítulo añade el punto de entrada en sí: una ruta, una dirección, un servidor.

axum es la elección para el servidor HTTP - un framework moderno de Rust construido sobre tokio. tokio gestiona tareas que esperan en la red sin bloquear un hilo. Esto se llama async.

Lo mínimo que hay que saber ahora mismo: async fn es una función que sabe esperar; .await es “espera aquí”; #[tokio::main] hace que async fn main() y .await dentro de ella funcionen. Todo lo demás lo gestiona tokio.

La construcción

El workspace recibe un tercer crate. Una nueva entrada en el Cargo.toml raíz:

# Cargo.toml - CAMBIADO
[workspace]
members = ["crates/core", "crates/cli", "crates/api"]
resolver = "2"

Crea el directorio:

$ mkdir -p crates/api/src

crates/api/Cargo.toml - un crate binario con dos dependencias:

# crates/api/Cargo.toml - NUEVO
[package]
name = "tq-api"
version = "0.1.0"
edition = "2024"

[[bin]]
name = "tq-api"
path = "src/main.rs"

[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }

features = ["full"] activa el conjunto completo de funciones de tokio. Puede ajustarse más adelante; por ahora los detalles son una distracción.

El servidor vive en crates/api/src/main.rs:

// crates/api/src/main.rs - NUEVO
use axum::{routing::get, Router};

async fn health() -> String {
    "tq ok".to_string()
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(health));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Router::new().route("/", get(health)) - una ruta: una petición GET a / se despacha a la función health. TcpListener::bind("0.0.0.0:3000") le dice al SO: las peticiones entrantes en el puerto 3000 son nuestras. Un puerto es el número por el que un cliente encuentra el servicio correcto en una máquina; 3000 es simplemente nuestra elección. axum::serve(listener, app) inicia el bucle de procesamiento de peticiones. Sin .await nunca comenzaría; con él - se ejecuta hasta que se interrumpa.

Añade un nuevo objetivo al Makefile:

## serve: start the HTTP server
.PHONY: serve
serve:
	@cargo run -p tq-api

El resultado

Inicia el servidor en un terminal:

$ make serve

Dos formas de verificar que funciona: abre http://localhost:3000/ en un navegador - la página muestra tq ok. O en otro terminal:

$ curl localhost:3000/
tq ok

El puerto está abierto. La petición llegó, el handler respondió, la conexión se cerró - el servidor se quedó esperando la siguiente. Para detenerlo, pulsa Ctrl+C en el terminal o cierra la ventana.

make ci pasa: cargo build compila los tres crates, los tests - los mismos 38.

El código completo de tq para este capítulo está en 6-the-port/02-a-port-in-the-world/.


Lore: Async en breve

Una función ordinaria devuelve un valor o bloquea el hilo hasta que termina. Un servidor que bloquea un hilo en cada petición las gestiona de una en una - inaceptable bajo cualquier carga real.

async fn devuelve un future - una promesa de resultado. Tokio mantiene un pool de hilos y decide qué future avanzar en cada momento. .await es el punto donde una función dice: “estoy esperando - ve a hacer otra cosa por ahora.”

Tres reglas de uso:

Entender lo que ocurre por debajo - Future, Poll, wakers - es un estudio aparte. Usarlo es posible ahora mismo.