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
tqpara este capítulo está en6-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:
async fn foo()- una función que sabe esperarfoo().await- espera el resultado#[tokio::main]- hace queasync fn main()y.awaitdentro de ella funcionen
Entender lo que ocurre por debajo - Future, Poll, wakers - es un estudio aparte. Usarlo
es posible ahora mismo.