El protocolo WebSocket, descrito en la especificación RFC 6455 proporciona una forma de intercambiar datos entre el navegador y el servidor a través de una conexión persistente. Los datos pueden ser pasados en ambas direcciones como «paquetes», sin romper la conexión y sin peticiones HTTP adicionales.

WebSocket es especialmente grande para los servicios que requieren un intercambio de datos continuo, por ejemplo, juegos en línea, sistemas de comercio en tiempo real, etc.

Un ejemplo sencillo

Para abrir una conexión websocket, necesitamos crear new WebSocket utilizando el protocolo especial ws en la url:

let socket = new WebSocket("ws://javascript.info");

También existe el protocolo encriptado wss://. Es como el HTTPS para los websockets.

Siempre prefiere wss://
wss:// El protocolo wss:// no sólo está encriptado, sino que es más fiable.

Eso es porque los datos de ws:// no están encriptados, visibles para cualquier intermediario. Los viejos servidores proxy no conocen WebSocket, pueden ver cabeceras «extrañas» y abortar la conexión.

Por otro lado, wss:// es WebSocket sobre TLS, (igual que HTTPS es HTTP sobre TLS), la capa de seguridad de transporte encripta los datos en el emisor y los desencripta en el receptor. Así que los paquetes de datos pasan encriptados a través de los proxies. No pueden ver lo que hay dentro y dejarlos pasar.

Una vez creado el socket, debemos escuchar los eventos en él. En total hay 4 eventos:

  • open – conexión establecida,
  • message – datos recibidos,
  • error – error de websocket,
  • close – conexión cerrada.

…Y si queremos enviar algo, entonces socket.send(data) lo hará.

Aquí tienes un ejemplo:

