Websockets escalables con Spring y RabbitMQ

1
6997

Índice de contenidos


1. Introducción

En ciertas ocasiones necesitamos implementar websockets para tener información actualizada en tiempo real con nuestro servidor en nuestras apps y webs.
En otros tutoriales de este sitio hemos visto como implementar esto con spring, o directamente contra un ActiveMQ.

En este tutorial vamos a ver cómo implementar la subscripción y envío de mensajes a un WebSocket controlado con Spring bajo el protocolo Stomp, para ver como se configura todo con anotaciones Spring Boot. Usaremos un ejemplo como el del tutorial oficial de spring.
Como nuestro WebSocket será utilizado por miles de clientes y ahora nos va la moda de hacer microservicios, vamos a dar un paso más y ver cómo podemos escalar esto y poder enviar mensajes desde cualquier otro microservicio sin importar contra que servidor concreto tienen los clientes abierto el websocket. Para ello utilizaremos RabbitMQ como broker.


2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro Retina 15′ (2.2 Ghz Intel Core I7, 16GB DDR3).
  • Sistema Operativo: Mac OS X El Capitan 10.11.6
  • Spring Boot 1.5.7
  • Stomp protocol
  • StompJS


3. ¿Qué vamos a instalar/configurar?

Vamos a implementar una aplicación JEE con Spring Boot que se pueda configurar en clúster para que sea escalable.

Vamos a utilizar el protocolo Stomp para simplificar las comunicaciones y poder utilizar en cliente librerías como StompJS.

Vamos a configurar un RabbitMQ para que se encargue de gestionar las subscripciones de nuestros clientes a través de websockets de tal manera que sea fácil enviar un mensaje a todos los clientes registrados en un topic sin saber contra cuál de los servidores del clúster tienen abierto el socket.

Vamos a ver cómo podemos securizar la subscripción a los topics a través de los websockets.


4. Creación y configuración básica del proyecto

Introducimos en nuestro proyecto Spring Boot la dependencia para poder utilizar websockets:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

Configuramos nuestra aplicación para que admita nuevos websockets con STOMP:

package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

@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("/gs-guide-websocket").withSockJS();
    }
}

Con esto aceptamos nuevos sockets bajo la url «/gs-guide-websocket», podrán enviar mensajes a urls tipo «/app/*» y subscribirse a topics en urls tipo «/topic/*».

Creamos nuestro primer «controller» para recibir mensajes a través del websocket a la ruta «/app/hello» con un objeto tipo «HelloMessage» que será automáticamente parseado desde el json que se reciba. Y vamos a enviar un mensaje al topic «/topic/greetings» con un objeto tipo «Greeting» que será parseado a json para ser enviado a través del websocket.

package hello;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

@Controller
public class GreetingController {

    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    public Greeting greeting(HelloMessage message) throws Exception {
        Thread.sleep(1000); // simulated delay
        return new Greeting("Hello, " + message.getName() + "!");
    }

}

Con un cliente js tipo «StompJS» podemos tener un código sencillo de conexión desde un browser a un websocket, para hacer envío de mensajes y subscribirse a topics para recepción de mensajes.

var stompClient = null;

function connect() {
    var socket = new SockJS('/gs-guide-websocket');
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function (frame) {
        setConnected(true);
        console.log('Connected: ' + frame);
        stompClient.subscribe('/topic/greetings', function (greeting) {
            showGreeting(JSON.parse(greeting.body).content);
        });
    });
}

function disconnect() {
    if (stompClient !== null) {
        stompClient.disconnect();
    }
    setConnected(false);
    console.log("Disconnected");
}

function sendName() {
    stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
}

5. Conectándolo a RabbitMQ

Ya podemos crear websockets pero si escalamos la aplicación los mensajes que enviemos a un topic solo serán recibidos por los clientes que tengan abierto el websocket contra ese mismo servidor.

Vamos a modificar la configuración de spring de websockets de la clase «WebSocketConfig» cambiando la linea ‘config.enableSimpleBroker(«/topic»)’ por:

	config.enableStompBrokerRelay("/queue", "/topic")
        .setUserDestinationBroadcast("/topic/unresolved.user.dest")
        .setUserRegistryBroadcast("/topic/registry.broadcast")
        .setRelayHost(springStompHost)
        .setRelayPort(springStompPort)
        .setClientLogin(springStompUsername)
        .setClientPasscode(springStompPassword)
        .setSystemLogin(springStompUsername)
        .setSystemPasscode(springStompPassword)

Para que RabbitMQ soporte STOMP debemos habilitarle el plugin de «rabbitmq_web_stomp»:

	rabbitmq-plugins enable --offline rabbitmq_web_stomp

Ahora es RabbitMQ el que mantiene el control de que websockets están conectados a que topics por lo que podemos enviar un mensaje a cualquier topic desde cualquier servidor con conexión a rabbitmq. Un ejemplo de envío usando «SimpMessagingTemplate»:

@Autowired
private SimpMessagingTemplate template;

  public void sendToTopicGreetings(Greeting greeting) {
      template.convertAndSend("/topic/greetings", greeting);
  }


6. Securizando subscripciones

Podemos añadir un interceptor para controlar las subscripciones a distintos topics y securizar éstas. Para ello modificamos la clase «WebSocketConfig»:

@Override
 public void configureClientInboundChannel(ChannelRegistration registration) {
	 registration.setInterceptors(new ChannelInterceptorAdapter() {
		@Override
		public Message preSend(Message message, MessageChannel channel) {
			if (message.getHeaders().get("stompCommand") == StompCommand.SUBSCRIBE) {
				//Añadir las condiciones deseadas de seguridad y lanzar excepción si no se pasan
				//message.getHeaders().get("simpDestination") --> para ver el destino de la subscripción: "/topic/*"
			}
		}
	 });
}
 

El cliente StompJS permite enviar headers para incluir la autenticación. Estos headers pueden obtenerse en el servidor del «message» con:

  Map<String, LinkedList<String>> nativeHeaders = (Map<String, LinkedList<String>>) message.getHeaders().get("nativeHeaders");
  LinkedList<String> authorizations = nativeHeaders == null ? null : nativeHeaders.get("Authorization");
  String authorization = authorizations == null || authorizations.size() == 0 ? null : authorizations.getFirst();
 


7. Conclusiones

Hemos visto cómo podemos utilizar WebSockets en nuestra app para tener comunicaciones en tiempo real en un entorno que es escalable y es fácilmente integrable como un microservicio nuevo en nuestro ecosistema, permitiendo a cualquier microservicio el envío de mensajes a los distintos topics de nuestros websockets utilizando RabbitMQ como broker.


8. Referencias

1 COMENTARIO

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