Cómo funcionan los WebSockets: Guía completa de conexiones en tiempo real
En los inicios de la web, el navegador era un simple visor de documentos. Solicitabas una página, el servidor la renderizaba y la conexión se cerraba. Este ciclo de solicitud-respuesta es el núcleo de HTTP (Hypertext Transfer Protocol).
Sin embargo, a medida que las aplicaciones web evolucionaron hacia experiencias ricas e interactivas, como chats en tiempo real, cotizaciones financieras en vivo, edición colaborativa y juegos multijugador, el modelo HTTP tradicional comenzó a mostrar sus limitaciones.
Para obtener actualizaciones en vivo, los desarrolladores inicialmente recurrieron a soluciones temporales:
- Short Polling: El navegador envía repetidamente solicitudes HTTP al servidor cada pocos segundos para preguntar si hay nuevos datos. Esto crea una enorme sobrecarga de cabeceras y desperdicia recursos del servidor.
- Long Polling (Comet): El navegador envía una solicitud y el servidor la mantiene abierta hasta que haya nuevos datos disponibles. Una vez que se envían los datos, la conexión se cierra y el navegador abre inmediatamente una nueva solicitud. Esto es complejo de gestionar y sigue incurriendo en una sobrecarga significativa en la configuración de la conexión.
Los WebSockets resolvieron estas limitaciones introduciendo un protocolo estandarizado para la comunicación persistente, bidireccional y dúplex completo (full-duplex) a través de una única conexión TCP.
¿Qué es un WebSocket?
Los WebSockets (definidos en RFC 6455) funcionan junto con HTTP. Mientras que HTTP es un protocolo sin estado en el que solo el cliente puede iniciar solicitudes, una conexión WebSocket permanece abierta indefinidamente, lo que permite que tanto el cliente como el servidor se envíen datos en cualquier momento con una latencia mínima.
Esta es la regla fundamental de los WebSockets:
Una vez establecida, cualquiera de las partes puede enviar mensajes en cualquier momento sin iniciar una nueva solicitud de conexión.
Guía paso a paso: El ciclo de vida de la conexión
Una conexión WebSocket pasa por tres fases distintas: el Handshake (apretón de manos), la Transferencia de datos y el Cierre.
1. El HTTP Handshake (Actualización del protocolo)
Dado que los cortafuegos y enrutadores están configurados para permitir el tráfico web estándar en los puertos 80 (HTTP) y 443 (HTTPS), los WebSockets comienzan su viaje como una solicitud HTTP/1.1 estándar. Esto se denomina Upgrade Handshake.
La solicitud del cliente
El cliente envía una solicitud HTTP GET con cabeceras específicas solicitando un cambio de protocolo:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
Upgrade: websocketyConnection: Upgrade: Informan al servidor que el cliente desea cambiar de protocolo.Sec-WebSocket-Key: Un valor aleatorio de 16 bytes codificado en Base64. Se utiliza para demostrar que el servidor recibió el apretón de manos y entiende el protocolo WebSocket.Sec-WebSocket-Version: Especifica la versión del protocolo WebSocket (normalmente la 13).Origin: Utilizado por el servidor para decidir si permite la conexión (comprobación de seguridad contra sitios no autorizados).
La respuesta del servidor
Si el servidor admite WebSockets, valida la solicitud y responde con un código de estado HTTP 101 Switching Protocols:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
- Cómo calcula el servidor
Sec-WebSocket-Accept:- El servidor toma la cabecera
Sec-WebSocket-Keydel cliente (dGhlIHNhbXBsZSBub25jZQ==). - Lo concatena con un GUID mágico estándar:
"258EAFA5-E914-47DA-95CA-C5AB0DC85B11". - Calcula el hash SHA-1 de la cadena combinada.
- Codifica el hash resultante en Base64.
- Si el cliente verifica que este valor coincide con sus expectativas, el apretón de manos tiene éxito, la conexión HTTP cambia a un socket TCP sin procesar y ambas partes pasan al protocolo WebSocket.
- El servidor toma la cabecera
2. Fragmentación y transferencia de datos
A diferencia de HTTP, que envía cabeceras de texto plano seguidas de un cuerpo, los WebSockets transmiten datos en paquetes binarios estructurados llamados tramas (frames).
Una trama WebSocket tiene una cabecera muy ligera (que varía de 2 a 14 bytes) seguida de la carga útil (payload). Esta cabecera contiene:
- Bit FIN (1 bit): Indica si esta es la trama final de un mensaje.
- Opcode (4 bits): Define el tipo de trama:
0x1: Trama de texto (codificada en UTF-8)0x2: Trama binaria0x8: Solicitud de cierre de conexión0x9: Ping0xA: Pong
- Bit de máscara (1 bit): Especifica si los datos de la carga útil están enmascarados.
- Longitud de la carga útil: El tamaño de los datos.
- Clave de enmascaramiento (4 bytes): Requisito de seguridad crucial: Todas las tramas enviadas desde el cliente al servidor deben estar enmascaradas (mediante XOR) utilizando una clave aleatoria de 4 bytes. Esto evita que las cachés proxy lean el tráfico o realicen ataques de envenenamiento de caché. Las tramas de servidor a cliente no deben estar enmascaradas.
Heartbeats (Ping/Pong)
Para evitar que los enrutadores y equilibradores de carga cierren conexiones inactivas, cualquiera de las partes puede enviar una trama Ping. El lado receptor debe responder inmediatamente con una trama Pong que contenga la misma carga útil.
3. Cierre de la conexión
Para cerrar una conexión de forma limpia:
- Un par envía una trama
Closeque contiene un código de estado (por ejemplo,1000para un cierre normal,1006para un cierre anormal) y un motivo de texto opcional. - El otro par responde con su propia trama
Close. - El socket TCP subyacente se cierra.
Ejemplo de código: Implementación de WebSockets en Node.js
Para ver los WebSockets en acción, escribamos una aplicación simple en Node.js. Crearemos un servidor WebSocket local que retransmite cualquier mensaje que reciba, junto con un script de cliente para conectarse a él.
El servidor WebSocket (server.js)
const { WebSocketServer } = require('ws');
const http = require('http');
// 1. Crear un servidor HTTP estándar
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('HTTP Server running. Use WebSocket to connect.\n');
});
// 2. Acoplar un servidor WebSocket al servidor HTTP
const wss = new WebSocketServer({ server });
wss.on('connection', (ws, req) => {
const clientIp = req.socket.remoteAddress;
console.log(`[Servidor] Nuevo cliente conectado desde ${clientIp}`);
// Enviar un mensaje de bienvenida al cliente
ws.send(JSON.stringify({ type: 'welcome', message: '¡Conectado al servidor WebSocket de Ghaznix!' }));
// Escuchar mensajes entrantes de este cliente
ws.on('message', (message) => {
console.log(`[Servidor] Recibido: ${message}`);
try {
const data = JSON.parse(message);
ws.send(JSON.stringify({
type: 'echo',
message: `Eco del servidor: ${data.text.toUpperCase()}`,
timestamp: new Date().toISOString()
}));
} catch (e) {
ws.send(JSON.stringify({ type: 'error', message: 'Formato JSON no válido' }));
}
});
// Manejar desconexión del cliente
ws.on('close', (code, reason) => {
console.log(`[Servidor] Cliente desconectado (Código: ${code}, Razón: ${reason.toString() || 'Ninguna'})`);
});
ws.on('error', (error) => {
console.error(`[Servidor] Error de socket: ${error.message}`);
});
});
server.listen(8080, () => {
console.log('Servidor WebSocket escuchando en ws://localhost:8080');
});
El cliente del navegador (JavaScript del lado del cliente)
Puede ejecutar este cliente directamente en la consola de su navegador:
// 1. Establecer conexión con el servidor
const socket = new WebSocket('ws://localhost:8080');
// 2. Manejador de conexión abierta
socket.addEventListener('open', (event) => {
console.log('[Cliente] Conectado al servidor.');
const payload = JSON.stringify({ text: 'hola, servidor!' });
socket.send(payload);
console.log(`[Cliente] Enviado: ${payload}`);
});
// 3. Escuchar mensajes del servidor
socket.addEventListener('message', (event) => {
const response = JSON.parse(event.data);
console.log('[Cliente] Mensaje recibido del servidor:', response);
});
// 4. Escuchar el cierre de la conexión
socket.addEventListener('close', (event) => {
console.log(`[Cliente] Conexión cerrada (Código: ${event.code})`);
});
// 5. Escuchar errores
socket.addEventListener('error', (error) => {
console.error('[Cliente] Error de WebSocket:', error);
});
HTTP frente a WebSockets: Comparación detallada
| Característica | HTTP/1.1 | WebSockets |
|---|---|---|
| Comunicación | Unidireccional (iniciada por el cliente) | Bidireccional (cliente o servidor) |
| Modelo de conexión | Solicitud-Respuesta (corta duración) | Persistente (larga duración) |
| Sobrecarga | Alta (cabeceras enviadas con cada solicitud) | Muy baja (sobrecarga de trama mínima) |
| Estado | Sin estado | Con estado (se mantiene el contexto de la conexión) |
| Protocolo | http:// o https:// |
ws:// o wss:// |
| Ideal para | Obtención de documentos, API REST | Chats en tiempo real, paneles de control, feeds |
Consideraciones de seguridad para WebSockets
Dado que los WebSockets eluden el enrutamiento HTTP estándar después del apretón de manos, introducen vectores de seguridad únicos:
- Utilice WebSocket Secure (
wss://): Ejecute siempre WebSockets sobre TLS/SSL (puerto 443). WSS cifra la carga útil de la trama, evitando escuchas e interferencias de intermediarios. - Validación de origen: Los WebSockets no están restringidos por la política de mismo origen (SOP). Valide siempre la cabecera
Originen el servidor durante el apretón de manos para evitar el acceso no autorizado. - Autenticación en el Handshake: Autentique a los usuarios antes de establecer la conexión. Normalmente esto se hace pasando un token (como un JWT) en los parámetros de consulta o verificando las cookies de sesión.
- Sanitización de entradas: Trate cada mensaje recibido a través de WebSockets como una entrada no confiable. Valide y limpie las cargas útiles para evitar secuencias de comandos en sitios cruzados (XSS).
Resumen
Los WebSockets transformaron las aplicaciones web en tiempo real al eliminar la sobrecarga de la consulta HTTP tradicional. Al mantener una única conexión TCP persistente, permiten la mensajería bidireccional instantánea, impulsando los paneles de control en tiempo real, los juegos multijugador y las aplicaciones de chat actuales. Comprender la actualización de HTTP, la arquitectura de tramas y las prácticas de seguridad cruciales le garantizará la creación de servicios en tiempo real rápidos y seguros.
Explore más tutoriales y guías para desarrolladores en el Blog de Ghaznix →