Índice de contenidos
- 1. Introducción
- 2. Entorno
- 3. ¿Qué vamos a instalar/configurar?
- 4. Creación y configuración básica del proyecto
- 5. Conectándolo a RabbitMQ
- 6. Securizando subscripciones
- 7. Conclusiones
- 8. Referencias
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.
Hola,
Me gustaría saber si el proyecto completo se encuentra en algún repositorio publico.
Topic: http://adictosaltrabajo.com/2017/11/06/websockets-escalables-con-spring-y-rabbitmq/
Muchas gracias