使用 Spring Boot 通過 STOMP 實現 WebSocket
已發表: 2022-03-11WebSocket 協議是讓您的應用程序處理實時消息的方法之一。 最常見的替代方案是長輪詢和服務器發送事件。 這些解決方案中的每一個都有其優點和缺點。 在本文中,我將向您展示如何使用 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 幀的幀類型,例如CONNECT
、 SUBSCRIBE
、 UNSUBSCRIBE
、 ACK
或SEND
。 一方面,這些命令對於管理通信非常方便,另一方面,它們允許我們實現具有更複雜功能的解決方案,例如消息確認。
服務器端: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
方法做了兩件事:
- 創建具有一個或多個目標的內存消息代理,用於發送和接收消息。 在上面的示例中,定義了兩個目標前綴:
topic
和queue
。 它們遵循這樣的約定,即通過 pub-sub 模型向所有訂閱客戶端發送消息的目的地應以topic
為前綴。 另一方面,私人消息的目的地通常以queue
為前綴。 - 定義前綴
app
用於過濾由使用@MessageMapping
註釋的方法處理的目標,您將在控制器中實現這些目標。 控制器處理完消息後,會將其發送給代理。
回到上面的代碼片段——可能你已經註意到了對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 框架中的ResourceServerConfigurerAdapter
或WebSecurityConfigurerAdapter
。 此外,實現消息模型通常是有益的,以便可以將傳輸的 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 保護接下來。