Cuando WebSocket es demasiado, o por qué SSE vuelve a estar de moda
\n\nImagine: necesita enviar datos del servidor al cliente en tiempo real. Tipos de cambio, notificaciones de nuevos mensajes, registros del servidor, progreso del procesamiento de archivos. Lo primero que viene a la mente es WebSocket. Pero, ¿qué pasa si no necesita un canal bidireccional? ¿Qué pasa si el cliente solo escucha y el servidor solo habla?
\n\nAquí entran en escena los Server-Sent Events (SSE): un protocolo que permite al servidor enviar datos al cliente a través de una conexión HTTP normal. Sin handshakes adicionales, sin protocolos complejos, sin dolores de cabeza con proxies y balanceadores de carga. Y sí, en Rust funciona de maravilla.
\n\nEn este artículo, analizaremos cómo implementar SSE en Rust desde cero, utilizando frameworks populares. Verá ejemplos funcionales, conocerá los obstáculos y comprenderá cuándo SSE es su mejor opción.
\n\n¿Qué son los Server-Sent Events y por qué Rust?
\n\nSSE es un estándar W3C (API EventSource) que permite al servidor enviar datos en flujo al cliente a través de HTTP. El cliente se conecta a un endpoint y el servidor comienza a enviar mensajes en formato text/event-stream. Todo funciona sobre HTTP/1.1 o HTTP/2 normal, no requiere proxies especiales y es compatible con todos los navegadores modernos.
¿Por qué Rust? Porque Rust nos ofrece:
\n- \n
- Asincronía sin sorpresas — Tokio y async-std permiten mantener miles de conexiones SSE en un solo hilo \n
- Seguridad de memoria — sin fugas, sin condiciones de carrera en transmisión multihilo \n
- Consumo mínimo de recursos — una conexión SSE consume unos pocos kilobytes \n
- Ecosistema — Actix Web, Rocket, Axum: todos soportan SSE de fábrica o con extensiones mínimas \n
Cómo funciona SSE: analizando el protocolo
\n\nEl cliente crea un EventSource en el navegador, especificando una URL. El servidor responde con el encabezado Content-Type: text/event-stream y comienza a enviar líneas en el formato:
data: ¡Hola, mundo!\\n\\n\n\nCada mensaje es un bloque donde data: contiene la carga útil. Los mensajes se separan por dos caracteres de nueva línea. Se puede transmitir JSON, números, cadenas, lo que sea. Adicionalmente, existen los campos id, event y retry.
Aquí hay un ejemplo mínimo de un mensaje SSE:
\n\nid: 1\nevent: update\ndata: {"status": "processing", "progress": 42}\n\n\n\nEl cliente en JavaScript recibe esto como un evento update con el campo data que contiene el JSON.
Escribiendo un servidor SSE en Rust con Actix Web
\n\nEmpecemos con el framework más popular: Actix Web. Instalemos las dependencias:
\n\n[dependencies]\nactix-web = "4"\nactix-rt = "2"\ntokio = { version = "1", features = ["full"] }\nserde = { version = "1", features = ["derive"] }\nserde_json = "1"\n\nAhora escribamos un endpoint simple que envíe la hora actual cada 2 segundos:
\n\nuse actix_web::{get, web, App, HttpRequest, HttpResponse, HttpServer, middleware};\nuse actix_web::rt::time;\nuse futures::stream::StreamExt;\nuse std::time::Duration;\n\n#[get("/events")]\nasync fn stream_events(req: HttpRequest) -> HttpResponse {\n let mut rx = tokio::sync::broadcast::channel::<String>(100).1;\n\n let stream = async_stream::stream! {\n loop {\n let message = format!("data: {}\\n\\n", chrono::Local::now());\n yield Ok::<_, actix_web::Error>(\n web::Bytes::from(message)\n );\n time::sleep(Duration::from_secs(2)).await;\n }\n };\n\n HttpResponse::Ok()\n .insert_header(("Content-Type", "text/event-stream"))\n .insert_header(("Cache-Control", "no-cache"))\n .insert_header(("Connection"