Introducción: por qué WebSocket en Rust es un tándem poderoso
Las aplicaciones web modernas requieren cada vez más transmisión instantánea de datos: chats, juegos en línea, cotizaciones bursátiles, notificaciones. El HTTP tradicional con su modelo de solicitud-respuesta no es eficiente aquí: crea carga y latencia innecesarias. El protocolo WebSocket viene al rescate, estableciendo una conexión bidireccional permanente entre el cliente y el servidor.
El lenguaje Rust con su énfasis en rendimiento, seguridad de memoria y abstracciones de costo cero es ideal para construir sistemas en tiempo real de alta carga. En este artículo, analizaremos cómo construir un servidor WebSocket en Rust usando la biblioteca tokio-tungstenite, y revisaremos los patrones clave para aplicaciones en tiempo real.
Fundamentos de WebSocket y preparación del entorno
¿Qué es WebSocket?
WebSocket es un protocolo que funciona sobre TCP. Permite que el servidor y el cliente intercambien mensajes en tiempo real sin necesidad de handshakes HTTP repetidos. La conexión comienza como una solicitud HTTP con una actualización a WebSocket, después de lo cual los datos se transmiten en tramas.
Instalación de dependencias
Para trabajar necesitaremos tokio (runtime asíncrono) y tokio-tungstenite (implementación de WebSocket). Cree un nuevo proyecto:
cargo new websocket_democd websocket_demoAgregue las dependencias en Cargo.toml:
[dependencies]tokio = { version = "1", features = ["full"] }tokio-tungstenite = "0.21"futures-util = "0.3"Creación de un servidor WebSocket simple
Servidor echo básico
Empecemos con un servidor simple que acepta conexiones y envía de vuelta los mensajes recibidos (modo echo). Este es el clásico «Hello, World» para WebSocket.
use tokio::net::TcpListener;use tokio_tungstenite::accept_async;use tokio_tungstenite::tungstenite::Message;use futures_util::StreamExt;
#[tokio::main]async fn main() { let addr = "127.0.0.1:8080"; let listener = TcpListener::bind(addr).await.expect("No se pudo enlazar la dirección"); println!("Servidor iniciado en {}", addr);
while let Ok((stream, peer)) = listener.accept().await { println!("Nueva conexión: {}", peer); tokio::spawn(handle_connection(stream)); }}
async fn handle_connection(stream: tokio::net::TcpStream) { let ws_stream = accept_async(stream) .await .expect("Error en el handshake WebSocket");
let (_, mut read) = ws_stream.split();
while let Some(msg) = read.next().await { match msg { Ok(Message::Text(text)) => { println!("Recibido: {}", text); // Aquí se podría enviar una respuesta, pero por simplicidad lo omitimos } Ok(Message::Close(_)) => break, _ => {} } }}Este código inicia un servidor TCP en el puerto 8080. Para cada nueva conexión se crea una tarea asíncrona handle_connection, que realiza el handshake WebSocket y lee los mensajes entrantes.
Agregamos envío de mensajes
Para hacer un servidor echo completo, necesitamos dividir el flujo en lectura y escritura. Usamos futures_util::StreamExt y SinkExt:
use futures_util::SinkExt;
async fn handle_connection(stream: tokio::net::TcpStream) { let ws_stream = accept_async(stream) .await .expect("Error en el handshake WebSocket");
let (mut write, mut read) = ws_stream.split();
while let Some(msg) = read.next().await { match msg { Ok(Message::Text(text)) => { let echo = format!("Echo: {}", text); write.send(Message::Text(echo.into())).await.unwrap(); } Ok(Message::Close(_)) => break, _