概述
本文是WebSocket的故事系列第三篇第一節,將逐步深入Spring原始碼進行介紹,本系列的乾貨也將陸續在後面的幾篇文章中放出。WebSocket的故事系列計劃分五大篇,旨在由淺入深的介紹WebSocket以及在Springboot中如何快速構建和使用WebSocket提供的能力。本系列計劃包含如下幾篇文章:
第一篇,什麼是WebSocket以及它的用途
第二篇,Spring中如何利用STOMP快速構建WebSocket廣播式訊息模式
第三篇,Springboot中,如何利用WebSocket和STOMP快速構建點對點的訊息模式(1)
第四篇,Springboot中,如何利用WebSocket和STOMP快速構建點對點的訊息模式(2)
第五篇,Springboot中,實現網頁聊天室之自定義WebSocket訊息代理
第六篇,Springboot中,實現更靈活的WebSocket
本篇的主線
上一篇介紹Spring實現的最簡單的STOMP的一種模式,通過@SendTo註解,將訊息傳送到指定訊息代理,只要是訂閱過該訊息代理的客戶端,都會收到這個訊息。作為系列的第三篇,我會分三次來詳細介紹實現細節,本篇將由@SendTo和@SendToUser開始,深入Spring的WebSocket訊息傳送關鍵程式碼進行講解。為下一篇點對點訊息的講解鋪路。
本篇適合的讀者
想要了解STOMP協議,Spring內部程式碼細節,以及如何使用Springboot搭建WebSocket服務的同學。
前方高能預警
本篇的程式碼相對較多,我會盡量細緻講解。
神奇的@SendTo和@SendToUser
本篇我們將詳細介紹這兩個註解背後的故事。
@SendTo
上一篇中,我們利用@SendTo
註解,使方法的返回值推送到訊息代理器中,由訊息代理器廣播到訂閱路徑中去。但並沒有詳細的介紹訊息是怎樣被Spring框架處理,最後傳送廣播出去的。先放上上節中的關鍵程式碼:
@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()
方法:
Broker
的客戶端Session
,然後逐個傳送訊息。這裡,入參destination
就是Broker
的地址,而message
,就是我們返回資訊的封裝,其他細節這裡就不展開講了。
那麼如果我只是想用WebSocket向伺服器發出查詢請求,然後伺服器你就把查詢結果給我就行了,其他使用者就不用你廣播推送了,簡單點,就是我請求,你就推送給我。這又該怎麼辦呢?是的,@SendToUser
就能解決這個問題。
@SendToUser
先上程式碼片段:
@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()) + "!"); //根據傳入的資訊,返回一個歡迎訊息.
}
}
複製程式碼
可以看到,這裡我只是修改了註解,基於上節中我們的示例程式碼,我們啟動程式,試驗一下效果,結果發現並沒有收到返回資訊,這是為什麼呢?讓我們深入程式碼實現的關鍵節點來看看。
@SendToUser背後的實現細節
首先,在我們檢視程式碼細節之前,應該先靜態分析一下。根據之前我們介紹過的內容,很容易想到:
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
訊息。
總結
上例中,我們通過程式碼,詳細講解了一條客戶端訊息到達服務端後,是如何通過程式碼流轉,找到下面兩個關鍵引數的整個流程的。
- 訊息的目的地址
- 封裝返回訊息 希望大家能靜下心來仔細研讀,讀懂這部分程式碼,會對後續的文章理解有很大幫助,同時也能提高大家對Spring設計理念的感悟。瞭解更多Spring的實現細節。
本篇涉及到的程式碼
歡迎持續關注
小銘出品,必屬精品
歡迎關注xNPE技術論壇,更多原創乾貨每日推送。