Notificaciones vía web sockets con Spring Messaging en una aplicación AngularJS

7
18480

Existen muchas técnicas para mantener una conexión bidireccional entre cliente y servidor, en este tutorial vamos a ver como hacerlo con el soporte de Spring en una aplicación AngularJS

0. Índice de contenidos.


1. Introducción

Ya hemos visto como podemos implementar una conexión bidireccional entre cliente y servidor
estableciendo un canal de comunicación en tiempo real con el soporte de Web Sockets y:

En esta entrada haremos uso de la librería spring-websocket desarrollada bajo el proyecto
spring-messaging que es una abstracción sobre el soporte nativo
de web sockets del servidor sobre el cuál arranca nuestra aplicación, de modo tal que podemos seguir desplegando
sobre un Apache Tomcat pero en vez de configurar y registrar de forma nativa la conectividad lo haremos con el
soporte de Spring beneficiándonos de su configurabilidad y el soporte de inyección de dependencias en nuestros
servicios.

Los web sockets son una capa ligera de comunicación sobre TCP que permite el uso de otros protocolos a bajo nivel para encapsular los mensajes; en este tutorial usaremos STOMP desde una aplicación AngularJS para
conectarnos/desconectarnos al servidor y enviar y recibir mensajes.

2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.3 GHz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS Mavericks 10.9.4
  • Spring 4.1.6.RELEASE
  • Apache Tomcat 8.0.20


3. Configuración.

Como siempre, asumiendo que nuestro proyecto tiene el soporte de maven, lo primero es configurar las dependencias en el pom.xml:

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-webmvc</artifactId>
	<version>4.1.6.RELEASE</version>
</dependency>

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-web</artifactId>
	<version>4.1.6.RELEASE</version>
</dependency>

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-messaging</artifactId>
	<version>4.1.6.RELEASE</version>
</dependency>

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-websocket</artifactId>
	<version>4.1.6.RELEASE</version>
</dependency>

A continuación, podríamos configurar por anotaciones el soporte de web sockets, pero soy un viejuno y prefiero la configuración vía xml:

<beans 	xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xmlns:context="http://www.springframework.org/schema/context"
		xmlns:p="http://www.springframework.org/schema/p"
     	xmlns:mvc="http://www.springframework.org/schema/mvc"
	    xmlns:websocket="http://www.springframework.org/schema/websocket"
     	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 
     	http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd 
     	http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd ">

	<websocket:message-broker application-destination-prefix="/app">

		<websocket:stomp-endpoint path="/chat" >
		            <websocket:sockjs/>
		</websocket:stomp-endpoint>

		<websocket:simple-broker prefix="/topic" />

	</websocket:message-broker>
	
</beans>

Con lo anterior estamos habilitando:

  • websocket:message-broker: el soporte de mensajería generíco de spring-messaging para el prefijo de «app»; para remitir un mensaje desde el cliente a un endPoint «chat», se hará a través de «/app/chat»
  • websocket:stomp-endpoint: registra un endPoint «chat» habilitando SockJS
  • websocket:simple-broker: un sencillo broker de mensajería basado en memoria que registra un «topic», con ese prefijo, para el transporte de los mensajes a los clientes.

SockJS es una librería Javascript para navegadores que permite el uso de WebSockets orientado a objetos.
SockJS proporciona un api, cross-browser, cross-domain que permite una comunicación entre el navegador y
el servidor de modo tal que si el navegador soporta WebSockets hace uso de la implementación del mismo y, sino lo soporta, lo emula de forma nativa.

