Verwenden von Spring Boot für die WebSocket-Implementierung mit STOMP

Veröffentlicht: 2022-03-11

Das WebSocket-Protokoll ist eine der Möglichkeiten, mit der Ihre Anwendung Echtzeitnachrichten verarbeiten kann. Die gebräuchlichsten Alternativen sind lange Abfragen und vom Server gesendete Ereignisse. Jede dieser Lösungen hat ihre Vor- und Nachteile. In diesem Artikel zeige ich Ihnen, wie Sie WebSockets mit dem Spring Boot Framework implementieren. Ich werde sowohl die serverseitige als auch die clientseitige Einrichtung behandeln, und wir werden das STOMP-über-WebSocket-Protokoll verwenden, um miteinander zu kommunizieren.

Die Serverseite wird rein in Java codiert. Aber im Fall des Clients zeige ich Snippets, die sowohl in Java als auch in JavaScript (SockJS) geschrieben sind, da WebSockets-Clients normalerweise in Front-End-Anwendungen eingebettet sind. Die Codebeispiele zeigen, wie Nachrichten mithilfe des Pub-Sub-Modells an mehrere Benutzer gesendet werden und wie Nachrichten nur an einen einzelnen Benutzer gesendet werden. In einem weiteren Teil des Artikels werde ich kurz auf die Sicherung von WebSockets eingehen und darauf eingehen, wie wir sicherstellen können, dass unsere WebSocket-basierte Lösung auch dann funktionsfähig bleibt, wenn die Umgebung das WebSocket-Protokoll nicht unterstützt.

Bitte beachten Sie, dass das Thema Absicherung von WebSockets hier nur kurz angerissen wird, da es ein Thema ist, das komplex genug für einen eigenen Artikel ist. Aufgrund dessen und einiger anderer Faktoren, die ich im WebSocket in der Produktion anspreche? Abschnitt am Ende empfehle ich, Änderungen vorzunehmen, bevor Sie dieses Setup in der Produktion verwenden , lesen Sie bis zum Ende für ein produktionsbereites Setup mit vorhandenen Sicherheitsmaßnahmen.

WebSocket- und STOMP-Protokolle

Mit dem WebSocket-Protokoll können Sie eine bidirektionale Kommunikation zwischen Anwendungen implementieren. Es ist wichtig zu wissen, dass HTTP nur für den anfänglichen Handshake verwendet wird. Danach wird die HTTP-Verbindung auf eine neu geöffnete TCP/IP-Verbindung aktualisiert, die von einem WebSocket verwendet wird.

Das WebSocket-Protokoll ist ein eher Low-Level-Protokoll. Es definiert, wie ein Strom von Bytes in Frames umgewandelt wird. Ein Rahmen kann einen Text oder eine binäre Nachricht enthalten. Da die Nachricht selbst keine zusätzlichen Informationen zum Weiterleiten oder Verarbeiten enthält, ist es schwierig, komplexere Anwendungen zu implementieren, ohne zusätzlichen Code zu schreiben. Glücklicherweise erlaubt die WebSocket-Spezifikation die Verwendung von Unterprotokollen, die auf einer höheren Anwendungsebene arbeiten. Eines davon, das vom Spring Framework unterstützt wird, ist STOMP.

STOMP ist ein einfaches textbasiertes Messaging-Protokoll, das ursprünglich für Skriptsprachen wie Ruby, Python und Perl entwickelt wurde, um eine Verbindung zu Enterprise Message Brokern herzustellen. Dank STOMP können Kunden und Broker, die in verschiedenen Sprachen entwickelt wurden, Nachrichten untereinander senden und empfangen. Das WebSocket-Protokoll wird manchmal als TCP für Web bezeichnet. Analog heißt STOMP HTTP for Web. Es definiert eine Handvoll Frame-Typen, die auf WebSockets-Frames abgebildet werden, z. B. CONNECT , SUBSCRIBE , UNSUBSCRIBE , ACK oder SEND . Einerseits sind diese Befehle sehr praktisch, um die Kommunikation zu verwalten, andererseits ermöglichen sie uns, Lösungen mit anspruchsvolleren Funktionen wie der Nachrichtenbestätigung zu implementieren.

