Uso de Spring Boot para la implementación de WebSocket con STOMP
Publicado: 2022-03-11El protocolo WebSocket es una de las formas de hacer que su aplicación maneje mensajes en tiempo real. Las alternativas más comunes son los sondeos largos y los eventos enviados por el servidor. Cada una de estas soluciones tiene sus ventajas e inconvenientes. En este artículo, le mostraré cómo implementar WebSockets con Spring Boot Framework. Cubriré tanto la configuración del lado del servidor como del lado del cliente, y usaremos STOMP sobre el protocolo WebSocket para comunicarnos entre nosotros.
El lado del servidor se codificará exclusivamente en Java. Pero, en el caso del cliente, mostraré fragmentos escritos tanto en Java como en JavaScript (SockJS) ya que, por lo general, los clientes de WebSockets están integrados en las aplicaciones front-end. Los ejemplos de código demostrarán cómo transmitir mensajes a varios usuarios utilizando el modelo pub-sub y cómo enviar mensajes solo a un solo usuario. En otra parte del artículo, hablaré brevemente sobre la protección de WebSockets y cómo podemos asegurarnos de que nuestra solución basada en WebSocket permanecerá operativa incluso cuando el entorno no admita el protocolo WebSocket.
Tenga en cuenta que el tema de la protección de WebSockets solo se abordará brevemente aquí, ya que es un tema lo suficientemente complejo para un artículo separado. ¿Debido a esto y a varios otros factores que menciono en el WebSocket en producción? sección al final, recomiendo hacer modificaciones antes de usar esta configuración en producción , lea hasta el final para obtener una configuración lista para producción con medidas de seguridad implementadas.
Protocolos WebSocket y STOMP
El protocolo WebSocket le permite implementar una comunicación bidireccional entre aplicaciones. Es importante saber que HTTP se usa solo para el protocolo de enlace inicial. Después de que suceda, la conexión HTTP se actualiza a una conexión TCP/IP recién abierta que utiliza un WebSocket.
El protocolo WebSocket es un protocolo de nivel bastante bajo. Define cómo un flujo de bytes se transforma en marcos. Un marco puede contener un texto o un mensaje binario. Debido a que el mensaje en sí no proporciona información adicional sobre cómo enrutarlo o procesarlo, es difícil implementar aplicaciones más complejas sin escribir código adicional. Afortunadamente, la especificación WebSocket permite el uso de subprotocolos que operan en un nivel de aplicación superior. Uno de ellos, soportado por Spring Framework, es STOMP.
STOMP es un protocolo de mensajería simple basado en texto que se creó inicialmente para lenguajes de secuencias de comandos como Ruby, Python y Perl para conectarse a intermediarios de mensajes empresariales. Gracias a STOMP, los clientes y corredores desarrollados en diferentes idiomas pueden enviar y recibir mensajes entre ellos. El protocolo WebSocket a veces se denomina TCP para Web. De manera análoga, STOMP se llama HTTP para Web. Define un puñado de tipos de marcos que se asignan a los marcos de WebSockets, por ejemplo, CONNECT
, SUBSCRIBE
, UNSUBSCRIBE
, ACK
o SEND
. Por un lado, estos comandos son muy útiles para gestionar la comunicación y, por otro, nos permiten implementar soluciones con funciones más sofisticadas como el reconocimiento de mensajes.
El lado del servidor: Spring Boot y WebSockets
Para construir el lado del servidor WebSocket, utilizaremos el marco Spring Boot que acelera significativamente el desarrollo de aplicaciones web e independientes en Java. Spring Boot incluye el módulo spring-WebSocket
, que es compatible con el estándar API de Java WebSocket (JSR-356).
La implementación del lado del servidor WebSocket con Spring Boot no es una tarea muy compleja e incluye solo un par de pasos, que veremos uno por uno.
Paso 1. Primero, debemos agregar la dependencia de la biblioteca WebSocket.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
Si planea utilizar el formato JSON para los mensajes transmitidos, es posible que desee incluir también la dependencia GSON o Jackson. Es muy probable que también necesite un marco de seguridad, por ejemplo, Spring Security.
Paso 2. Luego, podemos configurar Spring para habilitar la mensajería WebSocket y STOMP.
Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/mywebsockets") .setAllowedOrigins("mydomain.com").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry config){ config.enableSimpleBroker("/topic/", "/queue/"); config.setApplicationDestinationPrefixes("/app"); } }
El método configureMessageBroker
hace dos cosas:
- Crea el intermediario de mensajes en memoria con uno o más destinos para enviar y recibir mensajes. En el ejemplo anterior, hay dos prefijos de destino definidos:
topic
yqueue
. Siguen la convención de que los destinos de los mensajes que se transmitirán a todos los clientes suscritos a través del modelo pub-sub deben tener el prefijotopic
. Por otro lado, los destinos de los mensajes privados suelen tener el prefijoqueue
. - Define la
app
de prefijo que se utiliza para filtrar destinos controlados por métodos anotados con@MessageMapping
que implementará en un controlador. El controlador, después de procesar el mensaje, lo enviará al intermediario.
Volviendo al fragmento anterior, probablemente haya notado una llamada al método withSockJS()
, habilita las opciones de respaldo de SockJS. Para abreviar, permitirá que nuestros WebSockets funcionen incluso si el protocolo WebSocket no es compatible con un navegador de Internet. Discutiré este tema con mayor detalle un poco más.
Hay una cosa más que necesita aclaración: por qué llamamos setAllowedOrigins()
en el punto final. A menudo es necesario porque el comportamiento predeterminado de WebSocket y SockJS es aceptar solo solicitudes del mismo origen. Por lo tanto, si su cliente y el lado del servidor usan diferentes dominios, se debe llamar a este método para permitir la comunicación entre ellos.
Paso 3 Implemente un controlador que manejará las solicitudes de los usuarios. Transmitirá el mensaje recibido a todos los usuarios suscritos a un tema determinado.
Aquí hay un método de muestra que envía mensajes al destino /topic/news
.
@MessageMapping("/news") @SendTo("/topic/news") public void broadcastNews(@Payload String message) { return message; }
En lugar de la anotación @SendTo
, también puede usar SimpMessagingTemplate
que puede conectar automáticamente dentro de su controlador.
@MessageMapping("/news") public void broadcastNews(@Payload String message) { this.simpMessagingTemplate.convertAndSend("/topic/news", message) }
En pasos posteriores, es posible que desee agregar algunas clases adicionales para proteger sus terminales, como ResourceServerConfigurerAdapter
o WebSecurityConfigurerAdapter
del marco Spring Security. Además, a menudo es beneficioso implementar el modelo de mensaje para que el JSON transmitido se pueda asignar a los objetos.

