WebSockets con Java y Tomcat 7

WebSockets con Java y Tomcat 7.


0. Índice de contenidos.


1. Introducción

WebSockets es una especificación relativamente nueva pero que cada vez va sonando con más fuerza de la mano de eso a lo que conocemos como HTML5. Esta especificación se basa en un canal de comunicación bidireccional entre un cliente y un servidor, mediante el cual pueden enviarse mensajes de un sentido a otro en cualquier momento sin necesidad de que haya una petición de por medio.

Esta especificación está especialmente dirigida a las denominadas “aplicaciones en tiempo real”, que son aquellas en las que el cliente puede estar informado de todo aquello que sucede en el sistema desde el mismo momento en que se produce un cambio. Ejemplos de este tipo de aplicaciones pueden ser: juegos multijugador, aplicaciones de monitorización, chats, herramientas de trabajo colaborativo, etc…

En el caso de una herramienta de trabajo colaborativo, cuando un equipo de trabajo está delante de la pantalla (cada uno en su ordenador) y un miembro finaliza una tarea y actualiza su estado a “finalizada”, la aplicación notifica inmediatamente al resto de usuarios (o a uno, o a varios) de que esa tarea está cerrada e inmediatamente ven un cambio en el estado de esa tarea.

En un juego multijugador podríamos visualizar el movimiento de otro jugador. En un chat veríamos como nos llega un mensaje de otro usuario en el momento en que nos lo envía. En una herramienta de monitorización, la temperatura que marca el sensor de algún componente de un sistema, etc, etc, etc… La idea es que tenemos la información desde el mismo instante en que se genera.

En este tutorial intentaremos explicar qué es la especificación WebSocket, en qué casos puede ser interesante su uso y cómo implementarla desde el lado del cliente con nuestro navegador y desde el servidor con Apache Tomcat 7 mediante varios ejemplos.


2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.2 Ghz Intel Core I7, 8GB DDR3).
  • Sistema Operativo: Mac OS Snow Leopard 10.6.7
  • Entorno de desarrollo: Intellij Idea 11.1 Ultimate.
  • Apache Tomcat 7.0.29
  • Maven 3.0.3
  • jQuery 1.7.2
  • jQuery-UI 1.8.22
  • Google Chrome 20
  • Mozilla Firefox 14

3. ¿Qué son los WebSockets?.


3.1. La especificación.

La especificación WebSocket (llevada a cabo por la IETF) provee un canal de comunicación bi-direccional entre el navegador (cliente) y el servidor, enviando y recibiendo mensajes de manera simultánea. El cliente puede enviar datos al servidor por este canal pero, lo más interesante, es que el servidor puede enviar datos al cliente sin necesidad de que éste realice una petición para solicitar datos. Se establece una única conexión entre cliente y servidor que permite que cualquiera de los dos actores pueda enviar mensajes al otro en cualquier momento.

¿Y esto para qué vale?. Pues como hemos comentado en la introducción, sobre todo para aplicaciones que requieren constantes actualizaciones en el frontal debido a la interacción de terceras personas (o sistemas). Son las denominadas “aplicaciones en tiempo real”.

Antes de la aparción de WebSockets se utilizaban dos técnicas para implementar este comportamiento:

  • AJAX polling: Consiste en realizar constantemente peticiones al servidor preguntándole si se ha producido algún evento que requiera una actualización en la vista. Por ejemplo: en un chat, desde la pantalla donde llegan los posibles mensajes que recibe el usuario por parte de otros usuarios, se estarían enviando peticiones HTTP al servidor (cada 2 segundos, 5, 10, o lo que sea) preguntándole ¿tengo mensajes?, ¿tengo mensajes?, ¿tengo mensajes?, etc… Esta es una solución bastante ineficiente debido a que se genera una gran cantidad de tráfico con el servidor sobre todo en aplicaciones con un elevado número de usuarios e intervalo entre peticiones pequeño. Además, pensemos que la mayoría de las veces el servidor responderá lo mismo: “no, no hay cambios”.
  • Comet o “long polling”: Es una técnica muy parecida a la que utilizan los WebSockets. Consiste en realizar una única petición al servidor de forma que éste responde diciendo que va a devolver la respuesta en “trozos” (streaming). La petición queda abierta hasta que el servidor responda con todas las porciones de respuesta que solicita el cliente, que no son otra cosa que eventos que se producen en el servidor notificando un cambio de estado en el cliente. Como digo, es parecido a lo que hacen los WebSockets, sin embargo su implementación es bastante más compleja.

