Utilizzo di Spring Boot per l'implementazione di WebSocket con STOMP
Pubblicato: 2022-03-11Il protocollo WebSocket è uno dei modi per far sì che la tua applicazione gestisca i messaggi in tempo reale. Le alternative più comuni sono il polling lungo e gli eventi inviati dal server. Ognuna di queste soluzioni ha i suoi vantaggi e svantaggi. In questo articolo, ti mostrerò come implementare WebSocket con Spring Boot Framework. Tratterò sia la configurazione lato server che quella lato client e utilizzeremo il protocollo STOMP su WebSocket per comunicare tra loro.
Il lato server sarà codificato esclusivamente in Java. Ma, nel caso del client, mostrerò snippet scritti sia in Java che in JavaScript (SockJS) poiché, in genere, i client WebSocket sono incorporati nelle applicazioni front-end. Gli esempi di codice dimostreranno come trasmettere messaggi a più utenti utilizzando il modello pub-sub e come inviare messaggi solo a un singolo utente. In un'altra parte dell'articolo, parlerò brevemente della protezione dei WebSocket e di come possiamo garantire che la nostra soluzione basata su WebSocket rimanga operativa anche quando l'ambiente non supporta il protocollo WebSocket.
Si prega di notare che l'argomento della protezione dei WebSocket verrà trattato solo brevemente qui poiché è un argomento abbastanza complesso per un articolo separato. A causa di questo e di molti altri fattori che tocco nel WebSocket in produzione? sezione alla fine, consiglio di apportare modifiche prima di utilizzare questa configurazione in produzione , leggere fino alla fine per una configurazione pronta per la produzione con misure di sicurezza in atto.
Protocolli WebSocket e STOMP
Il protocollo WebSocket consente di implementare la comunicazione bidirezionale tra le applicazioni. È importante sapere che HTTP viene utilizzato solo per l'handshake iniziale. Dopo che si verifica, la connessione HTTP viene aggiornata a una connessione TCP/IP appena aperta utilizzata da un WebSocket.
Il protocollo WebSocket è un protocollo di livello piuttosto basso. Definisce come un flusso di byte viene trasformato in frame. Un frame può contenere un testo o un messaggio binario. Poiché il messaggio stesso non fornisce informazioni aggiuntive su come instradarlo o elaborarlo, è difficile implementare applicazioni più complesse senza scrivere codice aggiuntivo. Fortunatamente, la specifica WebSocket consente l'utilizzo di sottoprotocolli che operano a un livello applicativo superiore. Uno di questi, supportato dallo Spring Framework, è STOMP.
STOMP è un semplice protocollo di messaggistica basato su testo che è stato inizialmente creato per linguaggi di scripting come Ruby, Python e Perl per connettersi a broker di messaggi aziendali. Grazie a STOMP, clienti e broker sviluppati in diverse lingue possono inviare e ricevere messaggi da e verso l'altro. Il protocollo WebSocket è talvolta chiamato TCP per Web. Analogamente, STOMP è chiamato HTTP per Web. Definisce una manciata di tipi di frame mappati sui frame WebSocket, ad esempio CONNECT
, SUBSCRIBE
, UNSUBSCRIBE
, ACK
o SEND
. Da un lato, questi comandi sono molto utili per gestire la comunicazione mentre, dall'altro, ci consentono di implementare soluzioni con funzionalità più sofisticate come il riconoscimento dei messaggi.
Il lato server: Spring Boot e WebSocket
Per costruire WebSocket lato server, utilizzeremo il framework Spring Boot che velocizza notevolmente lo sviluppo di applicazioni web e standalone in Java. Spring Boot include il modulo spring-WebSocket
, compatibile con lo standard API Java WebSocket (JSR-356).
L'implementazione di WebSocket lato server con Spring Boot non è un compito molto complesso e include solo un paio di passaggi, che analizzeremo uno per uno.
Passaggio 1. Innanzitutto, è necessario aggiungere la dipendenza della libreria WebSocket.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
Se prevedi di utilizzare il formato JSON per i messaggi trasmessi, potresti voler includere anche la dipendenza GSON o Jackson. Molto probabilmente, potresti anche aver bisogno di un framework di sicurezza, ad esempio Spring Security.
Passaggio 2. Quindi, possiamo configurare Spring per abilitare la messaggistica WebSocket e 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"); } }
Il metodo configureMessageBroker
fa due cose:
- Crea il broker di messaggi in memoria con una o più destinazioni per l'invio e la ricezione di messaggi. Nell'esempio sopra, sono definiti due prefissi di destinazione:
topic
equeue
. Seguono la convenzione secondo cui le destinazioni per i messaggi da trasmettere a tutti i client sottoscritti tramite il modello pub-sub devono essere precedute datopic
. D'altra parte, le destinazioni per i messaggi privati sono in genere precedute daqueue
. - Definisce l'
app
del prefisso usata per filtrare le destinazioni gestite dai metodi annotati con@MessageMapping
che implementerai in un controller. Il titolare, dopo aver elaborato il messaggio, lo trasmetterà al broker.
Tornando allo snippet sopra, probabilmente hai notato una chiamata al metodo withSockJS()
, abilita le opzioni di fallback di SockJS. Per farla breve, consentirà ai nostri WebSocket di funzionare anche se il protocollo WebSocket non è supportato da un browser Internet. Discuterò questo argomento in modo più dettagliato un po 'più avanti.
C'è un'altra cosa che deve essere chiarita: perché chiamiamo il metodo setAllowedOrigins()
sull'endpoint. È spesso richiesto perché il comportamento predefinito di WebSocket e SockJS consiste nell'accettare solo richieste della stessa origine. Quindi, se il tuo client e il lato server utilizzano domini diversi, questo metodo deve essere chiamato per consentire la comunicazione tra di loro.
Passaggio 3 . Implementare un controller che gestirà le richieste degli utenti. Trasmetterà il messaggio ricevuto a tutti gli utenti iscritti a un determinato argomento.
Ecco un metodo di esempio che invia messaggi alla destinazione /topic/news
.
@MessageMapping("/news") @SendTo("/topic/news") public void broadcastNews(@Payload String message) { return message; }
Invece dell'annotazione @SendTo
, puoi anche usare SimpMessagingTemplate
che puoi collegare automaticamente all'interno del tuo controller.
@MessageMapping("/news") public void broadcastNews(@Payload String message) { this.simpMessagingTemplate.convertAndSend("/topic/news", message) }
Nei passaggi successivi, potresti voler aggiungere alcune classi aggiuntive per proteggere i tuoi endpoint, come ResourceServerConfigurerAdapter
o WebSecurityConfigurerAdapter
dal framework Spring Security. Inoltre, è spesso utile implementare il modello di messaggio in modo che il JSON trasmesso possa essere mappato agli oggetti.

