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

xNPE發表於2018-08-12

概述

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

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

本篇的主線

承接上文對WebSocket的介紹,由WebSocket的傳送接收資訊談起,對STOMP協議做大致介紹,最後,通過Springboot和JS,實際編寫一個WebSocket例子,實現廣播式訊息傳送。

本篇適合的讀者

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


承上啟下

上一篇,我們介紹了WebSocket的握手過程,並未詳細介紹資訊的傳送,只是提到了WebSocket傳送是以幀為單位的。而WebSocket協議上也並沒有規定其訊息傳送的詳細格式。那就意味著每個使用WebSocket的開發者,都需要自己在服務端和客戶端定義一套規則,來傳輸資訊。那麼,有沒有已經造好的輪子呢?答案肯定是有的。這就是STOMP

STOMP(Simple Text Oriented Messaging Protocol)簡介

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

STOMP官方網站

其實STOMP協議並不是為WS所設計的, 它其實是訊息佇列的一種協議, 和AMQP,JMS是平級的。 只不過由於它的簡單性恰巧可以用於定義WS的訊息體格式。 目前很多服務端訊息佇列都已經支援了STOMP, 比如RabbitMQ, Apache ActiveMQ等。很多語言也都有STOMP協議的客戶端解析庫,像JAVA的Gozirra,C的libstomp,Python的pyactivemq,JavaScript的stomp.js等等。

STOMP協議

STOMP是一種基於幀的協議,一幀由一個命令,一組可選的Header和一個可選的Body組成。 STOMP是基於Text的,但也允許傳輸二進位制資料。 它的預設編碼是UTF-8,但它的訊息體也支援其他編碼方式,比如壓縮編碼。

STOMP服務端

STOMP服務端被設計為客戶端可以向其傳送訊息的一組目標地址。STOMP協議並沒有規定目標地址的格式,它由使用協議的應用自己來定義。 例如/topic/a,/queue/a,queue-a對於STOMP協議來說都是正確的。應用可以自己規定不同的格式以此來表明不同格式代表的含義。比如應用自己可以定義以/topic打頭的為釋出訂閱模式,訊息會被所有消費者客戶端收到,以/user開頭的為點對點模式,只會被一個消費者客戶端收到。

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架構

架構圖

WebSocket 的故事(二)—— Spring 中如何利用 STOMP 快速構建 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()}));
}

複製程式碼

演示

WebSocket 的故事(二)—— Spring 中如何利用 STOMP 快速構建 WebSocket 廣播式訊息模式
點選“Connect”按鈕後,如果連線成功,Connect按鈕會置灰;輸入名字後點選Send,服務端會返回歡迎語。


參考程式碼

本篇例子實現程式碼連結地址: SpringWebSocket Github


總結

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


小銘出品,必屬精品

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

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

相關文章