Explica el handshake del protocolo WebSocket, ¿y cuándo hace falta de verdad una conexión persistente?
Un WebSocket nace como una petición HTTP normal y solo se convierte en otra cosa cuando ambos lados aceptan dejar de hablar HTTP — el handshake te dice exactamente qué estás sacrificando a cambio.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Explica el handshake del protocolo WebSocket, ¿y cuándo hace falta de verdad una conexión persistente?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#El handshake de actualización
Una conexión WebSocket comienza como una petición HTTP GET completamente normal. El cliente le pide al servidor cambiar de protocolo sobre la misma conexión TCP en lugar de abrir una nueva:
GET /chat HTTP/1.1Host: api.example.comUpgrade: websocketConnection: UpgradeSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==Sec-WebSocket-Version: 13Origin: https://app.example.com
Sec-WebSocket-Key son 16 bytes aleatorios, codificados en base64, generados de cero por el cliente para este handshake. No es una credencial de seguridad — su función es probar que el servidor que recibe la petición realmente entiende el protocolo WebSocket (y no es, por ejemplo, un proxy de caché que reenvió a ciegas la cabecera Upgrade), y darle al navegador una defensa contra un proxy mal comportado que reproduzca una respuesta HTTP capturada como si fuera un handshake WebSocket real.
El servidor demuestra que entendió combinando esa clave con un GUID fijo definido por el protocolo, aplicando un hash, y devolviendo el resultado:
HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
El cálculo exacto es: base64(sha1(Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")). Ese GUID está fijo en el estándar (RFC 6455) — todos los servidores WebSocket del mundo usan el mismo literal. Está ahí únicamente para que ambos lados demuestren que implementan de verdad el protocolo WebSocket, no porque sea secreto.
const crypto = require('crypto');function computeAcceptValue(clientKey) {const MAGIC_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';return crypto.createHash('sha1').update(clientKey + MAGIC_GUID).digest('base64');}console.log(computeAcceptValue('dGhlIHNhbXBsZSBub25jZQ=='));// s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
En cuanto llega la respuesta 101 Switching Protocols, la conexión TCP deja de hablar HTTP por completo. A partir de ahí ambos lados intercambian frames WebSocket — un pequeño formato binario que lleva un opcode (texto, binario, ping, pong, close), una longitud de payload y, solo en los frames de cliente a servidor, una clave de máscara aplicada al payload, que existe para impedir que una página web maliciosa fabrique secuencias de bytes que un proxy ingenuo situado entre navegador y servidor pudiera malinterpretar como peticiones válidas. Desde este punto la conexión es full-duplex: cualquiera de los dos lados puede enviar un frame en cualquier momento, sin necesidad de emparejar petición y respuesta.
#Un servidor y un cliente mínimos
// server.js — Node, usando el paquete "ws"const { WebSocketServer } = require('ws');const wss = new WebSocketServer({ port: 8080 });wss.on('connection', (socket, request) => {console.log('Cliente conectado desde', request.socket.remoteAddress);socket.on('message', (data) => {const message = JSON.parse(data.toString());if (message.type === 'subscribe') {socket.send(JSON.stringify({ type: 'ack', channel: message.channel }));}});const heartbeat = setInterval(() => {if (socket.readyState === socket.OPEN) socket.ping();}, 30000);socket.on('close', (code, reason) => {clearInterval(heartbeat);console.log(`Conexión cerrada: ${code} ${reason}`);});});
// client.js — corre en el navegador, sin ninguna libreríaconst socket = new WebSocket('wss://api.example.com/chat');socket.addEventListener('open', () => {socket.send(JSON.stringify({ type: 'subscribe', channel: 'orders:42' }));});socket.addEventListener('message', (event) => {const payload = JSON.parse(event.data);console.log('Recibido:', payload);});socket.addEventListener('close', (event) => {console.log(`Cerrado: ${event.code} ${event.reason}`);});
Fíjate en los frames de control ping/pong en el código del servidor — existen porque una conexión TCP inactiva se ve exactamente igual, desde el punto de vista de la aplicación, si el otro lado está bien y simplemente callado, o si la máquina del otro lado se cayó y la red nunca te avisó. Un heartbeat es cómo detectas el segundo caso antes de que tu balanceador de carga o una lista de suscripciones en memoria acumule conexiones muertas en silencio.
#Cuándo hace falta esto de verdad y cuándo no
Los WebSockets se ganan su costo cuando el servidor necesita empujar datos que el cliente no pidió, con una frecuencia o latencia donde abrir una petición HTTP nueva por mensaje sería un desperdicio o demasiado lento: chat en vivo, edición colaborativa (varios cursores escribiendo en el mismo documento), estado de un juego multijugador, tickers de trading en vivo, o dashboards que necesitan actualizarse en menos de un segundo. En esos casos la alternativa —el cliente preguntando cada segundo "¿hay algo nuevo?"— o desperdicia peticiones cuando no cambió nada, o suma hasta un segundo de latencia cuando sí cambió.
Muy a menudo no hace falta uno. Si las actualizaciones son ocasionales (cada 30 segundos o menos frecuente), el polling simple es más sencillo y sin estado. Si el servidor necesita empujar pero el cliente nunca responde, Server-Sent Events (una única respuesta HTTP de larga duración transmitida como text/event-stream) suele ser la mejor herramienta: es HTTP normal, así que se multiplexa sin problemas sobre HTTP/2, tiene reconexión automática incorporada en la API EventSource del navegador, y no necesita ningún manejo especial de balanceadores de carga o proxies corporativos como a veces sí necesita una actualización TCP cruda. Y si la interacción es fundamentalmente de petición/respuesta —"tráeme este recurso"— un REST normal sobre HTTP es más simple, cacheable y escalable sin estado de una forma que una conexión WebSocket no lo es.
Ese último punto es el costo real de elegir WebSockets: hacen que tu servidor tenga estado. Una vez que un cliente está atado a un proceso de servidor específico por un socket abierto, una petición para los datos de ese cliente tiene que enrutarse de vuelta a ese proceso — no puedes simplemente hacer round-robin detrás de un balanceador sin estado como sí puedes con HTTP. Escalar horizontalmente suele implicar sesiones "sticky" más un backplane de pub/sub (Redis, Kafka, un servicio dedicado de fan-out de WebSockets) para que un mensaje publicado en cualquier instancia del servidor llegue al suscriptor correcto sin importar qué instancia tenga su socket. Cada conexión abierta también ocupa un descriptor de archivo y algo de memoria en el servidor mientras esté viva, así que un servicio con unos cientos de miles de conexiones inactivas tiene un perfil operativo muy distinto al de uno que sirve tráfico de petición/respuesta sin estado.