天天看點

WebSocket的故事(三)—— Springboot中,如何利用WebSocket和STOMP快速建構點對點的消息模式(1)...概述本篇的主線本篇适合的讀者前方高能預警神奇的@SendTo和@SendToUser總結本篇涉及到的代碼

概述

本文是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的實作細節。

本篇涉及到的代碼

SpringWebSocket Github

歡迎持續關注

小銘出品,必屬精品

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