3.2. API Javascript.

La W3C ya está intentando normalizar el API WebSockets. Ahora solo falta que los navegadores la implementen, pero de eso ya hablaremos más adelante…

El constructor: Recibe un parámetro obligatorio y otro opcional. El primero es la URL del servidor con el que estableceremos la conexión. El segundo (opcional) es una lista de subprotocolos de conexión. El WebSocket se construiría así.

El manejador de eventos onopen: manejador que es invocado cuando se abre una conexión con el servidor.

El manejador de eventos onclose: manejador que es invocado cuando se cierra conexión con el servidor.

El manejador de eventos onmessage: manejador que es invocado cuando llega un mensaje desde el servidor.

El manejador de eventos onerror: manejador que es invocado cuando se produce un error.

El método send: envía un mensaje de texto o datos binarios al servidor.

El método close: cierra la conexión con el servidor.


4. WebSockets con Tomcat 7.

Como hemos dicho anteriormente, WebSockets es una especificación que necesita ser implementada por cliente y servidor. Acabamos de ver cómo se define esta especificación en Javascript (lado del cliente), ahora veamos cómo se usa en nuestro servidor. En nuestro caso hemos elegido la versión 7.0.29 de Apache Tomcat.

Antes de nada quiero hacer notar que esta versión de Tomcat solo da soporte a los navegadores Firefox y Chrome. Safari de momento no funciona vía WebSockets con Tomcat 7.0.29, aunque se espera que lo haga en próximas versiones (no de Safari, que ya soporta WebSockets, sino de Tomcat). Internet Explorer, hasta su versión 9, todavía no implementa el API WebSockets aunque lo hará a partir de la versión 10.

Para utilizar WebSockets con Tomcat 7.0.29 haremos uso de las siguientes dependencias.

Una vez tenemos nuestras dependencias lo único que tenemos que hacer es crear un Servlet del tipo org.apache.catalina.websocket.WebSocketServlet, que extiende directamente de HttpServlet. Al extender de esta clase (abstracta) estaremos obligados a implementar el método createWebSocketInbound que devolverá la conexión que se habrá establecido con el servidor.

Como podemos ver, este método recibe un subprotocolo (recordemos que, en el punto anterior, vimos que podemos crear una conexión WebSockets indicando una lista de subprotocolos) y una petición http. Y debemos devolver un StreamInbound (ahora hablaremos de él).

Además del método createWebSocketInbound, que estamos obligados a implementar, podemos sobreescribir otros dos:

  • verifyOrigin: Con el que decidimos si aceptamos o no establecer conexión con el cliente que nos la solicita.
  • selectSubProtocol: Con el que decidimos qué subprotocolo utilizaremos.

Como acabamos de decir, el método createWebSocketInbound nos obliga a devolver un StreamInbound. Este StreamInbound, más que unos datos entrantes como indica su nombre, representa una conexión. Quizá no sea el nombre más acertado para expresar lo que representa, puede que haya faltado un poco de Clean Code pero ¿a quién no le ha pasado? :-S. StreamInbound es, nuevamente, una clase abstracta que debemos extender implementando los siguientes métodos.

Al implementar los métodos onBinaryData y onTextData estaremos implementando el comportamiento para saber qué hacer cuando el cliente (navegador) con el que se ha establecido la conexión nos envíe mensajes binarios o de texto.

Además de estos dos métodos, hay otros que no es necesario implementar, pero que podemos sobreescribir. Me parecen especialmente interesantes: onOpen y onClose. Estos métodos serán invocados cuando la conexión se crea y se cierra, respectivamente.

