WebSocket的故事(三)—— Springboot中,如何利用WebSocket和STOMP快速構建點對點的消息模式(1)

概述

本文是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和@SendToUser

本篇咱們將詳細介紹這兩個註解背後的故事。服務器

@SendTo

上一篇中,咱們利用@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就能解決這個問題。框架

@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()) + "!"); //根據傳入的信息,返回一個歡迎消息.
    }
}
複製代碼

能夠看到,這裏我只是修改了註解,基於上節中咱們的示例代碼,咱們啓動程序,試驗一下效果,結果發現並無收到返回信息,這是爲何呢?讓咱們深刻代碼實現的關鍵節點來看看。

@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/greetingsuserID是客戶端的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是關鍵字,au3ev44rSessionID,這樣子就把用戶和訂閱路徑惟一的匹配起來了

接着,咱們拿着 targetDestinations地址,調用了 SimpMessageTemplate類中的send方法,最終又來到了 SimpleBrokerMessageHandler類中,眼熟吧,沒錯,就是咱們在介紹 SendTo註解時提到的,只不過,這時候它的目的地址,是 /topic/greetings-userau3ev44r。至此,處理目的地址和封裝消息的工做就完成了。以後,會走實際發送過程,客戶端會收到返回的 greeting消息。

總結

上例中,咱們經過代碼,詳細講解了一條客戶端消息到達服務端後,是如何經過代碼流轉,找到下面兩個關鍵參數的整個流程的。

  • 消息的目的地址
  • 封裝返回消息 但願你們能靜下心來仔細研讀,讀懂這部分代碼,會對後續的文章理解有很大幫助,同時也能提升你們對Spring設計理念的感悟。瞭解更多Spring的實現細節。

本篇涉及到的代碼

SpringWebSocket Github

歡迎持續關注

小銘出品,必屬精品

歡迎關注xNPE技術論壇,更多原創乾貨每日推送。

相關文章
相關標籤/搜索