Creación del cliente WebSocket
Implementar un cliente es una tarea aún más sencilla.
Paso 1. Cliente Autowire Spring STOMP.
@Autowired private WebSocketStompClient stompClient;
Paso 2. Abra una conexión.
StompSessionHandler sessionHandler = new CustmStompSessionHandler(); StompSession stompSession = stompClient.connect(loggerServerQueueUrl, sessionHandler).get();
Una vez hecho esto, es posible enviar un mensaje a un destino. El mensaje se enviará a todos los usuarios suscritos a un tema.
stompSession.send("topic/greetings", "Hello new user");
También es posible suscribirse para recibir mensajes.
session.subscribe("topic/greetings", this); @Override public void handleFrame(StompHeaders headers, Object payload) { Message msg = (Message) payload; logger.info("Received : " + msg.getText()+ " from : " + msg.getFrom()); }
A veces es necesario enviar un mensaje solo a un usuario dedicado (por ejemplo, al implementar un chat). Luego, el cliente y el lado del servidor deben usar un destino separado dedicado a esta conversación privada. El nombre del destino se puede crear agregando un identificador único a un nombre de destino general, por ejemplo, /queue/chat-user123
. Los identificadores de sesión HTTP o STOMP se pueden utilizar para este propósito.
Spring hace que enviar mensajes privados sea mucho más fácil. Solo necesitamos anotar el método de un controlador con @SendToUser
. Luego, este destino será manejado por UserDestinationMessageHandler
, que se basa en un identificador de sesión. En el lado del cliente, cuando un cliente se suscribe a un destino con el prefijo /user
, este destino se transforma en un destino único para este usuario. En el lado del servidor, el destino de un usuario se resuelve en función del Principal
de un usuario.
Ejemplo de código del lado del servidor con la anotación @SendToUser
:
@MessageMapping("/greetings") @SendToUser("/queue/greetings") public String reply(@Payload String message, Principal user) { return "Hello " + message; }
O puede usar SimpMessagingTemplate
:
String username = ... this.simpMessagingTemplate.convertAndSendToUser(); username, "/queue/greetings", "Hello " + username);
Veamos ahora cómo implementar un cliente JavaScript (SockJS) capaz de recibir mensajes privados que podría enviar el código Java en el ejemplo anterior. Vale la pena saber que los WebSockets son parte de la especificación HTML5 y son compatibles con la mayoría de los navegadores modernos (Internet Explorer los admite desde la versión 10).
function connect() { var socket = new SockJS('/greetings'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { stompClient.subscribe('/user/queue/greetings', function (greeting) { showGreeting(JSON.parse(greeting.body).name); }); }); } function sendName() { stompClient.send("/app/greetings", {}, $("#name").val()); }
Como probablemente haya notado, para recibir mensajes privados, el cliente debe suscribirse a un destino general /queue/greetings
con el prefijo /user
. No tiene que molestarse con ningún identificador único. Sin embargo, el cliente debe iniciar sesión en la aplicación antes, por lo que se inicializa el objeto Principal
en el lado del servidor.
Protección de WebSockets
Muchas aplicaciones web utilizan autenticación basada en cookies. Por ejemplo, podemos usar Spring Security para restringir el acceso a ciertas páginas o Controladores solo a usuarios registrados. El contexto de seguridad del usuario se mantiene a través de una sesión HTTP basada en cookies que luego se asocia con las sesiones WebSocket o SockJS creadas para ese usuario. Los puntos finales de WebSockets se pueden proteger como cualquier otra solicitud, por ejemplo, en WebSecurityConfigurerAdapter
de Spring.
Hoy en día, las aplicaciones web a menudo usan API REST como back-end y tokens OAuth/JWT para la autenticación y autorización de usuarios. El protocolo WebSocket no describe cómo los servidores deben autenticar a los clientes durante el protocolo de enlace HTTP. En la práctica, se utilizan encabezados HTTP estándar (p. ej., Autorización) para este fin. Desafortunadamente, no es compatible con todos los clientes de STOMP. El cliente STOMP de Spring Java permite establecer encabezados para el apretón de manos:
WebSocketHttpHeaders handshakeHeaders = new WebSocketHttpHeaders(); handshakeHeaders.add(principalRequestHeader, principalRequestValue);
Pero el cliente JavaScript de SockJS no admite el envío de un encabezado de autorización con una solicitud de SockJS. Sin embargo, permite enviar parámetros de consulta que se pueden usar para pasar un token. Este enfoque requiere escribir un código personalizado en el lado del servidor que leerá el token de los parámetros de consulta y lo validará. También es importante asegurarse de que los tokens no se registren junto con las solicitudes (o que los registros estén bien protegidos), ya que esto podría generar una violación de seguridad grave.
Opciones de respaldo de SockJS
Es posible que la integración con WebSocket no siempre sea fluida. Algunos navegadores (p. ej., IE 9) no son compatibles con WebSockets. Además, los proxies restrictivos pueden hacer que sea imposible realizar la actualización de HTTP o cortan las conexiones que están abiertas durante demasiado tiempo. En tales casos, SockJS viene al rescate.
Los transportes de SockJS se dividen en tres categorías generales: WebSockets, HTTP Streaming y HTTP Long Polling. La comunicación comienza con SockJS enviando GET /info
para obtener información básica del servidor. En función de la respuesta, SockJS decide el transporte que se utilizará. La primera opción son WebSockets. Si no son compatibles, entonces, si es posible, se utiliza Streaming. Si esta opción tampoco es posible, se elige Polling como método de transporte.
¿WebSocket en producción?
Si bien esta configuración funciona, no es la "mejor". Spring Boot le permite utilizar cualquier sistema de mensajería completo con el protocolo STOMP (p. ej., ActiveMQ, RabbitMQ), y un intermediario externo puede admitir más operaciones STOMP (p. ej., reconocimientos, recibos) que el intermediario simple que utilizamos. STOMP Over WebSocket proporciona información interesante sobre WebSockets y el protocolo STOMP. Enumera los sistemas de mensajería que manejan el protocolo STOMP y podría ser una mejor solución para usar en producción. Especialmente si, debido a la gran cantidad de solicitudes, el intermediario de mensajes debe agruparse. (El intermediario de mensajes simple de Spring no es adecuado para la agrupación). Luego, en lugar de habilitar el intermediario simple en WebSocketConfig
, es necesario habilitar el relevo del intermediario Stomp que reenvía mensajes hacia y desde un intermediario de mensajes externo. En resumen, un intermediario de mensajes externo puede ayudarlo a crear una solución más escalable y robusta.
Si está listo para continuar su viaje como desarrollador de Java explorando Spring Boot, intente leer Uso de Spring Boot para OAuth2 y JWT REST Protection a continuación.