Por último, en el descriptor de despliegue web.xml tendríamos que declarar el front controller de spring MVC
con una configuración similar a la siguiente:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">


	<servlet>
		<servlet-name>rest</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>
	
	<servlet-mapping>
		<servlet-name>rest</servlet-name>
		<url-pattern>/rest/*</url-pattern>
	</servlet-mapping>
      

	<welcome-file-list>
		<welcome-file>index.html</welcome-file>
	</welcome-file-list>


</web-app> 


4. Creación de un manejador de mensajes.

Con el soporte de spring mvc podemos registrar un controlador de la siguiente forma

@Controller
public class MessagesController {
		@MessageMapping("/sendMessage")
		@SendTo("/topic/messages")
		public Message receive(Message message) throws Exception {
			Thread.sleep(3000); // simulated delay
			return message;
		}
}

Message es un POJO que encapsula como propiedades el texto del mensaje .


public class Message {

	private String text;
	
    public Message() {
    }

    public Message(String text) {
	   this.text = text;
    }

    public String getText() {
        return text;
    }
    
}

El método que mapea la URL «sendMessage», recibe un mensaje y remite el mismo a la cola de mensajería «topic/messages» a la que se pueden suscribir los clientes al conectarse al socket.

Con todo lo anterior tendríamos la siguiente configuración:

  • «topic/messages» para que los clientes se puedan suscribir y recibir los mensajes de saludo del servidor,
  • «app/chat» para que los clientes puedan enviar sus mensajes
  • el prefijo de los recursos anteriores será el contexto de la aplicación + la url-pattern de la declaración del servlet.


5. Soporte de STOMP en AngularJS.

En este punto tenemos dos opciones:

  • buscar un módulo mantenido por un tercero que nos de soporte de STOMP en el cliente, como muchas otras librerías que usamos,
  • implementar nuestro propio servicio de conexión, suscripción y envio de mensajes.

Vamos a optar por la segunda opción puesto que en pruebas de concepto anteriores hemos podido comprobar que
el soporte del modulo «angular-stomp» prevé la conectividad por ws:/ pero no soporta la conexión por http:// y
además queremos tener más control y así comprobamos como crear nuestro propio servicio en AngularJS.

Lo primero es añadir las dependencias de las librerías que nos van a proporcionar el soporte de web sockets y el protocolo de comunicaciones;
si para la parte servidora/java declarábamos las dependencias en el pom.xml de maven, nuestro proyecto de cliente/javascript está gestionado por bower
con lo que debemos añadir las siguientes dependencias a nuestro bower.json

"dependencies": {
	"sockjs": "0.3.4",
	"stomp-websocket": "2.3.4",
}

Con ello ya podemos hacer uso de la librería de socksJS para implementar nuestro servicio dentro de un módulo de prueba «pushApp»:

angular
    .module('pushApp', []).
    factory('stompService',['$rootScope', function($rootScope) {
    	
    	var client = {};
    	
        var service = function(url){
        	
            var socket = new SockJS(url);
            client = Stomp.over(socket);
            
            return {
            	subscribe: subscribe,
            	send: send,
            	connect: connect,
            	disconnect: disconnect
            }
    	};
        
		return service;
		
		function subscribe(queue, callback) {
            client.subscribe(queue, function() {
            	var args = arguments;
            	$rootScope.$apply(function() {
                    callback(args[0]);
                })
            })
		}
		
		function send(queue, headers, data){
			client.send(queue, headers, data);
		}

		function connect(user, password, on_connect, on_error, vhost) {
            client.connect(user, password,
            function(frame) {
             $rootScope.$apply(function() {
                 on_connect.apply(client, frame);
             })
            },
            function(frame) {
                $rootScope.$apply(function() {
                    on_error.apply(client, frame);
                })
            }, vhost);
			
		}

		function disconnect(callback){
            client.disconnect(function() {
                $rootScope.$apply(function() {
                    callback.apply(args);
                })
            })
		}
    }]
  );

En esta implementación inicial nos hemos basado en la librería que comentabamos pero modificando el prototipado y
el modo en el que se crea el cliente para permitir la conexión vía http.
Al no basarse en promesas propaga los cambios al ámbito $rootScope con la función $apply.

Para hacer uso de la librería bastaría con inyectarla en un controlador:

  angular.
    module('pushApp').
    controller('stompCtrl', ['$scope', 'stompService', stompCtrl]);
    
    function stompCtrl($scope, stompService){
      $scope.messages = [];
      $scope.text = "";
      $scope.chat = stompService('/tnt-labs/rest/chat');

      $scope.chat.connect("guest", "guest", function(){
          $scope.chat.subscribe("/topic/messages", function(message) {
              var body = JSON.parse(message.body)
              $scope.messages.push(body.text);
          });
      });
      
      $scope.sendName = function(){
          $scope.client.send("/app/sendMessage", {}, JSON.stringify({ 'text': $scope.text }));
      }
    }

Y desde el html podríamos tener un código como el siguiente para enviar mensajes y recibirlos,
tanto los nuestros como los de otros clientes.

<div ng-app="pushApp" >
	<div ng-controller="stompCtrl">
	    <div >
    	    <h1>Messages</h1>
        	<p ng-repeat="message in messages">{{message}}</p>
    	</div>
    	<div ng-disabled="$scope.client != undefined">
	        <label>Mensaje</label><input type="text" ng-model="text"/>
	        <button id="sendName" ng-click="sendName();">Send</button>
    	</div>
	</div>
</div>

A nivel de interfaz deberíamos ver algo como lo siguiente:



6. Referencias.


7. Conclusiones.

Hemos visto una aproximación adicional a las que ya proponiamos con web sockets haciendo uso además de un servicio en Angular.

Vale que no aún no tenemos un chat al uso, solo enviamos y recibimos mensajes a todos los usuarios conectados, pero no se puede tener todo en una primera aproximación.

Stay tuned!

Un saludo.

Jose

7 COMENTARIOS

  1. Hola, primero felicitarte por el excelente Post.
    Tengo una duda seria posible por ejemplo enviar un mensaje desde el servidor a los clientes a tráves del web socket según un evento propio del servidor, me refiero a que si por ejemplo tengo mi maquina servidor conectada a una placa arduino y cuando esta me de cierta información yo la comunique a todos mis clientes web.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad