WebSocket 的故事(二)—— Spring 中如何利用 STOMP 快速構建 WebSocket 廣播式消息模式

概述

本文是WebSocket的故事系列第二篇,WebSocket的故事系列計劃分五篇,旨在由淺入深的介紹WebSocket以及在Springboot中如何快速構建和使用WebSocket提供的能力。本系列計劃包含以下幾篇文章:html

第一篇,什麼是WebSocket以及它的用途
第二篇,Spring中如何利用STOMP快速構建WebSocket廣播式消息模式
第三篇,Springboot中,如何利用WebSocket和STOMP快速構建點對點的消息模式(1)
第四篇,Springboot中,如何利用WebSocket和STOMP快速構建點對點的消息模式(2)
第五篇,Springboot中,實現網頁聊天室之自定義WebSocket消息代理
第六篇,Springboot中,實現更靈活的WebSocket前端

本篇的主線

承接上文對WebSocket的介紹,由WebSocket的發送接收信息談起,對STOMP協議作大體介紹,最後,經過Springboot和JS,實際編寫一個WebSocket例子,實現廣播式消息發送。git

本篇適合的讀者

想要了解STOMP協議,以及如何使用Springboot搭建WebSocket服務的同窗。github


承上啓下

上一篇,咱們介紹了WebSocket的握手過程,並未詳細介紹信息的發送,只是提到了WebSocket發送是以幀爲單位的。而WebSocket協議上也並無規定其消息發送的詳細格式。那就意味着每一個使用WebSocket的開發者,都須要本身在服務端和客戶端定義一套規則,來傳輸信息。那麼,有沒有已經造好的輪子呢?答案確定是有的。這就是STOMPweb

STOMP(Simple Text Oriented Messaging Protocol)簡介

STOMP是一個用於C/S之間進行異步消息傳輸的簡單文本協議, 全稱是Simple Text Oriented Messaging Protocol。spring

STOMP官方網站後端

其實STOMP協議並非爲WS所設計的, 它實際上是消息隊列的一種協議, 和AMQP,JMS是平級的。 只不過因爲它的簡單性恰巧能夠用於定義WS的消息體格式。 目前不少服務端消息隊列都已經支持了STOMP, 好比RabbitMQ, Apache ActiveMQ等。不少語言也都有STOMP協議的客戶端解析庫,像JAVA的Gozirra,C的libstomp,Python的pyactivemq,JavaScript的stomp.js等等。bash

STOMP協議

STOMP是一種基於幀的協議,一幀由一個命令,一組可選的Header和一個可選的Body組成。 STOMP是基於Text的,但也容許傳輸二進制數據。 它的默認編碼是UTF-8,但它的消息體也支持其餘編碼方式,好比壓縮編碼。服務器

STOMP服務端

STOMP服務端被設計爲客戶端能夠向其發送消息的一組目標地址。STOMP協議並無規定目標地址的格式,它由使用協議的應用本身來定義。 例如/topic/a,/queue/a,queue-a對於STOMP協議來講都是正確的。應用能夠本身規定不一樣的格式以此來代表不一樣格式表明的含義。好比應用本身能夠定義以/topic打頭的爲發佈訂閱模式,消息會被全部消費者客戶端收到,以/user開頭的爲點對點模式,只會被一個消費者客戶端收到。websocket

STOMP客戶端

對於STOMP協議來講, 客戶端會扮演下列兩種角色的任意一種:

  • 做爲生產者,經過SEND幀發送消息到指定的地址
  • 做爲消費者,經過發送SUBSCRIBE幀到已知地址來進行消息訂閱,而當生產者發送消息到這個訂閱地址後,訂閱該地址的其餘消費者會受到經過MESSAGE幀收到該消息

實際上,WebSocket結合STOMP至關於構建了一個消息分發隊列,客戶端能夠在上述兩個角色間轉換,訂閱機制保證了一個客戶端消息能夠經過服務器廣播到多個其餘客戶端,做爲生產者,又能夠經過服務器來發送點對點消息。

STOMP幀結構

COMMAND
header1:value1
header2:value2

Body^@

^@表示行結束符

一個STOMP幀由三部分組成:命令,Header(頭信息),Body(消息體)

  • 命令使用UTF-8編碼格式,命令有SEND、SUBSCRIBE、MESSAGE、CONNECT、CONNECTED等。
  • Header也使用UTF-8編碼格式,它相似HTTP的Header,有content-length,content-type等。
  • Body能夠是二進制也能夠是文本。注意Body與Header間經過一個空行(EOL)來分隔。

來看一個實際的幀例子:

SEND
destination:/broker/roomId/1
content-length:57

{「type":"ENTER","content":"o7jD64gNifq-wq-C13Q5CRisJx5E"}

  • 第1行:代表此幀爲SEND幀,是COMMAND字段。
  • 第2行:Header字段,消息要發送的目的地址,是相對地址。
  • 第3行:Header字段,消息體字符長度。
  • 第4行:空行,間隔Header與Body。
  • 第5行:消息體,爲自定義的JSON結構。

更多STOMP協議的細節,若是你們感興趣,能夠參考上述的官方網頁,有更多詳細的幀結構介紹。下面,咱們將主要介紹用Springboot和JS實現後端和前端,構建一個WebSocket的小型應用場景。


使用Springboot構建基於STOMP的WebSocket廣播式通訊

Spring中的WebSocket架構

架構圖

圖中各個組件介紹:

  • 生產者型客戶端(左上組件): 發送SEND命令到某個目的地址(destination)的客戶端。
  • 消費者型客戶端(左下組件): 訂閱某個目的地址(destination), 並接收此目的地址所推送過來的消息的客戶端。
  • request channel: 一組用來接收生產者型客戶端所推送過來的消息的線程池。
  • response channel: 一組用來推送消息給消費者型客戶端的線程池。
  • broker: 消息隊列管理者,也能夠成爲消息代理。它有本身的地址(例如「/topic」),客戶端能夠向其發送訂閱指令,它會記錄哪些訂閱了這個目的地址(destination)。
  • 應用目的地址(圖中的」/app」): 發送到這類目的地址的消息在到達broker以前,會先路由到由應用寫的某個方法。至關於對進入broker的消息進行一次攔截,目的是針對消息作一些業務處理。
  • 非應用目的地址(圖中的」/topic」,也是消息代理地址): 發送到這類目的地址的消息會直接轉到broker。不會被應用攔截。
  • SimpAnnotatonMethod: 發送到應用目的地址的消息在到達broker以前, 先路由到的方法. 這部分代碼是由應用控制的。

消息從生產者發出到消費者消費的流轉流程

首先,生產者經過發送一條SEND命令消息到某個目的地址(destination),服務端request channel接受到這條SEND命令消息,若是目的地址是應用目的地址則轉到相應的由應用本身寫的業務方法作處理(對應圖中的SimpAnnotationMethod),再轉到broker(SimpleBroker)。若是目的地址是非應用目的地址則直接轉到broker。broker經過SEND命令消息來構建MESSAGE命令消息, 再經過response channel推送MESSAGE命令消息到全部訂閱此目的地址的消費者。 廢話很少說,下面直接上代碼。

Spring運用WebSocket實現簡單的廣播消息

場景描述

咱們來實現一個簡單聊天室的第一步,每當有用戶加入聊天室時,該用戶向服務器發送加入聊天室的消息,服務器向當前聊天室內的全部用戶發送歡迎語。

建立Message-handling Controller

在Spring中,STOMP消息會被路由到以Controller註解標識的類中。即咱們須要定義一個控制器類,並使用Controller註解來標識它,而後在其中實現具體的消息處理方法,咱們建立一個名爲GreetingController的類:

package com.xnpe.club.wbs.controller;

import com.xnpe.club.wbs.data.Greeting;
import com.xnpe.club.wbs.data.HelloMessage;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import org.springframework.web.util.HtmlUtils;

@Controller //使用Controller註解來標識這是一個控制器類
public class GreetingController {

    @MessageMapping("/hello") //使用MessageMapping註解來標識全部發送到「/hello」這個destination的消息,都會被路由到這個方法進行處理.
    @SendTo("/topic/greetings") //使用SendTo註解來標識這個方法返回的結果,都會被髮送到它指定的destination,「/topic/greetings」.
    //傳入的參數HelloMessage爲客戶端發送過來的消息,是自動綁定的。
    public Greeting greeting(HelloMessage message) throws Exception {
        Thread.sleep(1000); // 模擬處理延時
        return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"); //根據傳入的信息,返回一個歡迎消息.
    }

}
複製代碼

總體下來,greeting()方法的做用是,處理全部發到/hello這個destination的信息,並將處理的結果,發送到全部訂閱了/topic/greetings這個destination的客戶端。其中模擬的延時,其本質是爲了演示在WebSocket中,咱們無需考慮超時這樣的問題,即上一篇文章提到的,客戶端與服務端鏈接創建後,服務端能夠根據實際場景,在「任何有須要」的時候「推送」消息到客戶端,直到鏈接釋放。

爲Spring配置STOMP消息

剛纔咱們已經建立了消息處理控制器,也就是咱們的業務處理邏輯。如今咱們要爲Spring配置WebSocket和STOMP消息設置。 建立一個名爲WebSocketController的類:

package com.xnpe.club.wbs.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration //使用Configuration註解標識這是一個Springboot的配置類.
@EnableWebSocketMessageBroker //使用此註解來標識使能WebSocket的broker.即便用broker來處理消息.
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    //實現WebSocketMessageBrokerConfigurer中的此方法,配置消息代理(broker)
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic"); //啓用SimpleBroker,使得訂閱到此"topic"前綴的客戶端能夠收到greeting消息.
        config.setApplicationDestinationPrefixes("/app"); //將"app"前綴綁定到MessageMapping註解指定的方法上。如"app/hello"被指定用greeting()方法來處理.
    }

    @Override
    //用來註冊Endpoint,「/gs-guide-websocket」即爲客戶端嘗試創建鏈接的地址。
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/gs-guide-websocket").withSockJS();
    }

}
複製代碼

配置主要包含兩部份內容,一個是消息代理,另外一個是Endpoint,消息代理指定了客戶端訂閱地址,以及發送消息的路由地址;Endpoint指定了客戶端創建鏈接時的請求地址。

至此,服務端的配置工做就完成了,很是簡單。如今,讓咱們實現一個前端頁面,來驗證服務的工做狀況。

建立前端實現頁面

針對STOMP,前端咱們採用JavaScript的stomp的客戶端實現stomp.js以及WebSocket的實現SockJS。此處只展現核心代碼。

//使用SockJS和stomp.js來打開「gs-guide-websocket」地址的鏈接,這也是咱們使用Spring構建的SockJS服務。
function connect() {
    var socket = new SockJS('/gs-guide-websocket');
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function (frame) {
        //鏈接成功後的回調方法
        setConnected(true);
        console.log('Connected: ' + frame);
        //訂閱/topic/greetings地址,當服務端向此地址發送消息時,客戶端便可收到。
        stompClient.subscribe('/topic/greetings', function (greeting) {
            //收到消息時的回調方法,展現歡迎信息。
            showGreeting(JSON.parse(greeting.body).content);
        });
    });
}
//斷開鏈接的方法
function disconnect() {
    if (stompClient !== null) {
        stompClient.disconnect();
    }
    setConnected(false);
    console.log("Disconnected");
}
//將用戶輸入的名字信息,使用STOMP客戶端發送到「/app/hello」地址。它正是咱們在GreetingController中定義的greeting()方法所處理的地址.
function sendName() {
    stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
}

複製代碼

演示

點擊「Connect」按鈕後,若是鏈接成功,Connect按鈕會置灰;輸入名字後點擊Send,服務端會返回歡迎語。


參考代碼

本篇例子實現代碼連接地址: SpringWebSocket Github


總結

至此,咱們實現了一個最簡單的使用Spring,基於STOMP的WebSocket例子。下一篇咱們會基於這個例子,繼續完善聊天室功能,實現點對點的通訊功能。即兩個用戶如何點對點的聊天,敬請期待。


小銘出品,必屬精品

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

相關文章
相關標籤/搜索