WebSocket
協議是應用程序處理實時消息的方法之一。最多見的替代方案是長輪詢(long polling)和服務器推送事件(server-sent events)。這些解決方案中的每一個都有其優缺點。在本文中,我將向您展現如何使用Spring Boot
實現WebSocket
。我將介紹服務器端和客戶端設置,使用WebSocket
協議之上的STOMP
進行相互通訊。前端
服務器端將徹底用Java編碼。可是,就客戶端而言,我將展現用Java
和JavaScript(SockJS)
編寫的片斷,由於一般,WebSocket
客戶端嵌入在前端應用程序中。代碼示例將演示如何使用pub-sub
模型向多個用戶廣播消息以及如何僅向單個用戶發送消息。在本文的另外一部分中,我將簡要討論WebSocket安全問題以及如何確保即便環境不支持WebSocket
協議,基於WebSocket
的解決方案也能運行。java
注意,WebSocket
安全話題僅在此處簡要介紹,由於這是一個很是複雜的問題,能夠單獨撰寫一篇文章。因爲這個緣由,以及我在文章最後一節WebSocket in production?
中說起的因素,我建議在生產中先對安全設置進行修改,直到生產就緒,安全措施到位爲止。web
WebSocket
協議容許應用程序之間實現雙向通訊。重要的是要知道HTTP
僅用於初始握手。初次握手以後,HTTP
鏈接將升級爲被WebSocket
使用的新TCP/IP
鏈接。spring
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
服務器端並非一項很是複雜的任務,只包含幾個步驟,咱們將逐一介紹。bash
*步驟1:*首先,添加WebSocket庫依賴項。服務器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
複製代碼
若是計劃使用JSON
格式傳輸消息,則可能還須要包含GSON
或Jackson
依賴項。您還可能還須要一個安全框架,例如Spring Security
。websocket
*步驟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
修飾的方法處理。回到上面的代碼段 - 可能你已經注意到對方法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
推薦關注公衆號:鍋外的大佬
每日推送國外優秀的技術翻譯文章,勵志幫助國內的開發者更好地成長!