Die Serverseite: Spring Boot und WebSockets

Um die WebSocket-Serverseite zu erstellen, verwenden wir das Spring Boot-Framework, das die Entwicklung von Standalone- und Webanwendungen in Java erheblich beschleunigt. Spring Boot enthält das spring-WebSocket Modul, das mit dem Java-WebSocket-API-Standard (JSR-356) kompatibel ist.

Die serverseitige Implementierung von WebSocket mit Spring Boot ist keine sehr komplexe Aufgabe und umfasst nur ein paar Schritte, die wir nacheinander durchgehen werden.

Schritt 1. Zuerst müssen wir die Abhängigkeit der WebSocket-Bibliothek hinzufügen.

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

Wenn Sie das JSON-Format für übertragene Nachrichten verwenden möchten, möchten Sie möglicherweise auch die GSON- oder Jackson-Abhängigkeit einbeziehen. Sehr wahrscheinlich benötigen Sie zusätzlich ein Sicherheitsframework, zum Beispiel Spring Security.

Schritt 2. Dann können wir Spring konfigurieren, um WebSocket- und STOMP-Messaging zu aktivieren.

 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"); } }

Die Methode configureMessageBroker macht zwei Dinge:

  1. Erstellt den speicherinternen Nachrichtenbroker mit einem oder mehreren Zielen zum Senden und Empfangen von Nachrichten. Im obigen Beispiel sind zwei Zielpräfixe definiert: topic und queue . Sie folgen der Konvention, dass Zielen für Nachrichten, die über das Pub-Sub-Modell an alle abonnierten Clients weitergeleitet werden sollen, das topic vorangestellt werden sollte. Andererseits wird den Zielen für private Nachrichten typischerweise queue vorangestellt.
  2. Definiert die Präfix- app , die zum Filtern von Zielen verwendet wird, die von Methoden behandelt werden, die mit @MessageMapping kommentiert sind und die Sie in einem Controller implementieren. Der Controller sendet die Nachricht nach Verarbeitung an den Broker.

Spring Boot WebSocket: Wie Nachrichten serverseitig behandelt werden

Wie Nachrichten serverseitig behandelt werden (Quelle: Spring-Dokumentation)


Zurück zum obigen Snippet – wahrscheinlich haben Sie einen Aufruf der Methode withSockJS() bemerkt – es aktiviert SockJS-Fallback-Optionen. Um es kurz zu machen, es lässt unsere WebSockets auch dann funktionieren, wenn das WebSocket-Protokoll von einem Internetbrowser nicht unterstützt wird. Auf dieses Thema werde ich noch etwas ausführlicher eingehen.

Es gibt noch eine Sache, die geklärt werden muss – warum wir die Methode setAllowedOrigins() auf dem Endpunkt aufrufen. Dies ist häufig erforderlich, da das Standardverhalten von WebSocket und SockJS darin besteht, nur Anforderungen gleichen Ursprungs zu akzeptieren. Wenn also Ihr Client und die Serverseite unterschiedliche Domänen verwenden, muss diese Methode aufgerufen werden, um die Kommunikation zwischen ihnen zu ermöglichen.

Schritt 3 . Implementieren Sie einen Controller, der Benutzeranforderungen verarbeitet. Es sendet eine empfangene Nachricht an alle Benutzer, die ein bestimmtes Thema abonniert haben.

Hier ist eine Beispielmethode, die Nachrichten an das Ziel /topic/news sendet.

 @MessageMapping("/news") @SendTo("/topic/news") public void broadcastNews(@Payload String message) { return message; }

Anstelle der Annotation @SendTo können Sie auch SimpMessagingTemplate verwenden, das Sie in Ihrem Controller automatisch verdrahten können.

 @MessageMapping("/news") public void broadcastNews(@Payload String message) { this.simpMessagingTemplate.convertAndSend("/topic/news", message) }