Tras de este rollo teórico, vamos a pasar a ver un sencillo ejemplo que nos ayudará a asimilar mucho mejor los conceptos. Posteriormente veremos un ejemplo más complejo.


5. Ejemplo sencillo.

En este ejemplo vamos a ver cómo hacer una especie de “Hola mundo”, donde el cliente (navegador) envía un mensaje al servidor indicando el nombre del usuario y el servidor le responde con un saludo.

Paso 1: vamos a crear nuestro WebSocketServlet:

Como vemos en la línea 16, con la anotación @WebServlet registramos nuestro WebSocketServlet haciendo que responda a las peticiones que lleguen al contexto /simple.

En la línea 32, implementamos nuestro StreamInbound (mediante nuestra nueva clase WebSocketConnection) que es la conexión que debemos devolver. Para ello hacemos uso de la clase org.apache.catalina.websocket.MessageInbound que es una extensión de StreamInbound que nos ayuda con la implementación de los métodos onBinaryMessage y onTextMessage.

Por último, en la línea 28, vemos cómo devolvemos nuestra conexión, que es la clase WebSocketConnection que hemos creado.

El que prefiera la configuración del Servlet por .xml en vez de por anotaciones (como es mi caso), puede hacerlo eliminando la anotación @WebServlet de la clase que acabamos de ver y añadiendo lo siguiente al web.xml.

Paso 2: Vamos a preparar nuestro cliente, el navegador, para que establezca una conexión WebSocket con el servidor. Así que toca un poco de Javascript…

Creamos 3 funciones: connect, disconnect y sendMessage: a las que invocaremos cuando queramos conectarnos al servidor, desconectarlos y enviar un mensaje, respectivamente… En la función connect, implementamos el comportamiento de los manejadores de eventos que explicamos en el punto 3.2.

Pues únicamente con esto el resultado sería algo así:

Yo creo que más fácil imposible :). Para el que esté interesado en “trastear” un poco más, dejo todo el código fuente de este ejemplo y del que viene en https://github.com/marlandy/websockets-tomcat.

Hemos visto este ejemplo para intentar asentar un poco los conceptos que hemos explicado en los puntos anteriores pero, como diría mi compañero y jefe Alejandro Pérez, esto en una aplicación real “no sirve ni para envolver pescado”, así que vamos a ver un ejemplo más complejo.


6. Ejemplo complejo: un chat.

Como dijimos anteriormente, el uso de los WebSockets está especialmente indicado para aplicaciones “en tiempo real”. Un muy buen ejemplo de este tipo de aplicaciones es un chat. Pues vamos con ello.

Vamos a establecer los requisitos que deberá cumplir nuestra aplicación.

  • La aplicación pedirá al usuario un nombre para identificarle.
  • Un usuario identificado podrá mantener una conversación con uno o varios usuarios conectados.
  • Cuando un usuario se registre en la aplicación (introducir el nombre de usuario) deberá poder ver los nombres de todos los usuarios que están conectados.
  • Al pulsar sobre el nombre de un usuario se abrirá una zona de diálogo con la que podrá mantener una conversación con el usuario seleccionado.
  • Cuando a un usuario conectado le llegue un mensaje de otro usuario, se abrirá una zona de diálogo donde podrá mantener una conversación.
  • El listado de usuarios conectados se irá actualizando en tiempo real según vayan accediendo o saliendo de la aplicación.
  • El usuario se podrá desconectar cuando lo desee.

Para llevar a cabo todo esto, necesitamos indentificar muy claramente los tipos de mensajes que va a manejar nuestra aplicación. Son tres, y los representaremos con JSON:

connectionInfo: es un mensaje que devuelve el servidor al cliente cuando éste se acaba de conectar. Contiene el nombre del usuario y la lista de los demás usuarios conectados.

statusInfo: es un mensaje que contiene información sobre un evento de conexión o desconexión de un usuario a la aplicación. Se envía de servidor a cliente. Servirá para mantener actualizada la lista de usuarios conectados.

