Acompaña el evento final de la Maratón Behind the Code 2020: 05/12 - Online ¡Inscríbete ahora!

¿Qué son los Websockets?

¿Qué son los Websockets?

Un poco de historia

Para entender el beneficio que aportan los WebSockets es necesario recordar que hasta hace poco, crear aplicaciones web que se pudieran considerar realmente dinámicas era virtualmente imposible. La World Wide Web había sido diseñada originalmente para consultar información estática almacenada en archivos HTML. Navegar de una página a otra solo se podía hacer siguiendo ligas. Posteriormente, los desarrolladores empezaron a crear sitios web un poco más interactivos, mediante el uso de formas. La información proporcionada por el usuario se mandaba al servidor (a un CGI o a un servlet) y el servidor contestaba generando una nueva página dinámica. Eso sirvió para crear todo tipo de aplicaciones básicas, pero los desarrolladores no estaban satisfechos, porque el sistema seguía siendo muy ineficiente. El hecho de que cada vez que se tuviera que interactuar con el servidor hubiera que cargar de nuevo toda la página hacía que los usuarios percibieran que las aplicaciones web eran mucho más lentas que sus equivalentes nativas y bastante menos atractivas. Para resolver el problema, Microsoft ideó una solución. La empresa creó una nueva función JavaScript, XMLHttpRequest, la cual añadió a su navegador Internet Explorer. Esta función, aún muy utilizada actualmente, permite invocar un URL (pasando parámetros de forma opcional) y obtener un resultado del servidor, el cual se puede utilizar para actualizar la página (usando el DOM o Document Object-Model), sin necesidad de volver a cargarla. Mediante el uso de XMLHttpRequest, Microsoft pudo crear un cliente web (Outlook Web Access) para su sistema de correo que funcionaba de forma muy similar a Outlook sobre Windows. El éxito fue inmediato y los demás browsers no tardaron en adoptar esa función, que se volvió rápidamente estándar.

Esta innovación creó toda una nueva generación de aplicaciones web que se conoció como AJAX (Asynchronous JavaScript and XML, un nombre pegadizo pero engañoso, porque en realidad no se necesita usar XML al invocar la función XMLHttpRequest). Las aplicaciones AJAX permitieron que las aplicaciones web se parecieran cada vez más a las aplicaciones nativas y lograron que cada vez pasemos más tiempo dentro del browser y que usemos menos aplicaciones nativas. De hecho, gracias a este fenómeno es cada vez más viable usar dispositivos que solo pueden ejecutar aplicaciones web, como lo demuestra el éxito de las Chromebooks de Google y la existencia de los teléfonos celulares basados en Firefox OS que solo pueden ejecutar aplicaciones basadas en tecnologías web.

Sin embargo, a pesar de todas sus virtudes, la función XMLHttpRequest tiene una serie de limitantes importantes. La principal es que la interacción con el servidor debe iniciarse desde el cliente, ya sea por una interacción desde el interfaz gráfico (por ejemplo apretando un botón) o a través de un temporizador. Esto significa que si queremos actualizar las páginas conectadas a nuestro servidor como respuesta a un evento enviado por el servidor, simplemente no es posible hacerlo usando XMLHttpRequest. Por eso, cuando seguimos un evento deportivo por Internet, tenemos que estar recargando la página una y otra vez, para ver si ha cambiado el marcador. Ese sistema de estar poleando el servidor una y otra vez es sumamente ineficiente y la carga de peticiones que genera obliga a invertir mucho en infraestructura, un gasto que se podría evitar. Por eso era necesario buscar otra solución mucho más elegante.

WebSockets

