Utilizarea Spring Boot pentru implementarea WebSocket cu STOMP
Publicat: 2022-03-11Protocolul WebSocket este una dintre modalitățile de a face ca aplicația dvs. să gestioneze mesajele în timp real. Cele mai comune alternative sunt sondajele lungi și evenimentele trimise de server. Fiecare dintre aceste soluții are avantajele și dezavantajele sale. În acest articol, vă voi arăta cum să implementați WebSockets cu Spring Boot Framework. Voi acoperi atât configurarea serverului, cât și cea a clientului și vom folosi STOMP peste protocolul WebSocket pentru a comunica între ei.
Partea serverului va fi codificată exclusiv în Java. Dar, în cazul clientului, voi afișa fragmente scrise atât în Java, cât și în JavaScript (SockJS), deoarece, de obicei, clienții WebSockets sunt încorporați în aplicațiile front-end. Exemplele de cod vor demonstra cum să difuzați mesaje către mai mulți utilizatori folosind modelul pub-sub, precum și cum să trimiteți mesaje doar unui singur utilizator. Într-o altă parte a articolului, voi discuta pe scurt despre securizarea WebSockets și despre cum ne putem asigura că soluția noastră bazată pe WebSocket va rămâne operațională chiar și atunci când mediul nu acceptă protocolul WebSocket.
Vă rugăm să rețineți că subiectul securizării WebSockets va fi abordat doar pe scurt aici, deoarece este un subiect suficient de complex pentru un articol separat. Datorită acestui fapt și al altor câțiva factori pe care îi ating în WebSocket în producție? la sfârșit, vă recomand să faceți modificări înainte de a utiliza această configurație în producție , citiți până la sfârșit pentru o configurare gata de producție cu măsuri de securitate în vigoare.
Protocoale WebSocket și STOMP
Protocolul WebSocket vă permite să implementați comunicații bidirecționale între aplicații. Este important de știut că HTTP este folosit doar pentru strângerea de mână inițială. După ce se întâmplă, conexiunea HTTP este actualizată la o conexiune TCP/IP recent deschisă care este utilizată de un WebSocket.
Protocolul WebSocket este un protocol de nivel scăzut. Acesta definește modul în care un flux de octeți este transformat în cadre. Un cadru poate conține un text sau un mesaj binar. Deoarece mesajul în sine nu oferă informații suplimentare despre cum să-l rutați sau să îl procesați, este dificil să implementați aplicații mai complexe fără a scrie cod suplimentar. Din fericire, specificația WebSocket permite utilizarea sub-protocoalelor care operează la un nivel superior de aplicație. Unul dintre ele, susținut de Spring Framework, este STOMP.
STOMP este un protocol de mesagerie simplu, bazat pe text, care a fost creat inițial pentru limbaje de scripting precum Ruby, Python și Perl pentru a se conecta la brokerii de mesaje de întreprindere. Datorită STOMP, clienții și brokerii dezvoltați în diferite limbi pot trimite și primi mesaje unul către și de la celălalt. Protocolul WebSocket este uneori numit TCP pentru Web. În mod analog, STOMP se numește HTTP pentru Web. Acesta definește o mână de tipuri de cadre care sunt mapate pe cadre WebSockets, de exemplu, CONNECT , SUBSCRIBE , UNSUBSCRIBE , ACK sau SEND . Pe de o parte, aceste comenzi sunt foarte utile pentru a gestiona comunicarea, în timp ce, pe de altă parte, ne permit să implementăm soluții cu caracteristici mai sofisticate, cum ar fi recunoașterea mesajelor.
Partea serverului: Spring Boot și WebSockets
Pentru a construi partea de server WebSocket, vom folosi cadrul Spring Boot, care accelerează semnificativ dezvoltarea aplicațiilor web și independente în Java. Spring Boot include modulul spring-WebSocket , care este compatibil cu standardul Java WebSocket API (JSR-356).
Implementarea serverului WebSocket cu Spring Boot nu este o sarcină foarte complexă și include doar câțiva pași, pe care îi vom parcurge unul câte unul.
Pasul 1. Mai întâi, trebuie să adăugăm dependența de bibliotecă WebSocket.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>Dacă intenționați să utilizați formatul JSON pentru mesajele transmise, poate doriți să includeți și dependența GSON sau Jackson. Foarte probabil, este posibil să aveți nevoie în plus de un cadru de securitate, de exemplu, Spring Security.
Pasul 2. Apoi, putem configura Spring pentru a activa mesajele WebSocket și 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"); } } Metoda configureMessageBroker face două lucruri:
- Creează brokerul de mesaje în memorie cu una sau mai multe destinații pentru trimiterea și primirea mesajelor. În exemplul de mai sus, sunt definite două prefixe de destinație:
topicșiqueue. Ei urmează convenția conform căreia destinațiile pentru mesajele care urmează să fie transmise către toți clienții abonați prin modelul pub-sub ar trebui să fie prefixate cutopic. Pe de altă parte, destinațiile pentru mesajele private sunt de obicei prefixate dequeue. - Definește
appde prefix care este utilizată pentru a filtra destinațiile gestionate prin metode adnotate cu@MessageMappingpe care le vei implementa într-un controler. Controlorul, după procesarea mesajului, îl va trimite brokerului.
Revenind la fragmentul de mai sus — probabil ați observat un apel la metoda cu withSockJS() — acesta activează opțiunile de rezervă SockJS. Pentru a menține lucrurile pe scurt, va permite WebSocket-urile noastre să funcționeze chiar dacă protocolul WebSocket nu este acceptat de un browser de internet. Voi discuta acest subiect mai detaliat puțin mai departe.
Mai este un lucru care trebuie clarificat - de ce numim metoda setAllowedOrigins() la punctul final. Este adesea necesar deoarece comportamentul implicit al WebSocket și SockJS este de a accepta numai cereri de aceeași origine. Deci, dacă clientul dvs. și partea serverului folosesc domenii diferite, această metodă trebuie apelată pentru a permite comunicarea între ele.
Pasul 3 . Implementați un controler care va gestiona solicitările utilizatorilor. Acesta va difuza mesajul primit tuturor utilizatorilor abonați la un anumit subiect.
Iată un exemplu de metodă care trimite mesaje către destinația /topic/news .
@MessageMapping("/news") @SendTo("/topic/news") public void broadcastNews(@Payload String message) { return message; } În loc de adnotarea @SendTo , puteți utiliza și SimpMessagingTemplate pe care îl puteți conecta automat în controlerul dumneavoastră.
@MessageMapping("/news") public void broadcastNews(@Payload String message) { this.simpMessagingTemplate.convertAndSend("/topic/news", message) } În pașii următori, este posibil să doriți să adăugați câteva clase suplimentare pentru a vă securiza punctele finale, cum ar fi ResourceServerConfigurerAdapter sau WebSecurityConfigurerAdapter din cadrul Spring Security. De asemenea, este adesea benefic să implementați modelul de mesaj, astfel încât JSON transmis să poată fi mapat la obiecte.

Construirea clientului WebSocket
Implementarea unui client este o sarcină și mai simplă.
Pasul 1. Client Autowire Spring STOMP.
@Autowired private WebSocketStompClient stompClient;Pasul 2. Deschideți o conexiune.
StompSessionHandler sessionHandler = new CustmStompSessionHandler(); StompSession stompSession = stompClient.connect(loggerServerQueueUrl, sessionHandler).get();Odată făcut acest lucru, este posibil să trimiteți un mesaj către o destinație. Mesajul va fi trimis tuturor utilizatorilor abonați la un subiect.
stompSession.send("topic/greetings", "Hello new user");De asemenea, este posibil să vă abonați pentru mesaje.
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()); } Uneori este necesar să trimiteți un mesaj doar unui utilizator dedicat (de exemplu, atunci când implementați un chat). Apoi, clientul și partea serverului trebuie să utilizeze o destinație separată dedicată acestei conversații private. Numele destinației poate fi creat prin adăugarea unui identificator unic la un nume de destinație general, de exemplu, /queue/chat-user123 . Identificatorii de sesiune HTTP sau STOMP pot fi utilizați în acest scop.
Spring face trimiterea mesajelor private mult mai ușoară. Trebuie doar să adnotăm metoda unui Controller cu @SendToUser . Apoi, această destinație va fi gestionată de UserDestinationMessageHandler , care se bazează pe un identificator de sesiune. Pe partea clientului, atunci când un client se abonează la o destinație prefixată cu /user , această destinație este transformată într-o destinație unică pentru acest utilizator. Pe partea de server, o destinație de utilizator este rezolvată pe baza Principal utilizatorului.
Exemplu de cod pe server cu adnotare @SendToUser :
@MessageMapping("/greetings") @SendToUser("/queue/greetings") public String reply(@Payload String message, Principal user) { return "Hello " + message; } Sau puteți folosi SimpMessagingTemplate :
String username = ... this.simpMessagingTemplate.convertAndSendToUser(); username, "/queue/greetings", "Hello " + username);Să ne uităm acum la cum să implementăm un client JavaScript (SockJS) capabil să primească mesaje private care ar putea fi trimise de codul Java din exemplul de mai sus. Merită să știți că WebSockets fac parte din specificația HTML5 și sunt acceptate de majoritatea browserelor moderne (Internet Explorer le acceptă începând cu versiunea 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()); } După cum probabil ați observat, pentru a primi mesaje private, clientul trebuie să se aboneze la o destinație generală /queue/greetings prefixată cu /user . Nu trebuie să se deranjeze cu niciun identificator unic. Cu toate acestea, clientul trebuie să se conecteze la aplicație înainte, astfel încât obiectul Principal de pe partea serverului este inițializat.
Securizarea WebSockets
Multe aplicații web folosesc autentificarea bazată pe cookie-uri. De exemplu, putem folosi Spring Security pentru a restricționa accesul la anumite pagini sau Controlere numai pentru utilizatorii conectați. Contextul de securitate al utilizatorului este apoi menținut prin sesiunea HTTP bazată pe cookie-uri care este asociată ulterior cu sesiunile WebSocket sau SockJS create pentru utilizatorul respectiv. Punctele finale WebSockets pot fi securizate ca orice alte solicitări, de exemplu, în WebSecurityConfigurerAdapter de la Spring.
În zilele noastre, aplicațiile web folosesc adesea API-urile REST ca back-end și token-uri OAuth/JWT pentru autentificarea și autorizarea utilizatorilor. Protocolul WebSocket nu descrie modul în care serverele ar trebui să autentifice clienții în timpul strângerii de mână HTTP. În practică, anteturile standard HTTP (de exemplu, Autorizare) sunt utilizate în acest scop. Din păcate, nu este acceptat de toți clienții STOMP. Clientul STOMP al Spring Java permite setarea antetelor pentru strângere de mână:
WebSocketHttpHeaders handshakeHeaders = new WebSocketHttpHeaders(); handshakeHeaders.add(principalRequestHeader, principalRequestValue);Dar clientul JavaScript SockJS nu acceptă trimiterea antetului de autorizare cu o solicitare SockJS. Cu toate acestea, permite trimiterea parametrilor de interogare care pot fi utilizați pentru a transmite un token. Această abordare necesită scrierea codului personalizat în partea de server care va citi simbolul din parametrii de interogare și îl va valida. De asemenea, este important să vă asigurați că token-urile nu sunt înregistrate împreună cu cererile (sau jurnalele sunt bine protejate), deoarece acest lucru ar putea introduce o încălcare gravă a securității.
Opțiuni de rezervă SockJS
Integrarea cu WebSocket poate să nu se desfășoare întotdeauna fără probleme. Unele browsere (de exemplu, IE 9) nu acceptă WebSockets. În plus, proxy-urile restrictive pot face imposibilă efectuarea upgrade-ului HTTP sau întrerup conexiunile care sunt deschise prea mult timp. În astfel de cazuri, SockJS vine în ajutor.
Transporturile SockJS se încadrează în trei categorii generale: WebSockets, HTTP Streaming și HTTP Long Polling. Comunicarea începe cu SockJS care trimite GET /info pentru a obține informații de bază de la server. Pe baza răspunsului, SockJS decide asupra transportului care va fi utilizat. Prima alegere sunt WebSockets. Dacă nu sunt acceptate, atunci, dacă este posibil, se utilizează Streaming. Dacă nici această opțiune nu este posibilă, atunci Polling este ales ca metodă de transport.
WebSocket în producție?
În timp ce această configurație funcționează, nu este „cea mai bună”. Spring Boot vă permite să utilizați orice sistem de mesagerie cu drepturi depline cu protocolul STOMP (de exemplu, ActiveMQ, RabbitMQ), iar un broker extern poate accepta mai multe operațiuni STOMP (de exemplu, confirmări, chitanțe) decât brokerul simplu pe care l-am folosit. STOMP Over WebSocket oferă informații interesante despre WebSockets și protocolul STOMP. Enumeră sistemele de mesagerie care se ocupă de protocolul STOMP și ar putea fi o soluție mai bună de utilizat în producție. Mai ales dacă, din cauza numărului mare de solicitări, brokerul de mesaje trebuie grupat. (Brokerul simplu de mesaje Spring nu este potrivit pentru grupare.) Apoi, în loc să activeze brokerul simplu în WebSocketConfig , este necesar să se activeze releul broker Stomp care redirecționează mesajele către și de la un broker extern de mesaje. În concluzie, un broker extern de mesaje vă poate ajuta să construiți o soluție mai scalabilă și mai robustă.
Dacă sunteți gata să continuați călătoria dezvoltatorului Java prin explorarea Spring Boot, încercați să citiți în continuare Utilizarea Spring Boot pentru OAuth2 și JWT REST Protection .
