使用 Spring Boot 通過 STOMP 實現 WebSocket

已發表: 2022-03-11

WebSocket 協議是讓您的應用程序處理實時消息的方法之一。 最常見的替代方案是長輪詢和服務器發送事件。 這些解決方案中的每一個都有其優點和缺點。 在本文中,我將向您展示如何使用 Spring Boot 框架實現 WebSockets。 我將介紹服務器端和客戶端設置,我們將使用 STOMP over WebSocket 協議相互通信。

服務器端將完全用 Java 編碼。 但是,在客戶端的情況下,我將展示用 Java 和 JavaScript (SockJS) 編寫的代碼片段,因為 WebSockets 客戶端通常嵌入在前端應用程序中。 代碼示例將演示如何使用 pub-sub 模型向多個用戶廣播消息,以及如何僅向單個用戶發送消息。 在本文的另一部分,我將簡要討論保護 WebSocket 的安全,以及我們如何確保我們的基於 WebSocket 的解決方案即使在環境不支持 WebSocket 協議的情況下也能保持運行。

請注意,保護 WebSockets 的主題在這裡只會簡單地涉及,因為它是一個足夠複雜的主題,可以單獨寫一篇文章。 因此,以及我在生產中的 WebSocket 中涉及的其他幾個因素? 最後的部分,我建議在生產中使用此設置之前進行修改,請閱讀直到最後,以獲取具有安全措施的生產就緒設置。

WebSocket 和 STOMP 協議

WebSocket 協議允許您在應用程序之間實現雙向通信。 重要的是要知道 HTTP 僅用於初始握手。 發生這種情況後,HTTP 連接將升級為 WebSocket 使用的新打開的 TCP/IP 連接。

WebSocket 協議是一個相當低級的協議。 它定義瞭如何將字節流轉換為幀。 框架可能包含文本或二進制消息。 因為消息本身並沒有提供任何關於如何路由或處理它的額外信息,所以如果不編寫額外的代碼就很難實現更複雜的應用程序。 幸運的是,WebSocket 規範允許使用在更高的應用程序級別上運行的子協議。 Spring Framework 支持的其中之一是 STOMP。

STOMP 是一個簡單的基於文本的消息傳遞協議,最初是為 Ruby、Python 和 Perl 等腳本語言創建的,用於連接到企業消息代理。 多虧了 STOMP,以不同語言開發的客戶端和代理可以相互發送和接收消息。 WebSocket 協議有時稱為 Web 的 TCP。 類似地,STOMP 被稱為 Web 的 HTTP。 它定義了一些映射到 WebSockets 幀的幀類型,例如CONNECTSUBSCRIBEUNSUBSCRIBEACKSEND 。 一方面,這些命令對於管理通信非常方便,另一方面,它們允許我們實現具有更複雜功能的解決方案,例如消息確認。

服務器端:Spring Boot 和 WebSockets

為了構建 WebSocket 服務器端,我們將使用 Spring Boot 框架,它顯著加快了 Java 中獨立和 Web 應用程序的開發速度。 Spring Boot 包含spring-WebSocket模塊,該模塊與 Java WebSocket API 標準(JSR-356)兼容。

使用 Spring Boot 實現 WebSocket 服務器端並不是一項非常複雜的任務,並且只包含幾個步驟,我們將一一進行。

步驟 1.首先,我們需要添加 WebSocket 庫依賴。

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

如果您計劃使用 JSON 格式傳輸消息,您可能還需要包含 GSON 或 Jackson 依賴項。 很可能,您可能還需要一個安全框架,例如 Spring Security。

第 2 步。然後,我們可以配置 Spring 以啟用 WebSocket 和 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"); } }

configureMessageBroker方法做了兩件事:

  1. 創建具有一個或多個目標的內存消息代理,用於發送和接收消息。 在上面的示例中,定義了兩個目標前綴: topicqueue 。 它們遵循這樣的約定,即通過 pub-sub 模型向所有訂閱客戶端發送消息的目的地應以topic為前綴。 另一方面,私人消息的目的地通常以queue為前綴。
  2. 定義前綴app用於過濾由使用@MessageMapping註釋的方法處理的目標,您將在控制器中實現這些目標。 控制器處理完消息後,會將其發送給代理。

Spring Boot WebSocket:如何在服務器端處理消息

如何在服務器端處理消息(來源:Spring 文檔)


回到上面的代碼片段——可能你已經註意到了對withSockJS()方法的調用——它啟用了 SockJS 回退選項。 簡而言之,即使 Internet 瀏覽器不支持 WebSocket 協議,它也會讓我們的 WebSocket 工作。 我將更詳細地討論這個主題。

還有一件事需要澄清——為什麼我們在端點上調用setAllowedOrigins()方法。 它通常是必需的,因為 WebSocket 和 SockJS 的默認行為是只接受同源請求。 因此,如果您的客戶端和服務器端使用不同的域,則需要調用此方法以允許它們之間的通信。

步驟 3 。 實現一個控制器來處理用戶請求。 它將向訂閱給定主題的所有用戶廣播收到的消息。

這是一個將消息發送到目標/topic/news的示例方法。

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

除了註釋@SendTo ,您還可以使用SimpMessagingTemplate ,您可以在控制器內部自動裝配。

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

