關注公眾號:鍋外的大佬
每日推送國外優秀的技術翻譯文章,勵志幫助國內的開發者更好地成長!
WebSocket
協議是應用程序處理實時消息的方法之一。最常見的替代方案是長輪詢(long polling)和服務器推送事件(server-sent events)。這些解決方案中的每個都有其優缺點。在本文中,我將向您展示如何使用Spring Boot
實現WebSocket
。我將介紹服務器端和客戶端設置,使用WebSocket
協議之上的STOMP
進行相互通信。
服務器端將完全用Java編碼。但是,就客戶端而言,我將展示用Java
和JavaScript(SockJS)
編寫的片段,因為通常,WebSocket
客戶端嵌入在前端應用程序中。代碼示例將演示如何使用pub-sub
模型向多個用戶廣播消息以及如何僅向單個用戶發送消息。在本文的另一部分中,我將簡要討論WebSocket安全問題以及如何確保即使環境不支持WebSocket
協議,基於WebSocket
的解決方案也能運行。
注意,WebSocket
安全話題僅在此處簡要介紹,因為這是一個非常復雜的問題,可以單獨撰寫一篇文章。由於這個原因,以及我在文章最后一節WebSocket in production?
中提及的因素,我建議在生產中先對安全設置進行修改,直到生產就緒,安全措施到位為止。
WebSocket
協議允許應用程序之間實現雙向通信。重要的是要知道HTTP
僅用於初始握手。初次握手之后,HTTP
連接將升級為被WebSocket
使用的新TCP/IP
連接。
WebSocket
協議是一種相當低級的協議。它定義了如何將字節流轉換為幀。幀可以包含文本或二進制消息。由於消息本身不提供有關如何路由或處理它的任何其他信息,因此很難在不編寫其他代碼的情況下實現更復雜的應用程序。幸運的是,WebSocket
規范允許在更高的應用程序級別上使用子協議。STOMP
是其中之一,由Spring Framework
支持。
STOMP
是一種簡單的基於文本的消息傳遞協議,最初是為Ruby
,Python
和Perl
等腳本語言創建的,用於連接企業級消息代理。由於STOMP
,使不同語言開發的客戶端和代理可以相互發送和接收消息。WebSocket
協議有時稱為Web TCP
。以此類推,STOMP
被稱為Web HTTP
。它定義了一些映射到WebSocket
幀的幀類型,例如CONNECT
,SUBSCRIBE
,UNSUBSCRIBE
,ACK
或SEND
。一方面,這些命令非常便於管理通信,另一方面,它們允許我們實現具有更復雜功能的解決方案,如消息確認。
為了構建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
,用於過濾目標地址,這些地址在Controller
中被@MessageMapping
修飾的方法處理。
image
服務器端如何處理消息
回到上面的代碼段 - 可能你已經注意到對方法withSockJS()
的調用——它啟用了SockJS
后備選項。簡而言之,即使互聯網瀏覽器不支持WebSocket
協議,它也會讓我們的WebSockets
工作。我將進一步詳細討論這個主題。
還有一件事需要澄清——為什么我們在端點上調用setAllowedOrigins()
方法。一般是必需的,因為WebSocket
和SockJS
的默認行為是僅接受同源請求。因此,如果客戶端和服務端處於不同的域,則需要調用此方法允許它們之間的通信。
*步驟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
框架中的ResourceServerConfigurerAdapter
或WebSecurityConfigurerAdapter
。此外,實現消息模型通常是有益的,這樣傳輸的JSON
可以映射成對象。
客戶端實現是一項更簡單的任務。
*步驟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
注釋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代碼發送的消息。值得一提的是,WebSockets
是HTML5
規范的一部分,並且受到大多數現代瀏覽器的支持(從版本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
對象。
許多Web
應用程序使用基於cookie
的身份驗證,例如,我們可以使用Spring Security
限制已登錄的用戶訪問某些頁面或控制器限制。然后,通過基於cookie的HTTP會話維護用戶上下文安全,該會話稍后與為該用戶創建的WebSocket
或SockJS
會話相關聯。 WebSocket
端點可以像任何其他請求一樣受到保護,例如,在Spring
WebSecurityConfigurerAdapter
中的實現。
如今,Web
應用程序通常使用REST API
作為后端,使用OAuth/JWT
令牌進行用戶身份驗證和授權。 WebSocket
協議未描述服務器在HTTP
握手期間如何對客戶端進行身份驗證。實際上,標准HTTP
頭(例如,授權)用於此目的。不幸的是,並非所有STOMP
客戶端都支持它。 Spring
的STOMP
客戶端允許為握手設置標頭:
WebSocketHttpHeaders handshakeHeaders = new WebSocketHttpHeaders(); handshakeHeaders.add(principalRequestHeader, principalRequestValue);
但是SockJS
的JavaScript客戶端不支持使用SockJS
請求發送授權請求頭(Authorization)。但是,它允許發送可用於傳遞令牌的查詢參數。此方法需要在服務器端編寫自定義代碼,該代碼將從查詢參數中讀取令牌並對其進行驗證。特別重要的是確保令牌不與請求一起記錄(或日志受到良好保護),因為這可能會導致嚴重的安全違規。
與WebSocket
的集成可能並不總是盡如人意。某些瀏覽器(例如,IE 9)不支持WebSocket
。更重要的是,限制性代理可能使HTTP升級變得不可能,或者它們切斷了打開太久的連接。在這種情況下,SockJS就會伸出援手。
SockJS
傳輸分為三大類:WebSocket
,HTTP Streaming
和HTTP Long Polling
。通信從SockJS
發送GET
/info
以從服務器獲取基本信息開始。SockJS
根據響應決定使用的哪種傳輸方式。第一個選擇是WebSocket
。如果不支持,則盡可能使用Streaming
。如果Streaming
也不可用,則選擇輪詢作為傳輸方法。
雖然這種設置有效,但它並不是“最佳”。Spring Boot
允許您使用任何具有STOMP
協議的完整消息系統(例如,ActiveMQ,RabbitMQ),並且外部代理可以支持更多STOMP
操作(例如,確認,租借)而不是我們使用的簡單代理。 STOMP Over WebSocket
提供有關WebSocket
和STOMP
協議的信息。它列出了處理STOMP
協議的消息傳遞系統,可能是在生產中使用的更好的解決方案。特別是由於請求數量很大,消息代理需要進行集群(Spring的簡單消息代理不適合集群)。然后,不需要在WebSocketConfig
中啟用簡單代理,而是需要啟用Stomp
代理中繼,該中繼將消息轉發到外部消息代理和從外部消息代理轉發消息。總而言之,外部消息代理可以幫助您構建更具伸縮性和可靠性的解決方案。
作者:Tomasz Dąbrowski
譯者:Emma