In späteren Schritten möchten Sie möglicherweise einige zusätzliche Klassen hinzufügen, um Ihre Endpunkte zu sichern, z. B. ResourceServerConfigurerAdapter oder WebSecurityConfigurerAdapter aus dem Spring Security-Framework. Außerdem ist es oft von Vorteil, das Nachrichtenmodell so zu implementieren, dass übertragenes JSON Objekten zugeordnet werden kann.

Erstellen des WebSocket-Clients

Die Implementierung eines Clients ist eine noch einfachere Aufgabe.

Schritt 1. Autowire Spring STOMP-Client.

 @Autowired private WebSocketStompClient stompClient;

Schritt 2. Öffnen Sie eine Verbindung.

 StompSessionHandler sessionHandler = new CustmStompSessionHandler(); StompSession stompSession = stompClient.connect(loggerServerQueueUrl, sessionHandler).get();

Sobald dies geschehen ist, ist es möglich, eine Nachricht an ein Ziel zu senden. Die Nachricht wird an alle Benutzer gesendet, die ein Thema abonniert haben.

 stompSession.send("topic/greetings", "Hello new user");

Es ist auch möglich, Nachrichten zu abonnieren.

 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()); }

Manchmal ist es erforderlich, eine Nachricht nur an einen dedizierten Benutzer zu senden (z. B. bei der Implementierung eines Chats). Dann müssen der Client und die Serverseite ein separates Ziel verwenden, das dieser privaten Konversation gewidmet ist. Der Name des Ziels kann erstellt werden, indem eine eindeutige Kennung an einen allgemeinen Zielnamen angehängt wird, z. B. /queue/chat-user123 . Zu diesem Zweck können HTTP-Sitzungs- oder STOMP-Sitzungskennungen verwendet werden.

Spring macht das Versenden privater Nachrichten viel einfacher. Wir müssen nur die Methode eines Controllers mit @SendToUser . Anschließend wird dieses Ziel von UserDestinationMessageHandler , das auf einer Sitzungskennung beruht. Wenn ein Client auf der Clientseite ein Ziel mit dem Präfix /user abonniert, wird dieses Ziel in ein für diesen Benutzer eindeutiges Ziel umgewandelt. Auf der Serverseite wird ein Benutzerziel basierend auf dem Principal eines Benutzers aufgelöst.

Serverseitiger Beispielcode mit der Annotation @SendToUser :

 @MessageMapping("/greetings") @SendToUser("/queue/greetings") public String reply(@Payload String message, Principal user) { return "Hello " + message; }

Oder Sie können SimpMessagingTemplate verwenden :

 String username = ... this.simpMessagingTemplate.convertAndSendToUser(); username, "/queue/greetings", "Hello " + username);

Sehen wir uns nun an, wie ein JavaScript-Client (SockJS) implementiert wird, der in der Lage ist, private Nachrichten zu empfangen, die vom Java-Code im obigen Beispiel gesendet werden könnten. Es ist wichtig zu wissen, dass WebSockets Teil der HTML5-Spezifikation sind und von den meisten modernen Browsern unterstützt werden (Internet Explorer unterstützt sie seit Version 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()); }

Wie Sie wahrscheinlich bemerkt haben, muss der Client zum Empfangen privater Nachrichten ein allgemeines Ziel /queue/greetings mit dem Präfix /user abonnieren. Es muss sich nicht um eindeutige Kennungen kümmern. Der Client muss sich jedoch zuvor bei der Anwendung anmelden, damit das Principal -Objekt auf der Serverseite initialisiert wird.

Sichern von WebSockets

Viele Webanwendungen verwenden eine Cookie-basierte Authentifizierung. Beispielsweise können wir Spring Security verwenden, um den Zugriff auf bestimmte Seiten oder Controller nur auf angemeldete Benutzer zu beschränken. Der Sicherheitskontext des Benutzers wird dann über eine cookiebasierte HTTP-Sitzung verwaltet, die später mit WebSocket- oder SockJS-Sitzungen verknüpft wird, die für diesen Benutzer erstellt wurden. WebSockets-Endpunkte können wie alle anderen Anfragen gesichert werden, z. B. in WebSecurityConfigurerAdapter .