在後面的步驟中,您可能希望添加一些額外的類來保護您的端點,例如 Spring Security 框架中的ResourceServerConfigurerAdapterWebSecurityConfigurerAdapter 。 此外,實現消息模型通常是有益的,以便可以將傳輸的 JSON 映射到對象。

構建 WebSocket 客戶端

實現客戶端是一項更簡單的任務。

步驟 1. 自動裝配 Spring STOMP 客戶端。

 @Autowired private WebSocketStompClient stompClient;

步驟 2. 打開一個連接。

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

一旦完成,就可以向目的地發送消息。 該消息將發送給訂閱了某個主題的所有用戶。

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

也可以訂閱消息。

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

有時只需要向專用用戶發送消息(例如在實現聊天時)。 然後,客戶端和服務器端必須使用專用於此私人對話的單獨目的地。 可以通過將唯一標識符附加到通用目的地名稱(例如, /queue/chat-user123 )來創建目的地的名稱。 HTTP 會話或 STOMP 會話標識符可用於此目的。

Spring 使發送私人消息變得更加容易。 我們只需要用@SendToUser註釋控制器的方法。 然後,這個目的地將由UserDestinationMessageHandler處理,它依賴於會話標識符。 在客戶端,當客戶端訂閱以/user為前綴的目的地時,該目的地將轉換為該用戶唯一的目的地。 在服務器端,根據用戶的Principal解析用戶目的地。

帶有@SendToUser註釋的示例服務器端代碼:

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

或者您可以使用SimpMessagingTemplate

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

現在讓我們看看如何實現一個 JavaScript (SockJS) 客戶端,該客戶端能夠接收可以由上面示例中的 Java 代碼發送的私人消息。 值得知道的是,WebSockets 是 HTML5 規範的一部分,並且受到大多數現代瀏覽器的支持(Internet Explorer 從版本 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()); }

您可能已經註意到,要接收私人消息,客戶端需要訂閱以/user為前綴的通用目的地/queue/greetings 。 它不必為任何唯一標識符而煩惱。 但是客戶端需要先登錄到應用程序,所以初始化了服務端的Principal對象。

保護 WebSockets

許多 Web 應用程序使用基於 cookie 的身份驗證。 例如,我們可以使用 Spring Security 來限制對某些頁面或控制器的訪問僅限於登錄用戶。 然後通過基於 cookie 的 HTTP 會話維護用戶安全上下文,該會話稍後與為該用戶創建的 WebSocket 或 SockJS 會話相關聯。 WebSockets 端點可以像任何其他請求一樣受到保護,例如在 Spring 的WebSecurityConfigurerAdapter中。

如今,Web 應用程序通常使用 REST API 作為其後端,並使用 OAuth/JWT 令牌進行用戶身份驗證和授權。 WebSocket 協議沒有描述服務器在 HTTP 握手期間如何驗證客戶端。 在實踐中,標準 HTTP 標頭(例如,授權)用於此目的。 不幸的是,並非所有 STOMP 客戶端都支持它。 Spring Java 的 STOMP 客戶端允許為握手設置標頭:

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

但是 SockJS JavaScript 客戶端不支持通過 SockJS 請求發送授權標頭。 但是,它允許發送可用於傳遞令牌的查詢參數。 這種方法需要在服務器端編寫自定義代碼,該代碼將從查詢參數中讀取令牌並對其進行驗證。 確保令牌不與請求一起記錄(或日誌受到良好保護)也很重要,因為這可能會導致嚴重的安全違規。

SockJS 後備選項

與 WebSocket 的集成可能並不總是順利進行。 一些瀏覽器(例如​​,IE 9)不支持 WebSockets。 更重要的是,限制性代理可能會導致無法執行 HTTP 升級,或者它們會切斷打開時間過長的連接。 在這種情況下,SockJS 來救援。

SockJS 傳輸分為三大類:WebSockets、HTTP 流和 HTTP 長輪詢。 通信從 SockJS 發送GET /info以從服務器獲取基本信息開始。 根據響應,SockJS 決定要使用的傳輸。 第一個選擇是 WebSockets。 如果它們不受支持,則盡可能使用流式傳輸。 如果此選項也不可能,則選擇輪詢作為傳輸方法。

生產中的 WebSocket?

儘管此設置有效,但它並不是“最好的”。 Spring Boot 允許您使用任何具有 STOMP 協議的成熟消息系統(例如,ActiveMQ、RabbitMQ),並且外部代理可能支持比我們使用的簡單代理更多的 STOMP 操作(例如,確認、收據)。 STOMP Over WebSocket提供了有關 WebSockets 和 STOMP 協議的有趣信息。 它列出了處理 STOMP 協議的消息傳遞系統,並且可能是在生產中使用的更好的解決方案。 特別是如果由於大量請求,消息代理需要集群。 (Spring 的簡單消息代理不適合集群。)然後,不需要在WebSocketConfig中啟用簡單代理,而是需要啟用 Stomp 代理中繼,該中繼將消息轉發到外部消息代理並從外部消息代理轉發。 總而言之,外部消息代理可以幫助您構建更具可擴展性和健壯性的解決方案。

如果您已準備好繼續探索 Spring Boot 的 Java 開發人員之旅,請嘗試閱讀使用 Spring Boot 進行 OAuth2 和 JWT REST 保護接下來。