前情摘要
什麼是 STOMP?
STOMP
Simple or Streaming Text Orientated Messageing Protocal 是簡單(流)文本定向傳輸協議。
STOMP
是 WebSocket
更高級的子協議,它使用一個基於幀的格式來定義消息,與 HTTP 的 Request 和 Response 類似。
STOMP
提供可互操作的連接格式,允許 STOMP
客戶端與任意代理進行交互。STOMP
是一個非常簡單易用的協議, 服務器端實現起來會相對困難一些,編寫客戶端非常容易。
STOMP over WebSocket
STOMP over Websocket
即通過 WebSocket 建立 STOMP
連接,也就是說在 WebSocket 連接的基礎上再建立 STOMP
連接。
WebSocket 協議定義了兩種類型的消息,文本和二進制,但它們的內容是未定義的。
如果說 Socket 是 C/S 的TCP編程,同理 WebSocket 就是Web(B/S)的TCP編程,所以需要在客戶端與服務端之間定義一個機制去協商一個子協議——更高級別的消息協議,將它使用在 WebSocket 之上去定義每次發送消息的類別、格式和內容,等等。
子協議的使用是可選的,但無論哪種方式,客戶端和服務器都需要就一些定義消息內容的協議達成一致。於是,通常選擇在 WebSocket 協議上使用 STOMP
協議來定義內容格式。
接下來我們就一起來看下如何在實際的 Angular 8 項目中是使用 STOMP over WebSocket 進行數據傳遞的吧。
Angular 8 結合 ng2-stompjs
本文的案例是實際的 Angular 8
項目中的一個功能模塊(需要熟悉 Angular 和Typescript),模塊主要包含由右鍵生成負責生產消息的 context-menu-component
動態組件,進度監控 app-progress-bar
組件和日誌輸出 app-console-area
組件。
項目中使用的 UI 庫爲 ng-zorro-antd
,下面是 tabs
組件中的相關僞代碼(省略了組件間 Input Ouput 接口):
...
<nz-tab [nzType]="'card'">
<ng-template #consoleArea>控制檯</ng-template>
<app-progress-bar></app-progress-bar>
<app-console-area></app-console-area>
</div>
</nz-tab>
...
代碼與UI 視圖的對應關係如下:
STOMP
客戶端框架使用的是 ng2-stompjs
庫, ng2-stompjs
目前的版本是 7.xx ,其底層的 @stomp / stompjs 已被重寫,自此與 STOMP
標準具有嚴格的兼容性。
ng2-stompjs
是第一個可靠地支持二進制有效負載的 STOMP
JS客戶端庫。
安裝 ng2-stompjs:
$ npm install @stomp/ng2-stompjs
添加和注入 @stomp/ng2-stompjs:
使用前需要定義配置文件,在目錄 src/app/config/
創建 stomp.config.js
文件:
import { InjectableRxStompConfig } from '@stomp/ng2-stompjs';
import { STOMP_SERVER_BASE_URL } from 'server.config';
const _window: any = window;
export const myRxStompConfig: InjectableRxStompConfig = {
// Which server?
brokerURL: _window.STOMP_SERVER_BASE_URL
? _window.STOMP_SERVER_BASE_URL
: STOMP_SERVER_BASE_URL
// Headers
// Typical keys: login, passcode, host
connectHeaders: {
login: 'guest',
passcode: 'guest'
},
// How often to heartbeat?
// Interval in milliseconds, set to 0 to disable
heartbeatIncoming: 0, // Typical value 0 - disabled
heartbeatOutgoing: 20000, // Typical value 20000 - every 20 seconds
// Wait in milliseconds before attempting auto reconnect
// Set to 0 to disable
// Typical value 500 (500 milli seconds)
reconnectDelay: 200,
// Will log diagnostics on console
// It can be quite verbose, not recommended in production
// Skip this key to stop logging to console
debug: (msg: string): void => {
console.log(new Date(), msg);
}
}
在創建實例時,此配置將由 Angular Dependency Injection
機制注入 RxStompService
服務,在 src/app/app.module.ts
文件中,添加以下內容:
import { InjectableRxStompConfig, RxStompService, rxStompServiceFactory } from '@stomp/ng2-stompjs';
import { myRxStompConfig } from './config/stomp.config';
...
@NgModule({
declarations: [/* 聲明模塊內部成員的地方 */],
imports: [/* 導入的其他module */],
providers: [
{
provide: InjectableRxStompConfig,
useValue: myRxStompConfig
},
{
provide: RxStompService,
useFactory: rxStompServiceFactory,
deps: [InjectableRxStompConfig]
}
],
entryComponents: [/* 不會在模版中引用到的組件 */],
bootstrap: [AppComponent]
})
export class AppModule {}
建立連接
我們現在將 RxStompService
依賴注入 app-progress-bar
組件中,爲此我們將它添加到構造函數中,如下所示:
constructor(private rxStompService: RxStompService) { }
爲了能實時接收服務器發送過來的消息,我們需要在 app-progress-bar
組件的生命週期函數 OnInit
中,使用 watch
方法進行訂閱:
ngOnInit() {
// 訂閱 STOMP 消息
this.topicSubscription = this.rxStompService.watch('/topic/message').subscribe((message: Message) => {
console.log(message.body);
}
this.errorSubscription = this.rxStompService.watch('/topic/error').subscribe((message: Message) => {
this.progressInfo = message.body;
});
}
注:app-message-bar
組件默認是不顯示的,當有消息傳遞進來時,此組件纔會顯示在頁面中,進度達到 100% 時,會自動隱藏。
STOMP 協議是如何將消息準確發送的目的地的呢?
文章開頭提到,STOMP
是一種基於幀的協議,其幀在 HTTP
上建立模型。一個框架由一個命令,一組可選的標題和一個可選的主體組成。
STOMP
服務器被建模爲可以向其發送消息的一組目標,STOMP
協議將目標視爲不透明字符串,其語法是特定於服務器實現的。另外,STOMP
沒有定義目的地 destination
的傳遞語義應該是什麼。目的地的傳遞或“消息交換”語義可能因服務器而異,甚至從目的地到目的地也不同,這使得服務器可以使用 STOMP
支持的語義進行創作。
STOMP
客戶端是一個用戶代理,可以在兩種(可能是同時的)模式下運行:
- 作爲生產者,通過
SEND
框架將消息發送到服務器上的目的地。 - 作爲消費者,發送
SUBSCRIBE
給定目的地的幀並從服務器接收消息作爲MESSAGE 幀。
我們的案例中兩種模式同時存在,發送消息的是生產者(我們上文提到的 context-menu-component
動態組件),接收消息的是消費者(app-progress-bar
組件)。消費者可以通過訂閱不同的 destination
,來獲得不同的推送消息,不需要開發人員去管理這些訂閱與推送目的地之前的關係。
接下來就介紹下作爲生產者的 context-menu-component
組件,看看它都做了哪些事情吧。
發送消息
context-menu-component
組件是觸發右鍵時動態產生的組件,它負責通過向不同的目的地 destination
下達不同的指令,進而來實現不同的功能需求。
使用 ng-zorro-antd
的 Dropdown
組件 ,動態生成:
// ts
public openProjectManagerContextMenu(context: ProjectManagerContext): void {
this.contextMenuComponent = this.nzDropdownService.create(context.mouseEvent, this.contextMenuTemplate);
}
當我們點擊運行用例按鈕時,它作爲生產者會向 STOMP
服務端目的地 SEND
消息指令。
// 運行用例
public runProjectCases(): void {
const streamTaskParam: StreamTaskParam = new StreamTaskParam();
streamTaskParam.project = this.globalService.projectInfo.projectName;
this.openTaskProgressModal('/app/run-project-cases', JSON.stringify(streamTaskParam));
}
從代碼得知,這會將消息發送到名爲的 /app/run-project-cases
的目的地,STOMP
將此目標視爲不透明字符串,並且目標名稱不承擔傳遞語義。
STOMP 定義了自己的消息傳輸體制。首先是通過一個後臺綁定的連接點 endpoint 來建立 socket 連接,然後生產者通過 SEND 方法,綁定好發送的目的地 destination,而 topic 和 app 則是一種消息處理手段的分支,走 app/url 的消息會被你設置到的 MassageMapping 攔截到,進行你自己定義的具體邏輯處理,而走 topic/url 的消息就不會被攔截,直接到 Simplebroker 節點中將消息推送出去。(其中 simplebroker 是 spring 的一種基於內存的消息隊列,你也可以使用 activeMQ,rabbitMQ 代替)。
因此目的地 /app/run-project-cases
生產出來的消息會被攔截,最終會發送到消費者 app-progress-bar
組件的 /topic/message
。
接收消息
app-progress-bar
組件作爲消費者使用 watch
方法啓動與代理的訂閱,this.rxStompService.watch('/topic/message')
將代理到目的地爲 /topic/message
的訂閱上,並返回 RxJS Observable
。
ngOnInit() {
// 訂閱 STOMP 消息
this.topicSubscription = this.rxStompService.watch('/topic/message').subscribe((message: Message) => {
console.log(message.body);
// do something
}
}
app-progress-bar
組件都做了些什麼事情呢?它負責建立 STOMP
連接,從服務器端接收文本流,並將這些流進行數據解析,解析出來的數據一部分用來控制進度條的數值變化,一部分用來控制 app-console-area
組件日誌的輸出節點。也就是說 app-console-area
組件中打印的內容是由 app-progress-bar
組件解析和傳遞的。
取消訂閱
我們知道 RxJS Observable
實際上就是一個函數,它接收一個 Observer
對象作爲參數,返回一個函數用來取消訂閱。所以我們可以在 app-progress-bar
組件銷燬時,調用 unsubscribe()
方法取消訂閱。
ngOnDestroy() {
this.topicSubscription.unsubscribe();
}
本文主要目的是是結合案例展現 STOMP
協議的使用場景,所以不會着重介紹案例上的功能以及實現細節。
瞭解更多請關注公衆號 webinfoq
: