Este nuevo post presenta un sencillo ejemplo del desarrollo de una aplicación web (lanzada e inicializada mediante Spring Boot) que hace uso de web sockets para establecer una comunicación bidireccional asíncrona entre el cliente (el navegador web) y el servidor (la aplicación ejecutada en el Tomcat embebido de Spring Boot).
Para ello usaremos:
- Spring Boot, para inicializar la aplicación, resolver las dependencias, ejecutarla en el Tomcat embebido, etc.
- Spring Web Sockets, para implementar la parte correspondiente a ellos.
- STOMP y SockJS para crear el cliente en JavaScript que se ejecutará desde una página web en el navegador y facilitar las comunicaciones via JSON.
El objetivo es desarrollar una aplicación web que:
- Conecte al servidor, estableciendo el canal de comunicación para recibir los mensajes desde el servidor.
- Envíe un mensaje al servidor y reciba una respuesta que mostrará por pantalla.
- Permanezca a la escucha de los mensajes que le lleguen desde el servidor y los imprima por pantalla.
- Finalice la conexión al servidor, cerrando el canal de comunicación establecido por los web sockets.
Los pasos a seguir serían los siguientes:
1. Inicialización de la aplicación con Spring Boot
Para conocer algo más sobre Spring Boot se puede consultar este otro post sobre el tema dentro del blog, o la documentación oficial. Basicamente lo que se pretende es usar Spring Boot para preparar una aplicación con todas las dependencias y configuraciones iniciales necesarias para desarrollar una aplicación web que soporte Spring Web Sockets. Usaremo Spring MVC y Spring Web Sockets para esto. Para ello creamos un proyecto Java en Eclipse, lo mavenizamos y añadimos las siguientes dependencias al pom.xml (podemos partir de la aplicación desarrollada en el post antes mencionado sobre Spring Boot):
1 2 3 4 5 6 7 8 9 10 11 |
<!-- Add typical dependencies for a web application --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Dependencies for a web sockets --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> |
2. Creamos los DTOs que usaremos para transferir la información entre el cliente y el servidor y viceversa
La aplicación que vamos a desarrollar es una sencilla web que facilita información meteorológica en tiempo real. Contaremos con un sencillo formulario web que permite introducir el nombre de una ciudad. Se envía el nombre de la ciudad al servidor y se recibe la información meteorológica sobre esa ciudad en ese momento. Además cualquier cliente que este escuchando en ese momento en el mismo canal, recibirá la misma información. También se puede recibir la información meteorológica en tiempo real si se escucha en el web socket y esta se envía de forma periódica desde el servidor.
Por lo tanto, tenemos dos tipos de mensajes. El que envía el cliente al servidor con el nombre de la ciudad, y el que envía el servidor al cliente con la información meteorológica. Esta información se enviará en JSON pero dentro del servidor se implementará como un par de DTOs o POJOs o Java beans, es decir, como un par de clases sencillas con sus atributos y metodos de acceso (get/set).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
/** * @author lagarcia * */ public final class PlaceInfo { /** The place name. */ private String place; /** * @param placeArg */ public PlaceInfo() { /* Empty */ } /** * @param placeArg */ public PlaceInfo(String placeArg) { place = placeArg; } /** * @return the place */ public String getPlace() { return place; } /** * @param placeArg * the place to set */ public void setPlace(String placeArg) { place = placeArg; } } /** * @author lagarcia * */ public final class WeatherInfo { /** The weather type. */ private WeatherType type; /** The local date and time. */ private LocalDateTime time; /** The local date time as string. */ private String localDateTimeStr; /** The place. */ private String place; /** * Defaul constructor. */ public WeatherInfo() { /* Empty */ } /** * Constructor with args. * * @param typeArg * @param timeArg * @param placeArg */ public WeatherInfo(WeatherType typeArg, LocalDateTime timeArg, String placeArg) { super(); type = typeArg; time = timeArg; place = placeArg; } /** * @return the type */ public WeatherType getType() { return type; } /** * @param typeArg * the type to set */ public void setType(WeatherType typeArg) { type = typeArg; } /** * @return the time */ public LocalDateTime getTime() { return time; } /** * @param timeArg * the time to set */ public void setTime(LocalDateTime timeArg) { time = timeArg; } /** * @return the place */ public String getPlace() { return place; } /** * @param placeArg * the place to set */ public void setPlace(String placeArg) { place = placeArg; } /** * @return the localDateTimeStr */ public String getLocalDateTimeStr() { return time.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); } /** * @param localDateTimeStrArg * the localDateTimeStr to set */ public void setLocalDateTimeStr(String localDateTimeStrArg) { localDateTimeStr = localDateTimeStrArg; } } |
Como no tenemos capa de persistencia ni acceso a base de datos o similar, crearemos un mapa con datos en memoria (WeatherData.java) donde insertaremos unos cuantos DTOs con información meteorológica de tres lozalizaciones, con datos como el tipo de tiempo, la hora en formato ISO o el lugar. 3. Configuración de los Web Sockets La configuración de los web sockets se hace en este caso como casi todo en Spring Boot, mediante una clase java y usando anotaciones.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/weather").withSockJS(); } } |
En este caso, indicamos con @Configuration que es una clase de configuración de Spring (lo que eran antes los antiguos XML). Ademas con @EnableWebSocketMessageBroker indicamos que vamos a configurar los web sockets. En el primer método establecemos el mecanismo (broker) que escribira en el canal de mensajes del servidor a los clientes y cuya URI (del canal) empezará por “/topic”. En este caso es un broker sencillo que solo mantiene sus mensajes en memoria (sin persistencia). También indicamos que los mensajes de los clientes al servidor irán dirigidos a una URL que empezará por “/app”. En el segundo método se establece “/weather” como la URI del web socket creado y se indica que se espera SockJS para establecer la comunicación desde el cliente.
4. El controlador (y los posibles servicios)
Esta parte comprende la implementación del web socket en sí. En este caso vamos a usar un @Controller de Spring MVC para desarrollar un servicio que recibe el mensaje desde el cliente y responde desde el servidor usando el web socket antes configurado.
También se puede desarrollar un servicio, bien a nivel de controlador web, o bien a nivel de servicio, que escriba directamente en el web socket desde el servidor. El cliente que este escuchando en el canal recibirá el mensaje igualmente.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
@Controller @EnableScheduling public final class WeatherController { /** A logger reference */ private static Logger logger = Logger.getLogger(WeatherController.class); /** The data storage. */ @Autowired private WeatherData wData; /** Web socket message template */ @Autowired private SimpMessagingTemplate template; /** * Get weather info for a given place. * * @param place * - The place * @return The weather info */ @MessageMapping("/weather") @SendTo("/topic/weatherinfo") public WeatherInfo getWeatherInfo(PlaceInfo place) { /* Default */ WeatherInfo wInfo = new WeatherInfo(); try { logger.info("Get weather from " + place.getPlace()); /* Lets make a pause */ Thread.sleep(1000); /* Return weather info for the given place */ wInfo = wData.getWeatherFrom(place.getPlace()); logger.info("Retrieving weather from " + place.getPlace()); } catch (Exception ex) { ex.printStackTrace(); } return wInfo; } /** * Run weather info periodically. */ // @Scheduled(fixedRate = 2000) public void runWInfo() { this.template.convertAndSend("/topic/weatherinfo", wData.getWeatherFrom("Madrid")); } /** * @return the wData */ public WeatherData getwData() { return wData; } /** * @param wDataArg * the wData to set */ public void setwData(WeatherData wDataArg) { wData = wDataArg; } } |
Las anotaciones que aplican a los Web Sockets son @MessageMapping(“/weather”) y @SendTo(“/topic/weatherinfo”). La primera indica que la URL a la que hay que invocar para enviar el mensaje al web socket es “/weather”. Entonces el JSON enviado se parseará automaticamente al DTO que es el argumento del método, como en un servicio web o un método de Spring MVC. La segunda anotación indica que la respuesta del método se enviará automaticamente a “/topic/weatherinfo”. Esa sería la URL del canal de respuestas desde el servidor en el que tendría que escuchar el cliente para recibir las respuestas.
Tenemos por lo tanto un método (o servicio) que recibe mensajes de los cliente y los contesta usando el web socket (getWeatherInfo). Pero además tenemos un método (runWInfo) que escribe directamente en el web socket sin más (si se descomenta la anotacion de @Scheduled se ejecutará cada 2 segundos). Para ello hace uso del SimpMessagingTemplate, indica la URL del web socket a escribir y el mensaje a enviar (el DTO).
Resumiendo:
- Hemos configurado un web socket “llamado” “/weather” (ver punto 3).
- Los clientes escriben al web socket si envían un mensaje a “/app/weather” (ver punto 3).
- El servidor escribe al web socket si envía un mensaje a “/topic/weatherinfo” (ver punto 3 y 4).
- Se puede usar el web socket para recibir del cliente y enviar desde el servidor (como en un servicio web o peticion HTTP estandar) o solo para enviar desde el servidor.
5. El cliente de JavaScript
El cliente es una página HTML muy sencilla que hace uso de las siguientes funciones de JavaScript:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
/* JavaScript for websocket client */ var stompClient = null; /* Show connection status */ function setConnected(connected) { var status = "disconnected"; if(connected){ status = "connected"; } document.getElementById('status').innerHTML = status; } /* Connect to topic using web sockets */ function connect() { var socket = new SockJS('/weather'); stompClient = Stomp.over(socket); stompClient.connect({}, function(frame) { setConnected(true); console.log('Connected: ' + frame); stompClient.subscribe('/topic/weatherinfo', function(weather){ showWeather(JSON.parse(weather.body)); }); }); } /* Disconnect */ function disconnect() { if (stompClient != null) { stompClient.disconnect(); } setConnected(false); console.log("Disconnected"); } /* Send place */ function sendPlace() { var name = document.getElementById('place').value; stompClient.send("/app/weather", {}, JSON.stringify({ 'place': name })); } /* Show weather info */ function showWeather(wInfo) { var response = document.getElementById('weatherInfo'); var newInfo = "<p>[" + wInfo.localDateTimeStr + "](" + wInfo.place + "): " + wInfo.type + "</p>" var content = response.innerHTML; response.innerHTML = content + newInfo; } |
A su vez estas funciones hacen uso de las librerias stomp.js y sockjs.js. El objetivo es implementar un cliente en JavaScript que:
a. Establezca la conexión con el web socket del servidor.
b. Envie mensajes por el web socket.
c. Escuche todos los mensajes que le lleguen por el web socket y los muestre por pantalla.
Para ello primero ha de conectar. Al conectar, se suscribe al canal del web socket, es decir, ya lo puede usar para enviar y recibir. Despues puede enviar o simplemente permanecer a la escucha. En el momento en el que lea algo del canal, lo procesará. Esto es lo que hace los metodos antes expuestos. El método connect establece la conexión con el web socket “/weather” y suscribe el cliente al canal de escucha “/topic/weatherinfo” que es donde escribe el servidor. En el momento en que recibe algo, lo pasa al método showWeather para que lo procese como un JSON. El método sendPlace() escribe un JSON a la URL “/app/weather”, que como establecimos en la configuración, es la URL en la que el servidor recibirá los mensajes del cliente.
6. El ejemplo
Este sería el aspecto del ejemplo. En un primer momento aparece como desconectado:
Pulsando en el boton de “Connect” establecemos la conexión entre el cliente y el servidor a través del web socket.
Ahora podemos simplemente esperar a que el servidor nos envie algo, o podemos preguntar nosotros, usando el campo y el boton de “Send place”. Si ponemos “Madrid” y pulsamos…
Para apreciar la potencia de la comunicación via web sockets, si abrimos otro navegador y vamos a la misma página, recibiremos también el mismo mensaje del servidor aunque no hayamos preguntado. Si estamos conectados estamos escuchando al mismo canal de comunicación y recibiendo los mismos mensajes que el resto de los clientes. Un ejemplo práctico de esto podría ser un chat o cualquier aplicación en tiempo real.
Se puede descargar el proyecto para Eclipse a modo de ejemplo de este enlace. Y el código fuente desde Github. Más información sobre Spring Web Sockets desde la web oficial.