Solon 之 STOMP

带刺的坐椅發表於2024-10-23

一、STOMP 簡介

如果直接使用 WebSocket 會非常累,就像用 Socket 編寫 Web 應用。沒有高層級的互動協議,就需要我們定義應用間所發訊息的語義,還需要確保連線的兩端都能遵循這些語義。

如 HTTP 在 TCP 套接字之上新增了請求-響應模型層一樣,STOMP 是在 WebSocket 之上提供了基於幀的線路格式層,用來定義訊息的語義。

與 HTTP 請求和響應類似,STOMP 幀由命令、一個或多個頭資訊以及負載組成。像下面這段,就是傳送資料的一個 STOMP 幀:

SEND
transaction:tx-0
destination:/app/hello
content-length:20

{"message":"Hello!"}

在這個示例中,STOMP 命令是 send,表明會傳送一些內容。緊接著是三個頭資訊:一個表示訊息的的事務機制,一個用來表示訊息要傳送到哪裡的目的地,另外一個則包含了負載的大小。然後,緊接著是一個空行,STOMP 幀的最後是負載內容。

二、服務端實現

1、啟用STOMP功能

STOMP 的訊息根據字首的不同分為三種。如下,以 /app 開頭的訊息都可以路由到帶有 @Mapping 註解的方法中;以/topic 開頭的訊息都會傳送到 STOMP 代理中,根據你所選擇的 STOMP 代理不同,目的地的可選字首也會有所限制;以 /user 開頭的訊息會將訊息重路由到某個使用者獨有的目的地上。

新增依賴

<dependency>
    <groupId>org.noear</groupId>
    <artifactId>solon-net-stomp</artifactId>
</dependency>

新增端點監聽,並設定 broker 目的地字首

@ServerEndpoint("/demo")
public class DemoStompBroker extends StompBroker {
    public DemoStompBroker() {
        this.setBrokerDestinationPrefixes("/topic/");
    }
}

2、處理來自客戶端的STOMP訊息

服務端處理客戶端發來的 STOMP 訊息,主要用的是 @Mapping 註解(和 MVC 開發一樣),也可以增加 @Message 方式限有定註解。如下:

@Message //如果不加,同時匹配 http 及其它請求
@Mapping("/app/marco")
@To("*:/topic/marco")
public Shout greeting(Shout shout) throws Exception {
    log.debug("接收到訊息:" + shout.getMessage());
    Shout s = new Shout();
    s.setMessage("Polo!");
    return s;
}

2.1 @Mapping 指定目的地是 /app/marco(我們將其約定為應用的目的地字首)。

2.2 方法接收一個 Shout 引數,因為 Solon 的執行器根據內容型別自動會將 STOMP 訊息的負載轉換為 Shout 物件。

2.3 尤其注意,這個處理器方法有一個返回值,這個返回值並不是返回給客戶端的,而是轉發給訊息代理的,如果客戶端想要這個返回值的話,只能從訊息代理訂閱。@To 註解重寫了訊息代理的目的地,如果不指定@To,幀所發往的目的地會與觸發處理器方法的目的地相同。

2.4 如果客戶端就是想要服務端直接返回訊息呢?聽起來不就是HTTP做的事情!即使這樣,STOMP 仍然為這種一次性的響應提供了支援,用的還是@Mapping 註解,與HTTP不同的是,這種請求-響應模式是非同步的...

@Message
@Mapping("/app/getShout")
public Shout getShout(){
   Shout shout = new Shout();
   shout.setMessage("Hello STOMP");
   return shout;
}

3、傳送訊息到客戶端

3.1 在應用的任意地方傳送訊息

使用 StompEmitter 介面,可以實現自由的向任意目的地傳送訊息。

@Inject
private StompEmitter stompEmitter;

/**
* 透過 http 介面,廣播訊息
*/
@Http
@Mapping("/broadcastShout")
public void broadcast(Context ctx, Shout shout) {
    String json = ctx.renderAndReturn(shout); //渲染資料
    stompEmitter.sendTo("/topic/shouts", json);
}

3.2 更多傳送訊息的方式

如果訊息只想傳送給特定的使用者呢?或者發給當前使用者?或者所有訂閱使用者?solon-net-stomp 給了兩種方式來實現這種功能:

  • 一種是 StompEmitter 介面的 sendTo 方法。
  • 一種是 基於 @To 註解。
StompEmitter 介面 對應的 @To 註解 說明
@To("target:destination?") To 註解表示式(stomp 請求時有效)
sendToSession @To(".:/...")
@To(".")
發給當前客戶端訂閱者
sendToUser @To("user:/...")
@To("user")
發給特定使用者訂閱者
sendTo @To("*:/...")
@To("*")
發給代理,再轉發給所有訂閱者

4、處理訊息異常

在處理訊息的時候,有可能會出錯並丟擲異常。因為STOMP訊息非同步的特點,傳送者可能永遠也不會知道出現了錯誤。可以調整端點監聽,新增 StompListener 實現。

@ServerEndpoint("/demo")
public class DemoStompBroker extends StompBroker implements StompListener{
    public DemoStompBroker(){
        //可選:新增鑑權監聽器(此示例,用本類實現監聽)
        this.addListener(this);
        this.setBrokerDestinationPrefixes("/topic/");
    }
    
    @Override
    public void onError(StompSession session, Throwable error) {
        //可選:如果出錯,反饋給客戶端(比如用 "/user/app/errors")
        getEmitter().sendToSession(session,
                "/user/app/errors",
                new Message(error.getMessage()));
    }
}

三、客戶端

STOMP 可以使用 stomp.js。介面參考: https://stomp-js.github.io/api-docs/latest/classes/Client.html

1、建立連線並訂閱

let stomp = new StompJs.Client({
    brokerURL: "ws://127.0.0.1:8080/demo?user=user01",
    onConnect: function (frame) {
        stomp.subscribe("/topic/marco", function (message) {
            let obj = JSON.parse(message.body);
            console.log("訂閱的服務端訊息:" + obj.message);
        });
        
        stomp.subscribe("/app/getShout", function (message) {
            let obj = JSON.parse(message.body);
            console.log("訂閱的服務端應勝訊息:" + obj.message);
        });
        
        stomp.subscribe("/user/app/errors", function (message) {
            console.log("訂閱的服務端返回的異常訊息:" + message.body);
        });
    }
});

2、傳送訊息

stomp.publish({
    destination: "/app/marco",
    headers: {"content-type": "text/json"},
    body: JSON.stringify({"message": "Marco!"})
});

相關文章