Spring Boot實現STOMP協議的WebSocket

WebSocket協議是應用程序處理實時消息的方法之一。最多見的替代方案是長輪詢(long polling)和服務器推送事件(server-sent events)。這些解決方案中的每一個都有其優缺點。在本文中,我將向您展現如何使用Spring Boot實現WebSocket。我將介紹服務器端和客戶端設置,使用WebSocket協議之上的STOMP進行相互通訊。前端

服務器端將徹底用Java編碼。可是,就客戶端而言,我將展現用JavaJavaScript(SockJS)編寫的片斷,由於一般,WebSocket客戶端嵌入在前端應用程序中。代碼示例將演示如何使用pub-sub模型向多個用戶廣播消息以及如何僅向單個用戶發送消息。在本文的另外一部分中,我將簡要討論WebSocket安全問題以及如何確保即便環境不支持WebSocket協議,基於WebSocket的解決方案也能運行。java

注意,WebSocket安全話題僅在此處簡要介紹,由於這是一個很是複雜的問題,能夠單獨撰寫一篇文章。因爲這個緣由,以及我在文章最後一節WebSocket in production?中說起的因素,我建議在生產中先對安全設置進行修改,直到生產就緒,安全措施到位爲止。web

1.WebSocket和STOMP協議

WebSocket協議容許應用程序之間實現雙向通訊。重要的是要知道HTTP僅用於初始握手。初次握手以後,HTTP鏈接將升級爲被WebSocket使用的新TCP/IP鏈接。spring

WebSocket協議是一種至關低級的協議。它定義瞭如何將字節流轉換爲幀。幀能夠包含文本或二進制消息。因爲消息自己不提供有關如何路由或處理它的任何其餘信息,所以很難在不編寫其餘代碼的狀況下實現更復雜的應用程序。幸運的是,WebSocket規範容許在更高的應用程序級別上使用子協議。STOMP是其中之一,由Spring Framework支持。後端

STOMP是一種簡單的基於文本的消息傳遞協議,最初是爲RubyPythonPerl等腳本語言建立的,用於鏈接企業級消息代理。因爲STOMP,使不一樣語言開發的客戶端和代理能夠相互發送和接收消息。WebSocket協議有時稱爲Web TCP。以此類推,STOMP被稱爲Web HTTP。它定義了一些映射到WebSocket幀的幀類型,例如CONNECTSUBSCRIBEUNSUBSCRIBEACKSEND。一方面,這些命令很是便於管理通訊,另外一方面,它們容許咱們實現具備更復雜功能的解決方案,如消息確認。瀏覽器

2.服務端:Spring Boot和WebSocket

爲了構建WebSocket服務器端,咱們將利用Spring Boot框架,該框架使得在Java中開發獨立程序和Web應用程序更快。 Spring Boot包含spring-WebSocket模塊,該模塊與Java WebSocket API標準(JSR-356)兼容。安全

使用Spring Boot實現WebSocket服務器端並非一項很是複雜的任務,只包含幾個步驟,咱們將逐一介紹。bash

*步驟1:*首先,添加WebSocket庫依賴項。服務器

<dependency>
  <groupId>org.springframework.boot</groupId>            
  <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
複製代碼

若是計劃使用JSON格式傳輸消息,則可能還須要包含GSONJackson依賴項。您還可能還須要一個安全框架,例如Spring Securitywebsocket

*步驟2:*而後,能夠配置Spring啓用WebSocketSTOMP消息傳遞。

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主要作兩件事情:

  • 建立內存中的消息代理,其中包含一個或多個用於發送和接收消息的目標。在上面的示例中,定義了兩個目標地址前綴:topicqueue。它們遵循如下慣例:經過pub-sub模型將以topic爲前綴的消息傳遞到全部訂閱客戶端的目標地址。另外一方面,私有消息的目標地址一般以queue爲前綴。
  • 定義前綴app,用於過濾目標地址,這些地址在Controller中被@MessageMapping修飾的方法處理。

—— 圖:服務器端如何處理消息

回到上面的代碼段 - 可能你已經注意到對方法withSockJS()的調用——它啓用了SockJS後備選項。簡而言之,即便互聯網瀏覽器不支持WebSocket協議,它也會讓咱們的WebSockets工做。我將進一步詳細討論這個主題。

還有一件事須要澄清——爲何咱們在端點上調用setAllowedOrigins()方法。通常是必需的,由於WebSocketSockJS的默認行爲是僅接受同源請求。所以,若是客戶端和服務端處於不一樣的域,則須要調用此方法容許它們之間的通訊。

*步驟3:*實現處理用戶請求的控制器 它將向訂閱特定主題的全部用戶廣播收到的消息。

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

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

也可使用SimpMessagingTemplate而不是註解@SendTo,您能夠在控制器內自動裝配(Autowired)。

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

在後面的步驟中,可能須要添加一些其餘類來保護端點,例如Spring Security框架中的ResourceServerConfigurerAdapterWebSecurityConfigurerAdapter。此外,實現消息模型一般是有益的,這樣傳輸的JSON能夠映射成對象。

3.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-user123HTTP會話或STOMP會話標識符可用於此目的。

Spring使發送私人消息變得更加容易。咱們只須要使用@SendToUser註釋Controller的方法。而後,目標地址將由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代碼發送的消息。值得一提的是,WebSocketsHTML5規範的一部分,而且受到大多數現代瀏覽器的支持(從版本10開始,Internet Explorer支持它們)。

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對象。

4.WebSocket安全

許多Web應用程序使用基於cookie的身份驗證,例如,咱們可使用Spring Security限制已登陸的用戶訪問某些頁面或控制器限制。而後,經過基於cookie的HTTP會話維護用戶上下文安全,該會話稍後與爲該用戶建立的WebSocketSockJS會話相關聯。 WebSocket端點能夠像任何其餘請求同樣受到保護,例如,在Spring WebSecurityConfigurerAdapter中的實現。

現在,Web應用程序一般使用REST API做爲後端,使用OAuth/JWT令牌進行用戶身份驗證和受權。 WebSocket協議未描述服務器在HTTP握手期間如何對客戶端進行身份驗證。實際上,標準HTTP頭(例如,受權)用於此目的。不幸的是,並不是全部STOMP客戶端都支持它。 SpringSTOMP客戶端容許爲握手設置標頭:

WebSocketHttpHeaders handshakeHeaders = new WebSocketHttpHeaders();
handshakeHeaders.add(principalRequestHeader, principalRequestValue);
複製代碼

可是SockJS的JavaScript客戶端不支持使用SockJS請求發送受權請求頭(Authorization)。可是,它容許發送可用於傳遞令牌的查詢參數。此方法須要在服務器端編寫自定義代碼,該代碼將從查詢參數中讀取令牌並對其進行驗證。特別重要的是確保令牌不與請求一塊兒記錄(或日誌受到良好保護),由於這可能會致使嚴重的安全違規。

5.SockJS後備選項

WebSocket的集成可能並不老是盡如人意。某些瀏覽器(例如,IE 9)不支持WebSocket。更重要的是,限制性代理可能使HTTP升級變得不可能,或者它們切斷了打開過久的鏈接。在這種狀況下,SockJS就會伸出援手。

SockJS傳輸分爲三大類:WebSocketHTTP StreamingHTTP Long Polling。通訊從SockJS發送GET /info以從服務器獲取基本信息開始。SockJS根據響應決定使用的哪一種傳輸方式。第一個選擇是WebSocket。若是不支持,則儘量使用Streaming。若是Streaming也不可用,則選擇輪詢做爲傳輸方法。

6.生產中使用WebSocket

雖然這種設置有效,但它並非「最佳」。Spring Boot容許您使用任何具備STOMP協議的完整消息系統(例如,ActiveMQ,RabbitMQ),而且外部代理能夠支持更多STOMP操做(例如,確認,租借)而不是咱們使用的簡單代理。 STOMP Over WebSocket提供有關WebSocketSTOMP協議的信息。它列出了處理STOMP協議的消息傳遞系統,多是在生產中使用的更好的解決方案。特別是因爲請求數量很大,消息代理須要進行集羣(Spring的簡單消息代理不適合集羣)。而後,不須要在WebSocketConfig中啓用簡單代理,而是須要啓用Stomp代理中繼,該中繼將消息轉發到外部消息代理和從外部消息代理轉發消息。總而言之,外部消息代理能夠幫助您構建更具伸縮性和可靠性的解決方案。

原文連接:www.toptal.com/java/stomp-…

做者:Tomasz Dąbrowski

譯者:Emma

推薦關注公衆號:鍋外的大佬

每日推送國外優秀的技術翻譯文章,勵志幫助國內的開發者更好地成長!

相關文章
相關標籤/搜索