一、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!"})
});