Se trata de un concepto muy similar al de los sockets TCP/IP y que permite que una aplicación web establezca un canal de comunicación bi-direccional persistente entre la capa de presentación HTML en el browser y el servidor. Los WebSockets utilizan los puertos estándar de http y https (80 y 443 respectivamente) y la especificación fue diseñada para evitar problemas causados por el uso de proxies y de firewalls. Es importante recalcar que aunque los WebSockets usan el puerto 80 al igual que HTTP, usan un protocolo de comunicación distinto, razón por la cual se usa ws:// en la URL en lugar de http://. También es posible encriptar la comunicación usando el protocolo wss://, el cual usará el puerto 443 (el mismo que HTTPS).

En el cliente usaremos JavaScript para interactuar con los WebSockets en la página web y del lado del servidor se puede usar una variedad de lenguajes de programación, entre los cuales están JavaScript (usado tras bambalinas por Node-Red, la tecnología que usaremos en este ejercicio) o Java (a partir de JEE 7).

Posibles problemas

Sin embargo, a pesar de las precauciones que tomaron los desarrolladores del estándar, es posible encontrarnos con problemas porque los administradores de los firewalls y/o de los sistemas de detección o prevención de intrusos (IDS/IPS) pueden haber decidido bloquear el uso de websockets. ¿Porqué iban a querer hacer eso? Es un tema de seguridad. Si bien los websockets no representan una amenaza especial, son una forma adicional de poder comunicarse con el exterior. En una época en la que la mayoría de las amenazas provienen de lo que se conoce como Advanced Persistance Threats (APT), programas que se ejecutan dentro de la red de las víctimas y que intentan recabar el máximo de información para enviarla de forma discreta al exterior donde los hackers la están esperando, un canal más de comunicaciones que se debe monitorear siempre es visto con recelo por parte de los responsables de seguridad.

Por ese motivo, considerando que es probable que algunos usuarios no podrán usar websockets para comunicarse con el servidor, es recomendable detectar una posible falla de conexión para al menos avisar del problema o mejor aún, dar a los usuarios de la aplicación web una alternativa, usando otra tecnología como XMLHttpRequest que no tiene el riesgo de estar bloqueada.

Pueden probar fácilmente si este ejercicio va a funcionar en su máquina, conectándose a la página https://www.websocket.org/echo.html (external link)

página de prueba, con los campos que se deben completar

¿Cómo funcionan los WebsSockets?

Modelo de programación

Los WebSockets son muy fáciles de utilizar, tal y como veremos a continuación. En realidad solo debemos conocer dos grupos de funciones. Por un lado están aquellas con las que vamos a administrar el ciclo de vida del objeto y por el otro, los callbacks o sea las funciones que el browser va a mandar invocar cuando detecte un evento relacionado con el websocket.

El ciclo de vida de los websockets

