本文是WebSocket的故事系列第三篇第一節,將逐步深刻Spring源碼進行介紹,本系列的乾貨也將陸續在後面的幾篇文章中放出。WebSocket的故事系列計劃分五大篇,旨在由淺入深的介紹WebSocket以及在Springboot中如何快速構建和使用WebSocket提供的能力。本系列計劃包含以下幾篇文章:html
第一篇,什麼是WebSocket以及它的用途
第二篇,Spring中如何利用STOMP快速構建WebSocket廣播式消息模式
第三篇,Springboot中,如何利用WebSocket和STOMP快速構建點對點的消息模式(1)
第四篇,Springboot中,如何利用WebSocket和STOMP快速構建點對點的消息模式(2)
第五篇,Springboot中,實現網頁聊天室之自定義WebSocket消息代理
第六篇,Springboot中,實現更靈活的WebSocketjava
上一篇介紹Spring實現的最簡單的STOMP的一種模式,經過@SendTo註解,將消息發送到指定消息代理,只要是訂閱過該消息代理的客戶端,都會收到這個消息。做爲系列的第三篇,我會分三次來詳細介紹實現細節,本篇將由@SendTo和@SendToUser開始,深刻Spring的WebSocket消息發送關鍵代碼進行講解。爲下一篇點對點消息的講解鋪路。git
想要了解STOMP協議,Spring內部代碼細節,以及如何使用Springboot搭建WebSocket服務的同窗。github
本篇的代碼相對較多,我會盡可能細緻講解。bash
本篇咱們將詳細介紹這兩個註解背後的故事。服務器
上一篇中,咱們利用@SendTo
註解,使方法的返回值推送到消息代理器中,由消息代理器廣播到訂閱路徑中去。但並無詳細的介紹消息是怎樣被Spring框架處理,最後發送廣播出去的。先放上上節中的關鍵代碼:session
@MessageMapping("/hello") //使用MessageMapping註解來標識全部發送到「/hello」這個destination的消息,都會被路由到這個方法進行處理.
@SendTo("/topic/greetings") //使用SendTo註解來標識這個方法返回的結果,都會被髮送到它指定的destination,「/topic/greetings」.
//傳入的參數Message爲客戶端發送過來的消息,是自動綁定的。
public Greeting greeting(HelloMessage message) throws Exception {
Thread.sleep(1000); // 模擬處理延時
return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"); //根據傳入的信息,返回一個歡迎消息.
}
}
複製代碼
上面方法中的返回值,會被廣播到/topic/greetings
這個訂閱路徑中,只要客戶端訂閱了這個路徑,都會接收到消息。Spring處理消息的主要類是SimpleBrokerMessageHandler
, 當須要發送廣播消息時,最終會調用其中的sendMessageToSubscribers()
方法:app
Broker
的客戶端
Session
,而後逐個發送消息。這裏,入參
destination
就是
Broker
的地址,而
message
,就是咱們返回信息的封裝,其餘細節這裏就不展開講了。
那麼若是我只是想用WebSocket向服務器發出查詢請求,而後服務器你就把查詢結果給我就好了,其餘用戶就不用你廣播推送了,簡單點,就是我請求,你就推送給我。這又該怎麼辦呢?是的,@SendToUser
就能解決這個問題。框架
先上代碼片斷:post
@MessageMapping("/hello") //使用MessageMapping註解來標識全部發送到「/hello」這個destination的消息,都會被路由到這個方法進行處理.
@SendToUser("/topic/greetings") //使用SendToUser註解來標識這個方法返回的結果,都會被髮送到請求它的用戶的destination.
//傳入的參數Message爲客戶端發送過來的消息,是自動綁定的。
public Greeting greeting(HelloMessage message) throws Exception {
Thread.sleep(1000); // 模擬處理延時
return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"); //根據傳入的信息,返回一個歡迎消息.
}
}
複製代碼
能夠看到,這裏我只是修改了註解,基於上節中咱們的示例代碼,咱們啓動程序,試驗一下效果,結果發現並無收到返回信息,這是爲何呢?讓咱們深刻代碼實現的關鍵節點來看看。
首先,在咱們查看代碼細節以前,應該先靜態分析一下。根據以前咱們介紹過的內容,很容易想到:
1.Spring WebSocket通道的創建最開始是源於Http協議的第一次握手,握手成功以後,就打開了客戶端和服務器的WebSocket通道,即客戶端與服務端經過一個
Session
來維持通訊。就像創建一條管道同樣,你有內容就傳給我,我有內容就傳給你。
2.上面的greeting
方法,其實是框架提供給開發者一個處理客戶端請求的一個時機,開發者能夠根據業務須要,對信息處理加工後,返回給客戶端須要的響應結果。那麼當這個方法return
的時候,也就是響應信息由服務端向客戶端返送的開始。
基於上述兩個基本結論,咱們開始分析代碼,首先就是從return
以後開始,看看代碼跑到了哪裏: AbstractMethodMessageHandler.java
中的handleMatch
方法
destination
來進行匹配,找到對應的處理類。在本例中,即根據
/hello
找到
GreetingController
(MessageMapping註解所在位置)。而後即經過
handleMatch
中的
invoke
方法,調用
GreetingController
中的
greeting
方法,
greeting
方法返回後,經過
handleRetureValue
處理其返回值,那麼它對應的方法又是什麼呢?咱們往下看:
順着這個方法,咱們到了一個重要的類,SendToMethodReturnValueHandler.java
從類的名字就能夠看出來,它是用來專門處理SendTo
相關注解的類。當用SendTo
註解的方法返回後,即調用此類中的handleReturnValue
方法來進行處理。代碼流程很清晰,你們參考圖片內的註釋便可。
兩個值得咱們繼續追蹤的點:
1.在
SendToUser
分支中,不管是廣播仍是非廣播消息,都用到了messagingTemplate
。這個messagingTemplate
是什麼?
2.廣播與非廣播的消息發送,都調用了一樣的方法,即convertAndSendToUser
。區別在於非廣播時,多了一個sessionId
參數。這個方法以及這個參數該如何去理解呢?
帶着這樣的疑問繼續追蹤,仍是在SendToMethodReturnValueHandler.java
這個類中:
這裏,咱們又接觸到一個新類,SimpMessagingTemplate
。它實現了convertAndSendToUser
方法,咱們有必要詳細介紹一下這個方法,它的代碼量不大,但卻相當重要:
public void convertAndSendToUser(String user, String destination, Object payload, @Nullable Map<String, Object> headers, @Nullable MessagePostProcessor postProcessor) throws MessagingException {
Assert.notNull(user, "User must not be null");
user = StringUtils.replace(user, "/", "%2F");
destination = destination.startsWith("/") ? destination : "/" + destination;
super.convertAndSend(this.destinationPrefix + user + destination, payload, headers, postProcessor);
}
複製代碼
介紹一下輸入參數:
user
:用戶標識,這裏就是客戶端與服務端連接的sessionId
destination
:這是SendToUser註解後括號內的參數值
payload
:Object
類型,它標識Controller
中定義的方法的返回值,這裏就是GreetingController
類中greeting
方法的返回值
headers
:返回信息的消息頭
postProcessor
:此處爲Null
\
首先對入參進行校驗和歸一化,重點在最後一行,入參處作了字符串拼接,將原來的destination
拼接爲/user/userID/topic/greetings
,userID
是客戶端的SessionID
。拼接結果destination=「/user/au3ev44r/topic/greetings「
。好,接下來,咱們來看一下這個方法:
AbstractMessageSendingTemplate<D>.java
中:
public void convertAndSend(D destination, Object payload, @Nullable Map<String, Object> headers, @Nullable MessagePostProcessor postProcessor) throws MessagingException {
Message<?> message = this.doConvert(payload, headers, postProcessor);
this.send(destination, message);
}
複製代碼
它將要發送的Body
信息與Header
信息進行整合,獲得Message
信息。以後,調用send方法發送。以後通過一系列加工方法的流轉,最後到達了UserDestinationMessageHandler
類中的handleMessage
方法中。
resolveDestination
方法能識別帶
/user
的訂閱路徑並作出處理,
此處將sourceDestination
轉化成/topic/greetings-userau3ev44r
,userau3ev44r
中,user
是關鍵字,au3ev44r
是SessionID
,這樣子就把用戶和訂閱路徑惟一的匹配起來了。
接着,咱們拿着
targetDestinations
地址,調用了
SimpMessageTemplate
類中的send方法,最終又來到了
SimpleBrokerMessageHandler
類中,眼熟吧,沒錯,就是咱們在介紹
SendTo
註解時提到的,只不過,這時候它的目的地址,是
/topic/greetings-userau3ev44r
。至此,處理目的地址和封裝消息的工做就完成了。以後,會走實際發送過程,客戶端會收到返回的
greeting
消息。
上例中,咱們經過代碼,詳細講解了一條客戶端消息到達服務端後,是如何經過代碼流轉,找到下面兩個關鍵參數的整個流程的。
歡迎持續關注
小銘出品,必屬精品
歡迎關注xNPE技術論壇,更多原創乾貨每日推送。