El protocolo WebSocket, descrito en la especificación RFC 6455, brinda una forma de intercambiar datos entre el navegador y el servidor por medio de una conexión persistente. Los datos pueden ser pasados en ambas direcciones como paquetes âpacketsâ, sin cortar la conexión y sin pedidos adicionales de HTTP âHTTP-requestsâ.
WebSocket es especialmente bueno para servicios que requieren intercambio de información continua, por ejemplo juegos en lÃnea, sistemas de negocios en tiempo real, entre otros.
Un ejemplo simple
Para abrir una conexión websocket, necesitamos crearla new WebSocket usando el protocolo especial ws en la url:
let socket = new WebSocket("ws://javascript.info");
También hay una versión encriptada wss://. Equivale al HTTPS para los websockets.
wss://El protocolo wss:// no solamente está encriptado, también es más confiable.
Esto es porque los datos en ws:// no están encriptados y son visibles para cualquier intermediario. Entonces los servidores proxy viejos que no reconocen el protocolo WebSocket podrÃan interpretar los datos como cabeceras âextrañasâ y abortar la conexión.
En cambio wss:// es WebSocket sobre TLS (al igual que HTTPS es HTTP sobre TLS), la seguridad de la capa de transporte encripta los datos en el envÃo y los desencripta en el destino. AsÃ, los paquetes de datos pasan encriptados a través de los proxy, estos servidores no pueden ver lo que hay dentro y los dejan pasar.
Una vez que el socket es creado, debemos escuchar los eventos que ocurren en él. Hay en total 4 eventos:
openâ conexión establecida,messageâ datos recibidos,errorâ error en websocket,closeâ conexión cerrada.
â¦Y si queremos enviar algo, socket.send(data) lo hará.
Aquà un ejemplo:
let socket = new WebSocket("wss://javascript.info/article/websocket/demo/hello");
socket.onopen = function(e) {
alert("[open] Conexión establecida");
alert("Enviando al servidor");
socket.send("Mi nombre es John");
};
socket.onmessage = function(event) {
alert(`[message] Datos recibidos del servidor: ${event.data}`);
};
socket.onclose = function(event) {
if (event.wasClean) {
alert(`[close] Conexión cerrada limpiamente, código=${event.code} motivo=${event.reason}`);
} else {
// ej. El proceso del servidor se detuvo o la red está caÃda
// event.code es usualmente 1006 en este caso
alert('[close] La conexión se cayó');
}
};
socket.onerror = function(error) {
alert(`[error]`);
};
Para propósitos de demostración, tenemos un pequeño servidor server.js, escrito en Node.js, ejecutándose para el ejemplo de arriba. Este responde con âHello from server, Johnâ, espera 5 segundos, y cierra la conexión.
Entonces verás los eventos open â message â close.
Eso es realmente todo, ya podemos conversar con WebSocket. Bastante simple, ¿no es cierto?
Ahora hablemos más en profundidad.
Abriendo un websocket
Cuando se crea new WebSocket(url), comienza la conexión de inmediato.
Durante la conexión, el navegador (usando cabeceras o âheaderâ) le pregunta al servidor: â¿Soportas Websockets?â y si si el servidor responde âSÃâ, la comunicación continúa en el protocolo WebSocket, que no es HTTP en absoluto.
Aquà hay un ejemplo de cabeceras de navegador para una petición hecha por new WebSocket("wss://javascript.info/chat").
GET /chat
Host: javascript.info
Origin: https://javascript.info
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
Originâ La página de origen del cliente, ej.https://javascript.info. Los objetos WebSocket son cross-origin por naturaleza. No existen las cabeceras especiales ni otras limitaciones. De cualquier manera los servidores viejos son incapaces de manejar WebSocket, asi que no hay problemas de compatibilidad. Pero la cabeceraOrigines importante, pues habilita al servidor decidir si permite o no la comunicación WebSocket con el sitio web.Connection: Upgradeâ señaliza que el cliente quiere cambiar el protocolo.Upgrade: websocketâ el protocolo requerido es âwebsocketâ.Sec-WebSocket-Keyâ una clave de aleatoria generada por el navegador, usada para asegurar que el servidor soporta el protocolo WebSocket. Es aleatoria para evitar que servidores proxy almacenen en cache la comunicación que sigue.Sec-WebSocket-Versionâ Versión del protocolo WebSocket, 13 es la actual.
No podemos usar XMLHttpRequest o fetch para hacer este tipo de peticiones HTTP, porque JavaScript no tiene permitido establecer esas cabeceras.
Si el servidor concede el cambio a WebSocket, envÃa como respuesta el código 101:
101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
Aquà Sec-WebSocket-Accept es Sec-WebSocket-Key, recodificado usando un algoritmo especial. Al verlo, el navegador entiende que el servidor realmente soporta el protocolo WebSocket.
A continuación los datos son transferidos usando el protocolo WebSocket. Pronto veremos su estructura (âframesâ, marcos o cuadros en español). Y no es HTTP en absoluto.
Extensiones y subprotocolos
Puede tener las cabeceras adicionales Sec-WebSocket-Extensions y Sec-WebSocket-Protocol que describen extensiones y subprotocolos.
Por ejemplo:
-
Sec-WebSocket-Extensions: deflate-framesignifica que el navegador soporta compresión de datos. una extensión es algo relacionado a la transferencia de datos, funcionalidad que extiende el protocolo WebSocket. La cabeceraSec-WebSocket-Extensionses enviada automáticamente por el navegador, con la lista de todas las extensiones que soporta. -
Sec-WebSocket-Protocol: soap, wampsignifica que queremos transferir no cualquier dato, sino datos en protocolos SOAP o WAMP (âThe WebSocket Application Messaging Protocolâ). Los subprotocolos de WebSocket están registrados en el catálogo IANA. Entonces, esta cabecera describe los formatos de datos que vamos a usar.Esta cabecera opcional se establece usando el segundo parámetro de
new WebSocket, que es el array de subprotocolos. Por ejemplo, si queremos usar SOAP o WAMP:let socket = new WebSocket("wss://javascript.info/chat", ["soap", "wamp"]);
El servidor deberÃa responder con una lista de protocolos o extensiones que acepta usar.
Por ejemplo, la petición:
GET /chat
Host: javascript.info
Upgrade: websocket
Connection: Upgrade
Origin: https://javascript.info
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap, wamp
Respuesta:
101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap
Aquà el servidor responde que soporta la extensión âdeflate-frameâ, y únicamente SOAP de los subprotocolos solicitados.
Transferencia de datos
La comunicación WebSocket consiste de âframesâ (cuadros) de fragmentos de datos, que pueden ser enviados de ambos lados y pueden ser de varias clases:
- âtext framesâ â contiene datos de texto que las partes se mandan entre sÃ.
- âbinary data framesâ â contiene datos binarios que las partes se mandan entre sÃ.
- âping/pong framesâ son usados para testear la conexión; enviados desde el servidor, el navegador responde automáticamente.
- También existe âconnection close frameâ, y algunos otros frames de servicio.
En el navegador, trabajamos directamente solamente con frames de texto y binarios.
El método WebSocket .send() puede enviar tanto datos de texto como binarios.
Una llamada socket.send(body) permite en body datos en formato string o binarios, incluyendo Blob, ArrayBuffer, etc. No se requiere configuración: simplemente se envÃan en cualquier formato.
Cuando recibimos datos, el texto siempre viene como string. Y para datos binarios, podemos elegir entre los formatos Blob y ArrayBuffer.
Esto se establece en la propiedad socket.binaryType, que es "blob" por defecto y entonces los datos binarios vienen como objetos Blob.
Blob es un objeto binario de alto nivel que se integra directamente con <a>, <img> y otras etiquetas, asà que es una opción predeterminada saludable. Pero para procesamiento binario, para acceder a bytes individuales, podemos cambiarlo a "arraybuffer":
socket.binaryType = "arraybuffer";
socket.onmessage = (event) => {
// event.data puede ser string (si es texto) o arraybuffer (si es binario)
};
Limitaciones de velocidad
Supongamos que nuestra app está generando un montón de datos para enviar. Pero el usuario tiene una conexión de red lenta, posiblemente internet móvil fuera de la ciudad.
Podemos llamar socket.send(data) una y otra vez. Pero los datos serán acumulados en memoria (en un âbufferâ) y enviados solamente tan rápido como la velocidad de la red lo permita.
La propiedad socket.bufferedAmount registra cuántos bytes quedan almacenados (âbufferedâ) hasta el momento esperando a ser enviados a la red.
Podemos examinarla para ver si el âsocketâ está disponible para transmitir.
// examina el socket cada 100ms y envÃa más datos
// solamente si todos los datos existentes ya fueron enviados
setInterval(() => {
if (socket.bufferedAmount == 0) {
socket.send(moreData());
}
}, 100);
Cierre de conexión
Normalmente, cuando una parte quiere cerrar la conexión (servidor o navegador, ambos tienen el mismo derecho), envÃa un âframe de cierre de conexiónâ con un código numérico y un texto con el motivo.
El método para eso es:
socket.close([code], [reason]);
codees un código especial de cierre de WebSocket (opcional)reasones un string que describe el motivo de cierre (opcional)
Entonces el manejador del evento close de la otra parte obtiene el código y el motivo, por ejemplo:
// la parte que hace el cierre:
socket.close(1000, "Work complete");
// la otra parte:
socket.onclose = event => {
// event.code === 1000
// event.reason === "Work complete"
// event.wasClean === true (clean close)
};
Los códigos más comunes:
1000â cierre normal. Es el predeterminado (usado si no se proporcionacode),1006â no hay forma de establecerlo manualmente, indica que la conexión se perdió (no hay frame de cierre).
Hay otros códigos como:
1001â una parte se va, por ejemplo el server se está apagando, o el navegador deja la página,1009â el mensaje es demasiado grande para procesar,1011â error inesperado en el servidor,- â¦y asÃ.
La lista completa puede encontrarse en RFC6455, §7.4.1.
Los códigos de WebSocket son como los que hay de HTTP, pero diferentes. En particular, los códigos menores a 1000 son reservados, habrá un error si tratamos de establecerlos.
// en caso de conexión que se rompe
socket.onclose = event => {
// event.code === 1006
// event.reason === ""
// event.wasClean === false (no hay un frame de cierre)
};
Estado de la conexión
Para obtener el estado (state) de la conexión, tenemos la propiedad socket.readyState con valores:
0â âCONNECTINGâ: la conexión aún no fue establecida,1â âOPENâ: comunicando,2â âCLOSINGâ: la conexión se está cerrando,3â âCLOSEDâ: la conexión está cerrada.
Ejemplo Chat
Revisemos un ejemplo de chat usando la API WebSocket del navegador y el módulo WebSocket de Node.js https://github.com/websockets/ws. Prestaremos atención al lado del cliente, pero el servidor es igual de simple.
HTML: necesitamos un <form> para enviar mensajes y un <div> para los mensajes entrantes:
<!-- message form -->
<form name="publish">
<input type="text" name="message">
<input type="submit" value="Send">
</form>
<!-- div with messages -->
<div id="messages"></div>
De JavaScript queremos tres cosas:
- Abrir la conexión.
- Ante el âsubmitâ del form, enviar
socket.send(message)el mensaje. - Al llegar un mensaje, agregarlo a
div#messages.
Aquà el código:
let socket = new WebSocket("wss://javascript.info/article/websocket/chat/ws");
// enviar el mensaje del form
document.forms.publish.onsubmit = function() {
let outgoingMessage = this.message.value;
socket.send(outgoingMessage);
return false;
};
// mensaje recibido - muestra el mensaje en div#messages
socket.onmessage = function(event) {
let message = event.data;
let messageElem = document.createElement('div');
messageElem.textContent = message;
document.getElementById('messages').prepend(messageElem);
}
El código de servidor está fuera de nuestro objetivo. Aquà usaremos Node.js, pero no necesitas hacerlo. Otras plataformas también tienen sus formas de trabajar con WebSocket.
El algoritmo de lado de servidor será:
- Crear
clients = new Set()â un conjunto de sockets. - Para cada websocket aceptado, sumarlo al conjunto
clients.add(socket)y establecer un âevent listenerâmessagepara obtener sus mensajes. - Cuando un mensaje es recibido: iterar sobre los clientes y enviarlo a todos ellos.
- Cuando una conexión se cierra:
clients.delete(socket).
const ws = new require('ws');
const wss = new ws.Server({noServer: true});
const clients = new Set();
http.createServer((req, res) => {
// aquà solo manejamos conexiones websocket
// en proyectos reales tendremos también algún código para manejar peticiones no websocket
wss.handleUpgrade(req, req.socket, Buffer.alloc(0), onSocketConnect);
});
function onSocketConnect(ws) {
clients.add(ws);
ws.on('message', function(message) {
message = message.slice(0, 50); // la longitud máxima del mensaje será 50
for(let client of clients) {
client.send(message);
}
});
ws.on('close', function() {
clients.delete(ws);
});
}
Aquà está el ejemplo funcionando:
Puedes descargarlo (botón arriba/derecha en el iframe) y ejecutarlo localmente. No olvides instalar Node.js y npm install ws antes de hacerlo.
Resumen
WebSocket es la forma moderna de tener conexiones persistentes entre navegador y servidor .
- Los WebSockets no tienen limitaciones âcross-originâ.
- Están muy bien soportados en los navegadores.
- Pueden enviar y recibir datos string y binarios.
La API es simple.
Métodos:
socket.send(data),socket.close([code], [reason]).
Eventos:
open,message,error,close.
El WebSocket por sà mismo no incluye reconexión, autenticación ni otros mecanismos de alto nivel. Hay librerÃas cliente/servidor para eso, y también es posible implementar esas capacidades manualmente.
A veces, para integrar WebSocket a un proyecto existente, se ejecuta un servidor WebSocket en paralelo con el servidor HTTP principal compartiendo la misma base de datos. Las peticiones a WebSocket usan wss://ws.site.com, un subdominio que se dirige al servidor de WebSocket mientras que https://site.com va al servidor HTTP principal.
Seguro, otras formas de integración también son posibles.
Comentarios
<code>, para varias lÃneas â envolverlas en la etiqueta<pre>, para más de 10 lÃneas â utilice una entorno controlado (sandbox) (plnkr, jsbin, codepenâ¦)