let socket = new WebSocket("wss://javascript.info/article/websocket/demo/hello");socket.onopen = function(e) { alert(" Connection established"); alert("Sending to server"); socket.send("My name is John");};socket.onmessage = function(event) { alert(` Data received from server: ${event.data}`);};socket.onclose = function(event) { if (event.wasClean) { alert(` Connection closed cleanly, code=${event.code} reason=${event.reason}`); } else { // e.g. server process killed or network down // event.code is usually 1006 in this case alert(' Connection died'); }};socket.onerror = function(error) { alert(` ${error.message}`);};

Para la demostración, hay un pequeño servidor server.js escrito en Node.js, para el ejemplo anterior, funcionando. Responde con «Hola desde el servidor, Juan», luego espera 5 segundos y cierra la conexión.

Así que verás los eventos openmessageclose.

Eso es realmente, ya podemos hablar de WebSocket. Bastante sencillo, ¿no?

Ahora vamos a hablar más en profundidad.

Abrir un websocket

Cuando se crea el new WebSocket(url) se empieza a conectar inmediatamente.

Durante la conexión el navegador (mediante cabeceras) pregunta al servidor: «¿Soporta Websocket?» Y si el servidor responde «sí», entonces la conversación continúa en el protocolo WebSocket, que no es para nada HTTP.

Aquí tienes un ejemplo de las cabeceras del navegador para la petición realizada por new WebSocket("wss://javascript.info/chat").

GET /chatHost: javascript.infoOrigin: https://javascript.infoConnection: UpgradeUpgrade: websocketSec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==Sec-WebSocket-Version: 13

  • Origin – el origen de la página del cliente, por ejemplo https://javascript.info. Los objetos WebSocket son cross-origin por naturaleza. No hay cabeceras especiales ni otras limitaciones. Los servidores antiguos no pueden manejar WebSocket de todos modos, así que no hay problemas de compatibilidad. Pero la cabecera Origin es importante, ya que permite al servidor decidir si hablar o no de WebSocket con este sitio web.
  • Connection: Upgrade – señala que el cliente quiere cambiar el protocolo.
  • Upgrade: websocket – el protocolo solicitado es «websocket».
  • Sec-WebSocket-Key – una clave aleatoria generada por el navegador para la seguridad.
  • Sec-WebSocket-Version – versión del protocolo WebSocket, 13 es la actual.
El handshake de WebSocket no se puede emular

No podemos usar XMLHttpRequest o fetch para hacer este tipo de peticiones HTTP, porque JavaScript no puede establecer estas cabeceras.

Si el servidor acepta cambiar a WebSocket, debería enviar el código 101 de respuesta:

101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=

Aquí Sec-WebSocket-Accept está Sec-WebSocket-Key, recodificado usando un algoritmo especial. El navegador lo utiliza para asegurarse de que la respuesta se corresponde con la petición.

Después, los datos se transfieren utilizando el protocolo WebSocket, veremos su estructura («frames») próximamente. Y eso no es HTTP en absoluto.

Extensiones y subprotocolos

Puede haber cabeceras adicionales Sec-WebSocket-Extensions y Sec-WebSocket-Protocol que describen extensiones y subprotocolos.

Por ejemplo:

  • Sec-WebSocket-Extensions: deflate-frame significa que el navegador soporta la compresión de datos. Una extensión es algo relacionado con la transferencia de los datos, funcionalidad que extiende el protocolo WebSocket. La cabecera Sec-WebSocket-Extensions es enviada automáticamente por el navegador, con la lista de todas las extensiones que soporta.

  • Sec-WebSocket-Protocol: soap, wamp significa que nos gustaría transferir no cualquier dato, sino los datos en protocolos SOAP o WAMP («The WebSocket Application Messaging Protocol»). Los subprotocolos WebSocket están registrados en el catálogo de la IANA. Por lo tanto, esta cabecera describe los formatos de datos que vamos a utilizar.

    Esta cabecera opcional se establece utilizando el segundo parámetro de new WebSocket. Es la matriz de subprotocolos, por ejemplo, si queremos usar SOAP o WAMP:

    let socket = new WebSocket("wss://javascript.info/chat", );

El servidor debe responder con una lista de protocolos y extensiones que acepta usar.

Por ejemplo, la petición:

GET /chatHost: javascript.infoUpgrade: websocketConnection: UpgradeOrigin: https://javascript.infoSec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==Sec-WebSocket-Version: 13Sec-WebSocket-Extensions: deflate-frameSec-WebSocket-Protocol: soap, wamp

Respuesta:

101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=Sec-WebSocket-Extensions: deflate-frameSec-WebSocket-Protocol: soap

Aquí el servidor responde que soporta la extensión «deflate-frame», y sólo SOAP de los subprotocolos solicitados.

Transferencia de datos

La comunicación WebSocket consiste en «frames» – fragmentos de datos, que pueden ser enviados desde cualquier lado, y pueden ser de varios tipos:

  • «text frames» – contienen datos de texto que las partes se envían mutuamente.
  • «marcos de datos binarios» – contienen datos binarios que las partes se envían mutuamente.
  • «marcos de ping/pong» se utilizan para comprobar la conexión, enviados desde el servidor, el navegador responde a estos automáticamente.
  • También hay «marco de cierre de conexión» y algunos otros marcos de servicio.
    • En el navegador, trabajamos directamente sólo con marcos de texto o binarios.

      El método WebSocket .send() puede enviar datos de texto o binarios.

      Una llamada socket.send(body) permite body en formato de cadena o binario, incluyendo BlobArrayBuffer, etc. No hace falta configurar nada: basta con enviarlo en cualquier formato.

      Cuando recibimos los datos, el texto siempre viene como cadena. Y para los datos binarios, podemos elegir entre los formatos Blob y ArrayBuffer.

      Eso lo establece la propiedad socket.binaryType, es "blob" por defecto, por lo que los datos binarios vienen como objetos Blob.

      Blob es un objeto binario de alto nivel, se integra directamente con <a><img> y otras etiquetas, por lo que es un valor por defecto sano. Pero para el procesamiento binario, para acceder a bytes de datos individuales, podemos cambiarlo a "arraybuffer":

      socket.binaryType = "arraybuffer";socket.onmessage = (event) => { // event.data is either a string (if text) or arraybuffer (if binary)};

      Limitación de velocidad

      Imagina que nuestra aplicación está generando muchos datos para enviar. Pero el usuario tiene una conexión de red lenta, quizás en un internet móvil, fuera de una ciudad.

      Podemos llamar a socket.send(data) una y otra vez. Pero los datos se almacenarán en el buffer (memoria) y se enviarán sólo tan rápido como la velocidad de la red lo permita.

      La propiedad socket.bufferedAmount almacena cuántos bytes permanecen en el buffer en este momento, esperando a ser enviados por la red.

      Podemos examinarla para ver si el socket está realmente disponible para su transmisión.

      // every 100ms examine the socket and send more data// only if all the existing data was sent outsetInterval(() => { if (socket.bufferedAmount == 0) { socket.send(moreData()); }}, 100);

      Conexión cerrada

      Normalmente, cuando una parte quiere cerrar la conexión (tanto el navegador como el servidor tienen los mismos derechos), envían una «trama de cierre de conexión» con un código numérico y una razón textual.

      El método para ello es:

      socket.close(, );

      • code es un código especial de cierre de WebSocket (opcional)
      • reason es una cadena que describe el motivo de cierre (opcional)
      • Entonces la otra parte en close manejador de eventos obtiene el código y el motivo, e.g.:

        // closing party:socket.close(1000, "Work complete");// the other partysocket.onclose = event => { // event.code === 1000 // event.reason === "Work complete" // event.wasClean === true (clean close)};

        Los valores de código más comunes:

        • 1000 – el cierre por defecto, normal (se utiliza si no se suministra un code),
        • 1006 – no hay forma de establecer dicho código manualmente, indica que se ha perdido la conexión (no hay marco de cierre).

        Hay otros códigos como:

        • 1001 – la fiesta se va, por ejemplo. el servidor se cierra, o un navegador abandona la página,
        • 1009 – el mensaje es demasiado grande para procesarlo,
        • 1011 – error inesperado en el servidor,
        • …y así sucesivamente.
          • La lista completa se puede encontrar en RFC6455, §7.4.1.

            Los códigos de WebSocket son algo así como los códigos HTTP, pero diferentes. En particular, cualquier código menor que 1000 está reservado, habrá un error si intentamos establecer tal código.

            // in case connection is brokensocket.onclose = event => { // event.code === 1006 // event.reason === "" // event.wasClean === false (no closing frame)};

      Estado de la conexión

      Para obtener el estado de la conexión, adicionalmente existe la propiedad socket.readyState con valores:

      • 0 – «CONNECTING»: la conexión aún no se ha establecido,
      • 1 – «OPEN»: comunicando,
      • 2 – «CLOSING»: la conexión se está cerrando,
      • 3 – «CLOSED»: la conexión está cerrada.

      Ejemplo de chat

      Revisemos un ejemplo de chat utilizando la API WebSocket del navegador y el módulo WebSocket de Node.js https://github.com/websockets/ws. Prestaremos la principal atención al lado del cliente, pero el servidor también es sencillo.

      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></div>

      Desde JavaScript queremos tres cosas:

  1. Abrir la conexión.
  2. En el envío del formulario – socket.send(message) para el mensaje.
  3. En el mensaje entrante – anexarlo a div#messages.

Aquí está el código:

let socket = new WebSocket("wss://javascript.info/article/websocket/chat/ws");// send message from the formdocument.forms.publish.onsubmit = function() { let outgoingMessage = this.message.value; socket.send(outgoingMessage); return false;};// message received - show the message in div#messagessocket.onmessage = function(event) { let message = event.data; let messageElem = document.createElement('div'); messageElem.textContent = message; document.getElementById('messages').prepend(messageElem);}

El código del lado del servidor está un poco fuera de nuestro alcance. Aquí usaremos Node.js, pero no es necesario. Otras plataformas también tienen sus medios para trabajar con WebSocket.

El algoritmo del lado del servidor será:

  1. Crear clients = new Set() – un conjunto de sockets.
  2. Por cada websocket aceptado, añadirlo al conjunto clients.add(socket) y configurar message listener de eventos para obtener sus mensajes.
  3. Cuando se recibe un mensaje: iterar sobre los clientes y enviarlo a todos.
  4. Cuando se cierra una conexión: clients.delete(socket).
const ws = new require('ws');const wss = new ws.Server({noServer: true});const clients = new Set();http.createServer((req, res) => { // here we only handle websocket connections // in real project we'd have some other code here to handle non-websocket requests 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); // max message length will be 50 for(let client of clients) { client.send(message); } }); ws.on('close', function() { clients.delete(ws); });}

Aquí tienes el ejemplo de funcionamiento:

También puedes descargarlo (botón superior derecho del iframe) y ejecutarlo en local. Eso sí, no olvides instalar Node.js y npm install ws antes de ejecutarlo.

Resumen

WebSocket es una forma moderna de tener conexiones persistentes navegador-servidor.

  • Los WebSockets no tienen limitaciones de origen cruzado.
  • Están bien soportados en los navegadores.
  • Pueden enviar/recibir cadenas y datos binarios.
    • La API es sencilla.

      Métodos:

      • socket.send(data),
      • socket.close(, ).

      Eventos:

      • open,
      • message,
      • error, close.

      WebSocket por sí mismo no incluye reconexión, autenticación y muchos otros mecanismos de alto nivel. Así que hay bibliotecas cliente/servidor para eso, y también es posible implementar estas capacidades manualmente.

      A veces, para integrar WebSocket en un proyecto existente, la gente ejecuta el servidor WebSocket en paralelo con el servidor HTTP principal, y comparten una única base de datos. Las peticiones a WebSocket utilizan wss://ws.site.com, un subdominio que lleva al servidor WebSocket, mientras que https://site.com va al servidor HTTP principal.

      Seguramente, otras formas de integración también son posibles.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *