Spring框架4.0 M2 中的 WebSocket 消息架構

jopen 11年前發布 | 94K 次閱讀 Spring JEE框架 Spring Framework

正如以前我所寫的那樣,WebSocket API只是WebSocket形式消息應用的起點。許多實際的挑戰仍然存在。這也正是一個Tomcat郵件列表用戶最近苦思冥想的

確實對我來說Websocket仍然不是一個真正的“已經準備好的產品”,(我不討論Tomcat本身的實現,只是更一般性地討論)...IE里的本地WebSocket功能只是自IE-10開始才可用,而且允許低版本的IE運行WebSocket的方案是有些“不確定的”(例如轉到取決于Adobe的FlashPlayer而不是取決于IE本身)。(我們的大多數客戶都是相當大的公司,它們不打算更新瀏覽器,也不在防火墻上開放特殊的端口,只是請我們去做)

Spring框架4.0的第一個發布版提供了SockJS服務器端的支持,以及最好的和最多的綜合的WebSocket瀏覽器后備選項。在那些不支持WebSocket的瀏覽器里,我們需要這些后備選項。這種情況下網絡代理將阻止使用WebSocket。今天,簡單地把SockJS放置在瀏覽器里就能讓你創建WebSocket應用,而且在必要的時候由未被察覺的后備選項決定該怎么做。

即使有后備選項,仍有巨大的挑戰。一個socket是一個層次非常低的抽象,今天絕大多數web應用并不是針對socket編程的。這就是為什么WebSocket要定義一個子協議機制,從本質上啟用,促進基于WebSocket的高等級協議的使用,就像我們基于TCP使用的HTTP。

這個Spring Framework 4.0的第二個里程碑,使得使用基于WebSocket的高等級消息協議成為可能。為了演示這一點,我們匯集出了一個示例程序。

股票投資組合案例(Stock Portfolio Sample)

股票組合案例應用, 可自Github獲得, 加載一個用戶的投資組合頭寸,允許購買和出售股票,消費價格行情,以及顯示位置更新。這是一個相當簡單的應用。但是它解決了基于瀏覽器的消息應用可能會遇到的 許多一般性任務。

Spring框架4.0 M2 中的 WebSocket 消息架構

那么我們怎樣建立起一個這樣的應用呢?從使用HTTP和REST以后,我們就已習慣于基于URL和HTTP動作來表達需要做的事情。這里我們有一個socket和許多的消息。你又怎樣去告訴誰一個消息,并說明這個消息的意思呢?

Spring框架4.0 M2 中的 WebSocket 消息架構

瀏覽器與服務器必須在語義表達以前,對公共的消息格式達成一致。有幾個已存在的協議可以幫助做到這一點。我們為這個里程碑選擇了STOMP,這歸因于它的簡單與廣泛的支持

面向簡單的/流形式的文本消息協議(STOMP)

STOMP是為了簡單而創建的一種消息協議。它基于模仿HTTP協議的幀。幀由一個命令、可選的頭和可選的體組成。
例如股票投資應用需要接收股票報價,因此客戶端發送SUBSCRIBE幀,這幀頭中的目的表明客戶端打算訂閱什么:

SUBSCRIBE
id:sub-1
destination:/topic/price.stock.* 

當股票報價有效時,服務器發送含有匹配目的和訂閱id以及內容類型頭和內容體的MESSAGE幀:

MESSAGE
subscription:sub-1
message-id:wm2si1tj-4
content-type: application/json
destination:/topic/stocks.PRICE.STOCK.NASDAQ.EMC

{\"ticker\":\"EMC\",\"price\":24.19}</pre>為了在瀏覽器里實現這些,我們使用了 stomp.js SockJS客戶端

varsocket =newSockJS('/spring-websocket-portfolio/portfolio');
varclient = Stomp.over(socket);

varonConnect =function() { client.subscribe("/topic/price.stock.*",function(message) { // process quote }); }; client.connect('guest','guest', onConnect); </pre>這已經獲得巨大收獲!我們擁有了標準的消息格式和客戶端支持。
現在我們把其中一個移動到服務器端。

消息代理方案

服務器端的一個選項是純消息代理方案,這時消息可直接發送到傳統的消息代理如RabbitMQ,ActiveMQ等。即便不是所有的消息代理都支持,大多數都支持TCP上的STOMP,不過它們也逐漸支持WebSocket上的STOMP,而隨著RabbitMQ進一步發展,它也支持SockJS。我們的架構看起來如下:

Spring框架4.0 M2 中的 WebSocket 消息架構

這是一個可靠且可伸縮的的方案,然而可能不是最適合手邊的這種問題。消息代理通常在企業內部使用。直接把它們暴露在互聯網上不是理想的選擇。

如果我們已經從REST里學習到了什么,那么它就是我們不打算暴露如數據庫或者域模型這樣的我們系統內部的細節。

另外,做為一名Java開發人員,你想應用安全、有效性以及添加應用邏輯。在消息代理方案里,應用服務器位于消息代理之后,這很大程度上違背了大多數互聯網應用開發人員習慣的用法。

這就是像socket.io這樣的庫流行的原因。它簡單且滿足了開發互聯網應用的需求。另一方面,我們必須不能忽視消息代理處理消息的能力,它們真正的擅長是處理消息,而消息路由是個難題。我們需要兩者都最佳。

應用和消息代理方案

另一個方案是讓應用處理進來的消息且做為互聯網客戶端和消息代理之間的中間人。來自客戶端的消息通過應用流向代理,相反來自代理的消息通過應用返回給客戶端。這給應用提供了檢查進入的消息類型和“目的”頭的機會,以確定是處理消息呢,還是傳遞消息給代理。

Spring框架4.0 M2 中的 WebSocket 消息架構

這就是我們選擇的方案。為了更好的說明這個方案,下面有一些應用場景。

裝載投資組合應用的狀態

  •  客戶端請求投資組合應用的狀態
  •  應用裝載并給訂閱者返回數據的方式處理這個請求
  •  這種交互沒有涉及到消息代理
  • </ul>

    訂閱股票報價

    •   客戶端發送股票報價的訂閱請求
    •   應用把這條消息傳遞給消息代理
    •   消息代理傳送消息給所有已經訂閱的所有客戶端
    • </ul>

      接收股票報價

      •  報價服務發送股票報價信息給消息代理
      •  消息代里傳送消息給所有訂閱的用戶
      • </ul>

        執行交易

        •   客戶端發送交易請求
        •   應用處理這個請求,通過交易服務提交所要執行的交易
        •   這種交互里沒有涉及到消息代理
        • </ul>

          接收位置更新

          •   交易服務發送狀態更新消息給消息代理上的隊列
          •   消息代理發送狀態更新信息給客戶端
          •   更多發送消息給特定的客戶的細節在下面
          • </ul> 嚴格的來說是否使用消息代理是可以選擇的。為了初步認識代理,我們提供了直接可以使用的“簡單的”替代軟件。然而為了保證可伸縮性和部署多個應用服務器,建議使用消息代理。

            代碼片段

            讓我們看看客戶端和服務器端代碼的一些例子。
            下面是請求投資組合應用狀態的 portfolio.js

            stompClient.subscribe("/app/positions",function(message) {
              self.portfolio().loadPositions(JSON.parse(message.body));
            }); 
            在服務器端,PortfolioController檢測請求,然后返回投資組合應用的狀態,這解釋了互聯網應用里非常普通的請求-應答交互。由于我們使用Spring Security來保護HTTP請求,它包括產生WebSocket握手的請求。下面的principal方法參數是從用戶HttpServeletRequest的principal Spring Security集里提取出來的。

            @Controller
            publicclassPortfolioController {

            // ...

            @SubscribeEvent("/app/positions") publicList<PortfolioPosition> getPortfolios(Principal principal) { String user = principal.getName(); Portfolio portfolio =this.portfolioService.findPortfolio(user); returnportfolio.getPositions(); } } </pre>下面發送交易請求的protfolio.js:

            stompClient.send("/app/trade", {}, JSON.stringify(trade)); 
            在服務器端,PortfolioController發送執行的交易:

            @Controller
            publicclassPortfolioController {

            // ...

            @MessageMapping(value="/app/trade") publicvoidexecuteTrade(Trade trade, Principal principal) { trade.setUsername(principal.getName()); this.tradeService.executeTrade(trade); } } </pre>PortfolioController還可以處理不期望的例外,并發送消息給用戶。

            @Controller
            publicclassPortfolioController {

            // ...

            @MessageExceptionHandler @ReplyToUser(value="/queue/errors") publicString handleException(Throwable exception) { returnexception.getMessage(); } } </pre>從應用內部發送消息給訂閱的用戶意味著什么呢?下面是報價服務如何發送報價的:

            @Service
            publicclassQuoteService {

            privatefinalMessageSendingOperations<String> messagingTemplate;

            @Scheduled(fixedDelay=1000) publicvoidsendQuotes() { for(Quote quote :this.quoteGenerator.generateQuotes()) { String destination ="/topic/price.stock."+ quote.getTicker(); this.messagingTemplate.convertAndSend(destination, quote); } } } </pre>而下面是交易服務器在交易執行完成后是如何發送狀態更新的:

            @Service
            publicclassTradeService {

            // ...

            @Scheduled(fixedDelay=1500) publicvoidsendTradeNotifications() { for(TradeResult tr :this.tradeResults) { String queue ="/queue/position-updates"; this.messagingTemplate.convertAndSendToUser(tr.user, queue, tr.position); } } } </pre>而且以防你疑惑....不要疑惑,根據以前構建在線游戲應用的開發者在文檔里所提的建議PortfolioController還可以包含Spring的MVC方法(例如@RequestMapping):

            是的,把[消息]映射和Spring的MVC映射統一是很好的。沒有理由不統一
            它們。
             就像報價服務和交易服務一樣,Spring的MVC控制器方法也可以發布消息。

            Spring應用對消息處理的支持

            Spring Integration已經為眾所周知的企業集成模式和輕量級消息處理提供一流的抽象很長時間了。當我們在Spring Integration上面工作的時候,我們認識到后者才真正是我們需要構建的基礎。

            因此,我很高興地宣布我們已經把精選出來的整合到Spring框架的Spring Integration原型轉換為一個新的提前稱為spring消息處理的模塊。除了像Message,MessageChannel,MessageHandler以及其他核心抽象外,新的模塊還包括所有支持這篇文章中所闡釋的新特性的注釋腳本和類。

            有了這種思想,現在我們就可以看看股票投資組合應用的內部架構圖:

            Spring框架4.0 M2 中的 WebSocket 消息架構

            StompWebSocketHandler把進入的客戶端信息放到“分發”消息通道上。這個通道有3個訂閱者。第一個把消息委派給注釋方法,第二個轉發消息給STOMP消息代理,而第三個通過轉換目的為唯一的客戶端訂閱的隊列名而處理發送給各個用戶的消息(更詳細的細節見下面)。

            默認情況下應用是以一個“簡單的“提供初始化選項的消息代理運行的。正如樣例的README文件所說明的那樣,你可以通過激活或者不激活配置文件來在在“簡單的”和全功能的消息代理之間進行切換。

            Spring框架4.0 M2 中的 WebSocket 消息架構

            另一個可能更改的配置是從Executor切換到MessageChannel的消息傳遞的基于Reactor的實現。最近發布了第一個版本的Reactor項目還可以用來管理應用和消息代理之間的TCP連接。

            你可以看到包括新Spring Security Java配置全功能的應用配置。你也可能有興趣Java配置里對改進的STS的支持

            發送消息給單個用戶

            明白如何廣播消息給多個已訂閱的客戶是很容易的,只要發布消息給某個話題組。明白如何發送消息給特定的用戶卻是非常難。例如你可能捕捉到一個例外而且想發送錯誤消息。或者你可能接收到交易確認消息并想把這條消息發送給用戶。

            在傳統的消息處理應用里,創建臨時隊列并在期望應答的消息上設置“rely-to"頭是很常見的。然而在互聯網應用里這個工作卻相當難以處理。客戶端必須記住在所有應用的消息上設置必須的消息頭,而服務器應用可能需要跟蹤并傳遞這個消息頭。有時這樣的信息可能完全不是那么容易用,比如,當把HTTP POST當作傳遞消息的另一個選項的時候。

            為了支持這種需求,我們發送一個唯一的隊列后綴給每個已經連接的客戶端。然后添加這個后綴以創建唯一的隊列名。
            client.connect('guest','guest',function(frame) {

            varsuffix = frame.headers['queue-suffix'];

            client.subscribe("/queue/error"+ suffix,function(msg) { // 處理錯誤 });

            client.subscribe("/queue/position-updates"+ suffix,function(msg) { // 處理狀態更新 });

            }); </pre>然后在服務器端,給@messageExceptionHandler方法(或者任何消息處理方法)增加一個@ReplyToUser注釋腳本,用來以消息的形式發送返回值。

            @MessageExceptionHandler
            @ReplyToUser(value="/queue/errors")
            publicString handleException(Throwable exception) {
              // ...
            } 
            像交易服務(TradeService)這樣的所有其他類都可以使用消息模板獲得同樣的功能。
            String user ="fabrice";
            String queue ="/queue/position-updates";
            this.messagingTemplate.convertAndSendToUser(user, queue, position); 
            在以上兩種情況下,為了重新構造正確的隊列名,我們內部(通過配置 用戶隊列后綴解析器)定位用戶的隊列后綴。目前只有一個簡單的解釋器實現了這個功能。然而不管用戶是否連接到這個或者其他應用服務器,增加支持同樣功能的 Redis實現是很簡單的。

            結論

            但愿這篇文章是對新功能的有用的介紹。為了不使這篇文章過長,我鼓勵你去看看這些例子,思考一下它對你正在寫或者即將寫的應用意味著什么。由于9月早期我將發布一個新的版本,因此現在是反饋的最佳時間。

            SpringOne 2GX 2013即將來臨

            請你盡快訂購在圣克拉拉召開的SpringOne大會的位子。它確實是獲得正在開發的所有一切的一手資料和直接提供反饋的絕佳機會。希望今年有許多重要的新公告發布。看看最新的博客文檔是否正如我所說,接下來還會有更多的文章發上來!

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!