Heutzutage verwenden Webanwendungen häufig REST-APIs als Backend und OAuth/JWT-Token für die Benutzerauthentifizierung und -autorisierung. Das WebSocket-Protokoll beschreibt nicht, wie Server Clients während des HTTP-Handshakes authentifizieren sollen. In der Praxis werden hierfür Standard-HTTP-Header (z. B. Authorization) verwendet. Leider wird es nicht von allen STOMP-Clients unterstützt. Der STOMP-Client von Spring Java ermöglicht das Setzen von Headern für den Handshake:

 WebSocketHttpHeaders handshakeHeaders = new WebSocketHttpHeaders(); handshakeHeaders.add(principalRequestHeader, principalRequestValue);

Der SockJS-JavaScript-Client unterstützt jedoch nicht das Senden von Autorisierungsheadern mit einer SockJS-Anforderung. Es ermöglicht jedoch das Senden von Abfrageparametern, die zum Übergeben eines Tokens verwendet werden können. Dieser Ansatz erfordert das Schreiben von benutzerdefiniertem Code auf der Serverseite, der das Token aus den Abfrageparametern liest und validiert. Es ist auch wichtig sicherzustellen, dass Token nicht zusammen mit Anforderungen protokolliert werden (oder Protokolle gut geschützt sind), da dies zu einer ernsthaften Sicherheitsverletzung führen könnte.

SockJS-Fallback-Optionen

Die Integration mit WebSocket verläuft möglicherweise nicht immer reibungslos. Einige Browser (z. B. IE 9) unterstützen WebSockets nicht. Darüber hinaus können restriktive Proxys die Durchführung des HTTP-Upgrades unmöglich machen oder zu lange geöffnete Verbindungen unterbrechen. In solchen Fällen kommt SockJS zur Rettung.

SockJS-Transporte fallen in drei allgemeine Kategorien: WebSockets, HTTP-Streaming und HTTP-Long-Polling. Die Kommunikation beginnt damit, dass SockJS GET /info sendet, um grundlegende Informationen vom Server zu erhalten. Basierend auf der Antwort entscheidet SockJS über den zu verwendenden Transport. Die erste Wahl sind WebSockets. Wenn sie nicht unterstützt werden, wird nach Möglichkeit Streaming verwendet. Ist auch diese Option nicht möglich, wird Polling als Transportmethode gewählt.

WebSocket in Produktion?

Obwohl dieses Setup funktioniert, ist es nicht das „Beste“. Mit Spring Boot können Sie jedes vollwertige Messaging-System mit dem STOMP-Protokoll (z. B. ActiveMQ, RabbitMQ) verwenden, und ein externer Broker unterstützt möglicherweise mehr STOMP-Operationen (z. B. Bestätigungen, Quittungen) als der von uns verwendete einfache Broker. STOMP Over WebSocket bietet interessante Informationen über WebSockets und das STOMP-Protokoll. Es listet Messaging-Systeme auf, die das STOMP-Protokoll verarbeiten und eine bessere Lösung für den Einsatz in der Produktion sein könnten. Vor allem, wenn aufgrund der hohen Anzahl von Anfragen der Message Broker geclustert werden muss. (Der einfache Nachrichtenbroker von Spring ist nicht für Clustering geeignet.) Anstatt den einfachen Broker in WebSocketConfig zu aktivieren, muss dann das Stomp-Broker-Relay aktiviert werden, das Nachrichten an und von einem externen Nachrichtenbroker weiterleitet. Zusammenfassend lässt sich sagen, dass ein externer Nachrichtenbroker Ihnen beim Aufbau einer skalierbareren und robusteren Lösung helfen kann.

Wenn Sie bereit sind, Ihre Reise als Java-Entwickler mit der Erkundung von Spring Boot fortzusetzen, lesen Sie als Nächstes Verwenden von Spring Boot für OAuth2 und JWT REST Protection .