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

xNPE發表於2018-08-17

概述

本文是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()方法:

WebSocket的故事(三)—— Springboot中,如何利用WebSocket和STOMP快速構建點對點的訊息模式(1)
方法內部會迴圈呼叫當前所有訂閱此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方法

WebSocket的故事(三)—— Springboot中,如何利用WebSocket和STOMP快速構建點對點的訊息模式(1)
當客戶端傳送的訊息到達服務端後,會首先根據訊息的destination來進行匹配,找到對應的處理類。在本例中,即根據/hello找到GreetingController(MessageMapping註解所在位置)。然後即通過handleMatch中的invoke方法,呼叫GreetingController中的greeting方法,greeting方法返回後,通過handleRetureValue處理其返回值,那麼它對應的方法又是什麼呢?我們往下看:

順著這個方法,我們到了一個重要的類,SendToMethodReturnValueHandler.java

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

從類的名字就可以看出來,它是用來專門處理SendTo相關注解的類。當用SendTo註解的方法返回後,即呼叫此類中的handleReturnValue方法來進行處理。程式碼流程很清晰,大家參考圖片內的註釋即可。

繼續追蹤傳送邏輯

兩個值得我們繼續追蹤的點:

1.在SendToUser分支中,無論是廣播還是非廣播訊息,都用到了messagingTemplate。這個messagingTemplate是什麼?
2.廣播與非廣播的訊息傳送,都呼叫了同樣的方法,即convertAndSendToUser。區別在於非廣播時,多了一個sessionId引數。這個方法以及這個引數該如何去理解呢?

帶著這樣的疑問繼續追蹤,還是在SendToMethodReturnValueHandler.java這個類中:

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

這裡,我們又接觸到一個新類,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方法中。

WebSocket的故事(三)—— Springboot中,如何利用WebSocket和STOMP快速構建點對點的訊息模式(1)
其中的resolveDestination方法能識別帶/user的訂閱路徑並做出處理,此處將sourceDestination轉化成/topic/greetings-userau3ev44r,userau3ev44r中,user是關鍵字,au3ev44rSessionID,這樣子就把使用者和訂閱路徑唯一的匹配起來了

WebSocket的故事(三)—— Springboot中,如何利用WebSocket和STOMP快速構建點對點的訊息模式(1)
接著,我們拿著targetDestinations地址,呼叫了SimpMessageTemplate類中的send方法,最終又來到了SimpleBrokerMessageHandler類中,眼熟吧,沒錯,就是我們在介紹SendTo註解時提到的,只不過,這時候它的目的地址,是/topic/greetings-userau3ev44r。至此,處理目的地址和封裝訊息的工作就完成了。之後,會走實際傳送過程,客戶端會收到返回的greeting訊息。

總結

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

  • 訊息的目的地址
  • 封裝返回訊息 希望大家能靜下心來仔細研讀,讀懂這部分程式碼,會對後續的文章理解有很大幫助,同時也能提高大家對Spring設計理念的感悟。瞭解更多Spring的實現細節。

本篇涉及到的程式碼

SpringWebSocket Github

歡迎持續關注

小銘出品,必屬精品

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

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

相關文章