messageInfo: contiene información relativa a un mensaje que envía un usuario a otro (conversación). Se envía de cliente a servidor (cuando un usuario va a mandar un mensaje a otro) y de servidor a cliente (cuando recibe un mensaje de un usuario y lo redirige al destinatario).

Una vez identificados los mensajes que manejará nuestra aplicación, vamos a implementar el comportamiento de nuestro servidor.

Como vemos, todo gira en torno a la conexión que hemos creado: ChatConnection y a una colección donde almacenamos las conexiones de los usuarios a la aplicación (connections).

Si nos fijamos en la línea 56, método onOpen de nuestra clase ChatConnection, vemos que, cuando se crea una nueva conexión, enviamos un mensaje al usuario con datos relativos a su conexión (mensaje de tipo connectionInfo). Con esto, nuestro usuario podrá saber el número de usuarios que actualmente se encuentran conectados. Luego notificamos al resto de usuarios que hay un nuevo usuario en el chat (mensaje de tipo statusInfo), por lo que podrán actualizar su lista de usuarios on-line. Finalmente el servidor guarda la nueva conexión.

En la línea 63, método onClose vemos que, cuando un usuario sale del chat (cierra la conexión), informamos a todos los usuarios de que hay un usuario menos en el chat (mensaje de tipo statusInfo). Así pueden eliminar a ese usuario de la lista de usuarios on-line. Después el servidor elimina la referencia a esa conexión.

Por último, en la línea 74, método onTextMessage, el servidor envía el mensaje entrante de un usuario a su destinatario (tanto el mensaje entrante como el mensaje es de tipo messageInfo), que debe estar en la lista de usuarios activos en el chat.

Y ahora vamos a ver qué es lo que tenemos que hacer en el cliente:

El envío de mensajes y el cierre de conexión, o lo que es lo mismo, los métodos send y close del objeto WebSocket no los he incluido ya que son exáctamente iguales que los del punto anterior, aunque eso si, en formato JSON.

En el comportamiento que definimos en los manejadores de eventos vemos que:

  • Cuando se abre una conexión únicamente actualizamos la interface gráfica para habilitar botones como el de desconexión y actualizar nuestro estado.
  • Cuando se cierra una conexión, pues básicamente lo mismo, actualizamos la vista volviendo al estado original.
  • Cuando recibimos un mensaje realizaremos la acción correspondiente en función de su contenido. Si es un connectionInfo actualizamos la lista de usuarios conectados, si es un statusInfo eliminamos o añadimos al nuevo usuario desconectado o conectado y si es un messageInfo añadimos el mensaje al cuadro de diálogo correspondiente.

He omitido gran cantidad de código del lado del cliente porque creo que se sale del objetivo de este tutorial, sin embargo todo el que quiera ver el ejemplo en funcionamiento y “trastear” con él, puede hacerlo descargándoselo de aquí https://github.com/marlandy/websockets-tomcat.

Con el código que hemos visto y un poquito de jQuery y CSS, este es el resultado…

Inicio de la aplicación. Debemos identificarnos con un nombre de usuario.

Conectado. Observamos un listado con el resto de usuarios conectados al chat.

Al pulsar sobre un usuario conectado iniciamos una conversación.

Intercambio de mensajes entre dos usuarios.

Se pueden mantener conversaciones con varios usuarios simultáneamente.

El código fuente al completo de este ejemplo está disponible aquí https://github.com/marlandy/websockets-tomcat.


7. Referencias.


8. Conclusiones.

En este tutorial hemos comentado la especificación WebSocket, hemos explicado su API Javascript y hemos visto cómo implementarlo del lado del servidor gracias a Apache Tomcat 7.0.29.

Personalmente, los WebSockets me parecen una de las características más interesantes de todas aquellas que se quieren introducir con HTML5. Creo que tienen un potencial muy elevado y que, aun siendo una especificación relativamente nueva, los desarrolladores no tardarán mucho en incorporarla a sus aplicaciones. Recordemos que está especificación está dirigida principalmente a aplicaciones “en tiempo real”.

Espero que este tutorial os haya sido de ayuda. Un saludo.

Miguel Arlandy

marlandy@autentia.com

Twitter: @m_arlandy