En este artículo aprenderemos a implementar WebSocket con Java, ya que en el pasado ya había dado una completa Introducción a los WebSocket y explicamos que estos fueron introducidos como una mejora en HTML5, pero también dijimos que los WebSocket son ejecutados por el navegador, y estos requieren de dos partes, un WebSocket cliente (Navegador) y un WebSocket Server (Backend). Pues bien, ya habíamos platicado como es que los WebSocket funcionan del lado del navegador, es por eso que ahora hablaremos de la otra cara, los WebSocket Server.
Como el título de este articulo lo dice, hablaremos de cómo implementar los WebSocket utilizado Java como BackEnd, pero cabe mencionar que todos los lenguajes de programación deberían de tener sus propias API’s para soportar conexiones del lado del servidor. Los WebSocket fueron agregados a apartar de la versión Java EE 7 bajo la especificación JSR 356, es por ello que todos los Application Server certificados para Java EE 7 deberán de tener una implementación estándar de dicha especificación. Basta de charla y pasemos a cómo implementar un WebSocket con Java.
Implementando un WebSocket con Java
Para explicar cómo funcionan los WebSocket vamos a implementar un ejemplo muy simple, crearemos una barra de progreso la cual se cargará del valor 0 al 100 simulando que un proceso se está ejecutando en el BackEnd. En el formulario tendremos un botón que iniciara el proceso, cuando el usuario presione el botón, enviaremos un mensaje al BackEnd por medio del WebSocket, en ese momento, el BackEnd iniciara con un proceso de notificaciones el cual enviará por medio del WebSocket un mensaje con las actualizaciones de la barra de progreso. Este ejemplo está desarrollado con Java 8 y Wildfly 9.0.
Este ejemplo está compuesto de 4 archivos:
- html: página principal del proyecto, sobre la que mostraremos la barra de progreso.
- js: Archivo de JavaScript en donde está programado el WebSocket y el procesamiento de los mensajes de envío y recepción.
- css: archivo de clases de estilo (opcional).
- java: Clase donde implementamos el WebSocket Server con Java, esta se ejecuta del lado del servidor.
Todo el código del proyecto lo puedes descargar de GitHub: https://github.com/oscarjb1/ProgressWebSocket.git
Index.html
Primero que nada, veremos el documento index.html para entender como esta armada la pantalla:
<!DOCTYPE html> <html> <head> <title>WebSocket Progress</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <script type="text/javascript"src="websocket.js"/></script> <link rel="stylesheet" type="text/css" href="styles.css"> </head> <body> <section class="container"> <h1>WebSocket Progress</h1> <div> <div class="progress-container"> <form id="form"> <input id="btnSubmit" type="button" value="start" onclick="formSubmit();"/> <progress id="progress" value="0" max="100"></progress> <label for="progress" id="lblProgress"></label> </form> </div> </div> </section> </body> </html>
Lo primero que hacemos en el Header es importar al archivo styles.css del cual no hay mucho que decir, salvo que es utilizado para darle una apariencia más agradable a la página, y el archivo websocket.js del cual hablaremos más adelante.
El siguiente punto importante es el formulario, en el definimos un botón que al presionarlo ejecutara la función formSubmit() que está definido en el archivo websocket.js, que iniciara con el proceso de carga del progress bar.
También tenemos un progress, el cual será actualizado a medida que el WebSocket reciba las notificaciones del BackEnd, para lo cual utilizaremos el ID (progress) para identificarlo más adelante. Finalmente, debajo del progress tenemos un label el cual será actualizado con el progreso del progress bar.
Tanto el progress como el label son actualizados por Javascript a medida que el servidor manda los datos por medio del WebSocket.
Websocket.js
Por su nombre, podríamos pensar que se trata de una librería o algún script ya desarrollado, pero la realidad es que es un Script totalmente custom y desarrollado a la medida para esta solución.
var socket = new WebSocket("ws://localhost:8080/ProgressWebSocket-1.0-SNAPSHOT/progress"); socket.onmessage = onMessage; function onMessage(event) { var btnSubmit = document.getElementById("btnSubmit"); btnSubmit.disabled = true; var progress = document.getElementById("progress"); var data = JSON.parse(event.data); progress.value = data.value; var lblProgress = document.getElementById("lblProgress"); if(data.value < 100){ lblProgress.innerHTML = 'Progress: ' + data.value + '%'; }else{ btnSubmit.disabled = false; lblProgress.innerHTML = "Finish"; } } function formSubmit() { socket.send("{\"start\":\"true\"}"); }
La primera línea es la más importante, pues en esta se establece la conexión con el servidor, la clase WebSocket es una clase estándar que nos permitirá comunicarnos con el Servidor de una forma simple, esta clase tiene como parámetro la URL sobre la cual escucha el BackEnd, más adelante veremos cómo está formada esta URL, por lo pronto asumamos que existe un WebSocket Server que está escuchando.
En la línea 2 definimos la función que procesara los mensajes entrantes, es decir, cuando el BackEnd envíe un mensaje al navegador, este lo atenderá por medio del método definido, en este caso, establecemos el método onMessage para procesar los mensajes entrantes.
En la línea 4 definimos el método onMessage el cual implementa toda la lógica de procesamiento de los mensajes entrantes, el parámetro event corresponde al evento recibido y mediante event.data es posible recuperar el mensaje que envió el Servidor. Veamos qué es lo que hace. Primero que nada, en las líneas 5-6 obtenemos la referencia el botón y lo deshabilitamos para impedir que se presione mientras esta en procesamiento. En la línea 8 convertimos el mensaje enviado por el Server en un Json para poderlo procesar. En las líneas 10-11 obtenemos una referencia al progress y actualizamos el valor de la barra de progreso, para lo cual establecemos la propiedad value por el valor enviado por el Servidor. En las líneas 13-19 establecemos el valor del label, indicando el progreso recibido por el servidor, en caso de finalizar (100%), la etiqueta cambia a Finish y el botón se habilitado nuevamente.
Finalmente, en las últimas líneas, tenemos el método formSubmit, el cual es ejecutado por el botón cuando es presionado, este método manda un mensaje dummy al servidor mediante el método send del WebSocket.
ServerDashboardWebSocket.java
Esta es la parte principal para implementar un WebSocket con Java, ya que desde aquí es de donde se aceptan las conexiones del navegador y se procesan los mensajes de envío y recepción.
package com.osb.progresswebsocket.socket; import java.util.HashSet; import java.util.Set; import javax.enterprise.context.ApplicationScoped; import javax.websocket.OnClose; import javax.websocket.OnError; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; /** * @author Oscar Blancarte <oscarblancarte3@gmail.com> */ @ApplicationScoped @ServerEndpoint("/progress") public class ServerDashboardWebSocket { private Set<Session> sessions = new HashSet<>(); @OnOpen public void open(Session session) { System.out.println("Session opened ==>"); sessions.add(session); } @OnMessage public void handleMessage(String message, Session session) { System.out.println("new message ==> " + message); try { for (int c = 0; c < 100; c++) { for (Session s : sessions) { s.getBasicRemote().sendText("{\"value\" : \"" + (c + 1) + "\"}"); } Thread.sleep(100); } } catch (Exception e) { System.out.println(e.getMessage()); e.printStackTrace(); } } @OnClose public void close(Session session) { System.out.println("Session closed ==>"); sessions.remove(session); } @OnError public void onError(Throwable e) { System.out.println(e.getMessage()); e.printStackTrace(); } }
Primero que nada, observemos que esta es una clase común y corriente, no extiende ni implementa ninguna clase, sin embargo, esta anotada con @ApplicationScope y @ServerEndpoint. Si ya has trabajado con JavaEE es muy probable que conozcas la primera, la cual no pertenece al API de WebSocket, pero es utilizada para crear un Bean disponible durante toda la vida de la aplicación. La segunda, @ServerEndpoint es en la que debemos de poner atención, con tan solo marcar una clase con ella, le estamos diciendo al Application Server que esta se trata de un WebSocket Server el cual estará escuchando en la URL http://<HOST>:<PORT>/<APPLICATION>/progress, la última parte de la url (/progress) es la que definimos en la anotación @ServerEndpoint por lo que tenemos que tener cuidado de que coincida con la URL que definimos en el archivo websocket.js.
También podemos ver que tenemos una Set de Session. Las Session son objetos que representan las conexiones de los navegadores, por lo que tendremos un Set para no perder la referencia a todas las conexiones.
A continuación, veremos una serie de métodos anotados, estas anotaciones le dicen a Application Server que método debe de ejecutar ante cualquier evento. Veamos los diferentes métodos.
Método open, está marcado con la anotación @OnOpen y se ejecutara cada vez que una nueva conexión se establezca con el servidor, el parámetro Session representa la conexión y la almacenamos en el Set de sesiones.
Método close, esta anotado con @OnClose y se ejecutara cada vez que una sesión se desconecte. Recibe el objeto Session que se desconectó para poder identificarla.
Método onError, esta anotado con @OnError, y se ejecutara ante cualquier error, el error es enviado como parámetro.
Método handleMessage, anotado con @OnMessage, será ejecutado cada vez que llegue un nuevo mensaje de alguno de los navegadores. Este recibe como parámetro el mensaje enviado y el objeto Session que mando el mensaje. Este es el método más importante en nuestro ejemplo, porque una vez recibido un mensaje, iniciara una serie de envíos de mensajes al navegador. Observemos que haremos un ciclo del 1 al 100 y en cada interación enviaremos al Navegador un mensaje con el progreso, al finalizar cada interación, dormiremos el hilo 100 milisegundos para apreciar mejor como se llena la barra. Utilizamos el método getBasicRemote() para que nos regrese una implementación de envío asíncrono y seguido ejecutamos el método sendText, el cual se utiliza para mandar un mensaje de texto al navegador.
Resumen:
Repasemos como funciona todo, cuando entramos a la página, esta carga el archivo websocket.js y establece conexión inmediata con el Servidor, en este momento el método open de la clase ServerDashboardWebSocket.java es ejecutado para recibir la nueva sesión. Seguido, el usuario presiona el botón start y envía mediante el función websocket.send un mensaje al Servidor, el Servidor recibe el mensaje mediante el método handleMessage de la clase ServerDashboardWebSocket.java, el cual envía una serie de mensaje como respuesta al navegador, por su parte, el navegador procesa los mensajes entrantes mediante la función onMessage de JavaScript, en esta función se actualiza la barra de progreso.
Excelente post Oscar!!!!! A ponerlo en práctica!!
Gracias Gerardo, te recomiendo le des una revisada al post de Introducción a los WebSocket, donde tambien hablo del tema.
saludos 🙂
Hola ya cheque los 2 articulos , pero en ninguno usan headshake
Hola oscar quisiera saber si al actualizar la web mediante f5 o cambio hacia otra pag html se me asigna otro numero de session.
Hola Diego, esa es una fantástica pregunta!, te cuento como funciona. Recuerda que el WebSocket se crea desde JavaScript, por tal motivo, cada vez que el navegador se refresca carga de nuevo la página y destruye todo lo que existía previamente. Entonces la respuesta es SI, cada vez que actualizas la pestaña o cambias de página el WebSocket se desconecta y cuando la página vuelve a cargar crear otra Sesión.
Si lo que quieres es mantener existente el mismo WebSocket durante toda la estancia del usuario deberás usar Ajax para cargar dinámicamente el contenido sin tener que cambiar de página, de esta forma la página actual prevalece y solo se carga el contenido nuevo con una petición Ajax. Otra estrategia que puedes usar, que si bien no mantiene la sesión abierta, es que cada vez que te reconectes ligues la sesión con algún dato del usuario, así cada vez que se reconecte, sabrás que es la misma persona aunque la sesión sea distinta.
Espero que sea la respuesta que esperabas.
Saludos.
Buen articulo.
Tengo una duda, usando websockets en un cluster de tomcat, ¿podemos enviar un mensaje a todas las sesiones? del cluster ( no solo al nodo que origine el evento)
Hola, de forma natural no es posible, pues cada nodo tiene un registro independiente de todas las sesiones, sin embargo es posible hacerlo con ayuda de algunas herramientas. Mira la siguiente liga, y verás que es exactamente lo que necesitas: https://stackoverflow.com/questions/26853745/spring-websocket-in-a-tomcat-cluster
saludos.
Hola Oscar , como puedo hacer para que no se desconecte del websocket , hacer que se quedara el canal abierto por cada conexion que se realice
Hola Jorge, esa parte se configura desde el backend, tendrás que guardar la referencia a la sesión en un nivel de aplicación, ya que si guarda la referencia en una variable local, se desconectará apenas termine la ejecución del método. Por otra parte, puedes utilizar los eventos de los websocket para callar una desconexión del lado del navegador, para que en tal caso, vuelva a conectarse inmediatamente.
saludos.
muchas gracias Oscar ,excelente aporte me sirvió demasiado.
Saludos
Hola Oscar, como podria manejar diferentes eventos OnMessage? Lo que ocurre es que trato de hacer una pizarra compartida, entonces necesito en OnMessage para dibujar en el canvas lo que otros hagan, pero tambien quiero estar “pendiente” de si un usuario abandona la sala, o ingresa otro usuario, y eso lo voy a gestionar de otra manera, la cuestion es que no se como manejar por separado esos 2 eventos. Excelente post!
Hola Jean disculpa la demora, lo que podrías hacer, es que todos los mensajes tenga una propiedad “type”, la cual te permite distinguir que tipo de mensaje es, así, cuando te llegue un tipo determinado, lo procesas de una manera, y cuando te llega otro “type”, lo procesas de otra forma.
Hola, perdon pero como corro el proyecto de maven de git? ya le di mvn install pero después que debo hacer?
Hola Alfredo, tienes que descargar primero el código desde GitHub, una vez que lo tengas, lo puedes montar en tu IDE de preferencia, como NetBeans o Eclipse, una vez en el IDE, solo le dar ejecutar al proyecto y listo.
Muy bien explicado, da gusto encontrarse con gente como tu por Internet muchas gracias.
Gracias por el comentario 🙂
No entiendo como pones el servidor a la escucha, los servicios de java que he hecho los iniciaba en un Runnable pero este como lo inicio?
Es una web application normal, la despliego sobre un Wildfly,
Buen dia Oscar, primero comentarte que es muy útil el texto que escribiste de WebSocket, solo hay algo que no entiendo. En la red encontré: “As opposed to servlets, WebSocket endpoints are instantiated multiple times. The container creates an instance of an endpoint per connection to its deployment URI. Each instance is associated with one and only one connection. This facilitates keeping user state for each connection and makes development easier, because there is only one thread executing the code of an endpoint instance at any given time”. Acá entiendo que se crea una instancia por cada conexión, no? Entonces, como es que se mantiene el set de sesiones en la siguiente linea si esta no es estática?
private Set sessions = new HashSet();
Suponiendo que tengo n instancias cada una tendrá su propio Set, si una se conecta y se la session se agrega al Set, que pasa con las conexiones que existen previamente.
Saludos bro, de verdad es de gran ayuda tu blog.
Es correcto, un cliente puede tener múltiples conexiones abiertas al mismo tiempo, es decir, puede tener varias pestañas abiertas, lo que provoca que exista más de una conexión por usuario, lo que puedes hacer es tener un lista de conexiones por usuario.
It does not work,
Throws:
WebSocket connection to ‘ws://localhost:8081/ProgressWebSocket/progress’ failed: Error during WebSocket handshake: Unexpected response code: 404
Hola, es posible crear un P2P usando websockets? ¿Podrías ayudarme por favor?