El primer paso consiste en crear el WebSocket, normalmente al terminar de cargarse la página, dentro de la función onload() del objeto window.

    var socket = new WebSocket(‘ws://www.example.com/socketserver');

Podrá observar que lo único que necesitamos pasar como argumento es el url al que nos vamos a conectar. Cuando trabajemos sobre IBM Cloud, el url se dividirá en dos partes, el url de nuestro servidor (por ejemplo «Node-RED-prueba1.mybluemix.net» y la ruta que definiremos para el websocket (por ejemplo «/ws/misocket»).

Por motivos de seguridad, la especificación obliga que los navegadores solo puedan abrir un websocket. La idea es evitar posibles ataques de tipo DoS (Negación de Servicio) desde un browser. A pesar de ello, no todos los navegadores han implementado esta limitación. Sin embargo, es importante conocerla, porque de otra manera podría resultar incomprensible porqué un determinado programa funciona en un browser y en otro no. Otra consideración a tomar en cuenta es que si una página web se accesa usando el protocolo HTTPS, deberá usar forzosamente el protocolo wss para conectarse con el servidor. Esto es lógico porque no tiene sentido tener páginas seguras, con elementos inseguros, pero vale la pena recalcarlo.

A menos de que nuestra página solo esté buscando recibir mensajes del servidor, en algún momento tendremos que mandar información al servidor usando la función send, tal y como se muestra a continuación:

    socket.send(”Esta es una prueba”);

En nuestro ejemplo, mandaremos una cadena de caracteres al servidor. Sin embargo, utilizando tan solo un poco de JavaScript podemos intercambiar datos en formato JSON con el servidor, de forma muy natural. Veamos un ejemplo:

    var mensaje = {
            nombre: “Juan”,
            apellido: “Arbeloa”,
            edad: 27
        };

        socket.send(JSON.stringify(mensaje));

A efectos prácticos, este ejemplo es similar al anterior, porque en realidad también estamos mandando una cadena de caracteres, solo que en este caso, esa cadena representa un objeto JSON que puede ser convertido de nuevo a un objeto JavaScript muy fácilmente en su punto de destino tal y como veremos más adelante.

Finalmente, es posible que en algún momento desee cerrar la conexión hacia el servidor, lo cual se hace de forma lógica, tal y como se muestra a continuación:

socket.close();

Si la página web no cierra el websocket de forma explícita, no pasa nada, porque cuando el usuario se mueva a otra página, el servidor detectará automáticamente que el cliente ya no está conectado y cerrará la conexión. Esto es algo que es responsabilidad exclusiva del servidor, el cual debe estar comprobando a intervalos constantes que los clientes siguen conectados. Afortunadamente, eso es algo que hace por nosotros NODE-Red de forma automática, lo que simplifica el problema de forma significativa.

Callbacks para manejar los eventos

Los WebSockets funcionan de forma asíncrona. Esto significa que una aplicación nunca se quedará bloqueada en una línea de código esperando a que le llegue un mensaje del servidor. En lugar de eso, será el navegador el que nos avise cuando llegue un mensaje, invocando una función callback en la cual nosotros podremos procesarlo. Esto es importante porque permite evitar que la página web deje de responder a las interacciones con el usuario mientras se interactúa con el servidor.

En JavaScript, las funciones son objetos. Esto significa que son del tipo Object y que es posible asignar a una variable una función, como si de cualquier otro tipo de objeto se tratara. De hecho, las funciones no solo pueden ser almacenadas en variables, sino que además, pueden pasarse como argumento a una función o ser devueltas como resultado de la invocación de una función. Esta característica interesantísima de JavaScript, es la que se explota para implementar las funciones de callback que invocará el navegador cuando un WebSocket reciba un evento.

El objeto WebSocket tiene las siguientes propiedades:

  • onmessage
  • onopen
  • onclose
  • onerror

La idea es asignar a esos atributos funciones, las cuales serán invocadas cuando se produzcan los eventos que queremos detectar. En el siguiente ejemplo, asignamos al atributo onmessage una función anónima (se le llama de esta manera porque, como pueden observar, no tiene nombre).

    // Gestión de la recepción de mensajes mandados por el servidor.
  socket.onmessage = function(event) {
    var message = event.data;
    …
 };

Cuando se crea un WebSocket, ninguno de los atributos del objeto tiene valor alguno, por lo que es necesario crearlas si queremos recibir los eventos. Tengan en cuenta que como JavaScript, a diferencia de otros lenguajes como C o Java, no es strongly typed (es decir que no se tiene que definir de antemano el tipo de las variables y que el compilador verifica que no haya errores de confusión de tipos), nada impide que escribamos el siguiente código:

    socket.onmessage = “Hola”;

Esa línea de código está claramente mal porque el browser espera que el atributo onmessage contenga una función, no una cadena de caracteres. Para evitar problemas, internamente, antes de invocar la función onmessage, el navegador ejecutará un control para verificar que los atributos que deban contener funciones callback realmente contengan funciones. El código ejecutado por el browser internamente probablemente se parezca mucho al código que se muestra a continuación:

    if (typeof socket.onmessage === "function") {
      // Podemos invocar la función porque hemos comprobado que el atributo
      // onmessage realmente contiene una función

      socket.onmessage(evento);
   }