Creazione del client WebSocket
L'implementazione di un client è un compito ancora più semplice.
Passaggio 1. Client Autowire Spring STOMP.
@Autowired private WebSocketStompClient stompClient;
Passaggio 2. Apri una connessione.
StompSessionHandler sessionHandler = new CustmStompSessionHandler(); StompSession stompSession = stompClient.connect(loggerServerQueueUrl, sessionHandler).get();
Fatto ciò, è possibile inviare un messaggio a una destinazione. Il messaggio verrà inviato a tutti gli utenti iscritti a un argomento.
stompSession.send("topic/greetings", "Hello new user");
È anche possibile iscriversi ai messaggi.
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 volte è necessario inviare un messaggio solo a un utente dedicato (ad esempio quando si implementa una chat). Quindi, il client e il lato server devono utilizzare una destinazione separata dedicata a questa conversazione privata. Il nome della destinazione può essere creato aggiungendo un identificatore univoco a un nome di destinazione generale, ad esempio /queue/chat-user123
. A questo scopo possono essere utilizzati identificatori di sessione HTTP o STOMP.
La primavera rende molto più semplice l'invio di messaggi privati. Abbiamo solo bisogno di annotare il metodo di un Controller con @SendToUser
. Quindi, questa destinazione verrà gestita da UserDestinationMessageHandler
, che si basa su un identificatore di sessione. Sul lato client, quando un client si iscrive a una destinazione con il prefisso /user
, questa destinazione viene trasformata in una destinazione univoca per questo utente. Sul lato server, una destinazione utente viene risolta in base al Principal
.
Esempio di codice lato server con annotazione @SendToUser
:
@MessageMapping("/greetings") @SendToUser("/queue/greetings") public String reply(@Payload String message, Principal user) { return "Hello " + message; }
Oppure puoi usare SimpMessagingTemplate
:
String username = ... this.simpMessagingTemplate.convertAndSendToUser(); username, "/queue/greetings", "Hello " + username);
Vediamo ora come implementare un client JavaScript (SockJS) in grado di ricevere messaggi privati che potrebbero essere inviati dal codice Java nell'esempio sopra. Vale la pena sapere che i WebSocket fanno parte delle specifiche HTML5 e sono supportati dalla maggior parte dei browser moderni (Internet Explorer li supporta dalla versione 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()); }
Come probabilmente avrai notato, per ricevere messaggi privati, il client deve iscriversi a una destinazione generale /queue/greetings
con il prefisso /user
. Non deve preoccuparsi di identificatori univoci. Tuttavia, il client deve prima accedere all'applicazione, quindi l'oggetto Principal
sul lato server viene inizializzato.
Protezione dei WebSocket
Molte applicazioni Web utilizzano l'autenticazione basata sui cookie. Ad esempio, possiamo utilizzare Spring Security per limitare l'accesso a determinate pagine o Controller solo agli utenti registrati. Il contesto di sicurezza dell'utente viene quindi mantenuto tramite una sessione HTTP basata su cookie che viene successivamente associata alle sessioni WebSocket o SockJS create per quell'utente. Gli endpoint WebSocket possono essere protetti come qualsiasi altra richiesta, ad esempio in WebSecurityConfigurerAdapter
di Spring.
Al giorno d'oggi, le applicazioni Web utilizzano spesso API REST come token back-end e OAuth/JWT per l'autenticazione e l'autorizzazione degli utenti. Il protocollo WebSocket non descrive come i server devono autenticare i client durante l'handshake HTTP. In pratica, a questo scopo vengono utilizzate intestazioni HTTP standard (ad es. Autorizzazione). Sfortunatamente, non è supportato da tutti i client STOMP. Il client STOMP di Spring Java consente di impostare le intestazioni per l'handshake:
WebSocketHttpHeaders handshakeHeaders = new WebSocketHttpHeaders(); handshakeHeaders.add(principalRequestHeader, principalRequestValue);
Ma il client JavaScript SockJS non supporta l'invio di intestazione di autorizzazione con una richiesta SockJS. Tuttavia, consente l'invio di parametri di query che possono essere utilizzati per passare un token. Questo approccio richiede la scrittura di codice personalizzato sul lato server che leggerà il token dai parametri della query e lo convaliderà. È anche importante assicurarsi che i token non siano registrati insieme alle richieste (o che i registri siano ben protetti) poiché ciò potrebbe introdurre una grave violazione della sicurezza.
Opzioni di fallback di SockJS
L'integrazione con WebSocket potrebbe non andare sempre bene. Alcuni browser (ad es. IE 9) non supportano WebSocket. Inoltre, i proxy restrittivi possono rendere impossibile eseguire l'aggiornamento HTTP o interrompere le connessioni aperte per troppo tempo. In questi casi, SockJS viene in soccorso.
I trasporti SockJS rientrano in tre categorie generali: WebSocket, HTTP Streaming e HTTP Long Polling. La comunicazione inizia con SockJS che invia GET /info
per ottenere le informazioni di base dal server. Sulla base della risposta, SockJS decide il trasporto da utilizzare. La prima scelta sono WebSocket. Se non sono supportati, viene utilizzato, se possibile, lo streaming. Se anche questa opzione non è possibile, come metodo di trasporto viene scelto il polling.
WebSocket in produzione?
Sebbene questa configurazione funzioni, non è la "migliore". Spring Boot ti consente di utilizzare qualsiasi sistema di messaggistica completo con il protocollo STOMP (ad es. ActiveMQ, RabbitMQ) e un broker esterno può supportare più operazioni STOMP (ad es. conferme, ricevute) rispetto al semplice broker che abbiamo utilizzato. STOMP Over WebSocket fornisce informazioni interessanti sui WebSocket e sul protocollo STOMP. Elenca i sistemi di messaggistica che gestiscono il protocollo STOMP e potrebbe essere una soluzione migliore da utilizzare in produzione. Soprattutto se, a causa dell'elevato numero di richieste, è necessario raggruppare il broker di messaggi. (Il broker di messaggi semplici di Spring non è adatto per il clustering.) Quindi, invece di abilitare il broker semplice in WebSocketConfig
, è necessario abilitare l'inoltro del broker Stomp che inoltra i messaggi da e verso un broker di messaggi esterno. Per riassumere, un broker di messaggi esterno può aiutarti a creare una soluzione più scalabile e robusta.
Se sei pronto per continuare il tuo viaggio per sviluppatori Java esplorando Spring Boot, prova a leggere Utilizzo di Spring Boot per OAuth2 e JWT REST Protection in seguito.