OpenWire CPP 用戶端
連線能力 > 跨語言用戶端 > ActiveMQ Classic C++ 用戶端 > OpenWire CPP 用戶端
OpenWire C++ 用戶端
目標
我們希望能夠為 ActiveMQ Classic 提供一個 C++ API,該 API 在功能上大致與 Java API 保持一致,同時透過開放事件系統的較低層級,在應用程式設計中提供更大的彈性。我們提出了一個 API / 程式設計模型,該模型最大程度地讓訊息傳遞函式庫適應使用它的應用程式,而不是反過來。具體而言,我們的目標是
- 不對包含應用程式的執行緒限制做出任何假設(特別是,允許在單執行緒應用程式內進行非同步接收)
- 不對包含應用程式的事件迴圈特性做出任何假設
- 不對所需的函式庫做出任何假設 – 即,不強制使用特定的函式庫或智慧指標實作
- 在彈性與應用程式複雜性之間提供權衡
- 為非同步事件通知提供明確的 C++ 友善 API
- 提供可插拔的資料傳輸
- 最大化可移植性
它不對包含應用程式的執行緒或事件迴圈模型做出任何假設,因此不會對 C++ 程式設計施加任何限制,作為執行非同步訊息傳遞的固有成本。由於這種彈性且可插拔的設計,此函式庫特別容易實作為高階腳本語言介面與 ActiveMQ Classic 的原生層,因為它不對該語言的執行緒功能做出任何假設。
當我們使用術語程式設計模型時,我們指的是處理非同步訊息傳遞 API 中最複雜的部分 – 將訊息傳遞到應用程式。
JMS / Java 程式設計模型
JMS 1.1 提供兩種讓應用程式接收訊息的方式。一種是在 MsgConsumer 類別的 receive() 函式中進行封鎖接收。這將暫停呼叫執行緒,直到收到訊息為止。另一種方法是透過 MessageListener 子類別中的回呼。當在建立 MessageListener 的主題或佇列上收到訊息時,會呼叫 OnMessage 函式。
總而言之 – JMS 模型將執行緒結構強加給應用程式開發人員。他們必須使用多個執行緒,每個執行緒都進行同步 receive() 呼叫,或者依靠單獨的執行緒將訊息傳遞給他們。單執行緒模型是不可能的。
C++ 和 Java API 最適合的地方出現分歧
JMS API 將特定的執行緒模型強加到應用程式上。這在 Java 程式設計領域中是可以接受的,因為 Java 的設計使其最容易使用執行緒來多工處理通訊。執行緒在 Java 中非常容易且受到良好支援。
但是,在 C++ 中,常見的做法是不同的。大多數以事件驅動方式處理網路通訊的 C++ 程式都使用事件迴圈。這是一個執行緒,會等待多個檔案描述符的活動,並依靠作業系統在有資料時將其喚醒。檔案描述符是事件驅動 C 和 C++ 程式的通用語言,因為作業系統使其易於將它們用於任何類型的事件,並提供豐富的功能來一次等待多個檔案描述符。在 C 和 C+ 中,執行緒容易出錯(因為該語言中沒有並行功能),並且當使用足夠靈活的事件迴圈時,通常是不必要的。這樣做的結果是,非同步訊息傳遞系統的最佳 C+ API 是在 C++ 程式的這種常見做法中運作的 API,而不是強加任意的限制。特別是,即使是與應用程式沒有共用任何資料或程式碼的背景執行緒,也可能會影響它,因為多個執行緒和 UNIX 信號之間的交互沒有明確的定義。
總之,C++ 開發人員在使用可以傳遞事件的函式庫時,期望具有一定程度的開放性和彈性,以便他們可以在不求助於多執行緒的情況下,將多個事件來源整合在一起。大多數正確執行此操作的函式庫都會直接將網路連線的底層檔案描述符公開給應用程式 – 例如,X11 就是這樣做的 – 我們將從實際的設計規範開始。
C++ 的較低層級特性比 Java 帶來更多固有的設計決策。特別是,最大的問題之一是沒有標準的智慧指標實作。Boost 很受歡迎,但是每個企業都有自己的智慧指標實作,而且每個開發人員都有自己的個人偏好。因此,此函式庫不打算使用任何特定的智慧指標實作,因為它會將設計決策強加給在該領域有自己需求的企業。透過仔細使用參考和 API 語義,我們可以使記憶體所有權足夠明確,以避免出現陷阱。事實上,API 政策是所有傳回的指標都由應用程式負責釋放。這應該使其足夠明確。
我們提出一種三層方法,以在應用程式簡單性和彈性之間提供權衡。最低層級將執行最少的操作,並允許最廣泛的應用程式使用它,下一層級將更具功能性,但會有一些設計上的權衡,而最高層級將留給企業特定的需求。
建議的 C++ API 設計
我們提出的設計包含兩個層級。核心函式庫不擁有任何執行緒,並且僅作為篩選器 – 從代理接收資料並在必要時分派訊息。公開此函式庫的核心層級,以允許那些不想要任何執行緒(除了自己管理事件的執行緒)的 C++ 開發人員,和/或想要使用非 TCP 方法與代理進行通訊的開發人員。與 ActiveMQ Classic 代理的套接字應由傳輸層公開給應用程式,並且應用程式應將接收到的資料不透明地傳遞給核心函式庫。這允許使用單執行緒程式進行非同步訊息傳遞 – 這是某些開發人員的硬性要求。
這種方法存在缺點。如果應用程式封鎖某些其他活動,並且永遠不會處理來自代理的資料,則 TCP 套接字將備份,並且訊息將在代理端累積。此外,函式庫的單執行緒特性表示應用程式必須自行完成所有 I/O 工作,並實作一個單獨的執行緒,以便有意義地使用封鎖接收。
我們可以在第一層之上建立第二層(我們稱之為 BrokerSession),它提供這些功能 – 仍然整合到應用程式現有的事件結構中,但會產生背景執行緒的代價。背景執行緒處理來自 ActiveMQ Classic 的資料,並將其傳遞到核心函式庫,將任何產生的訊息放入內部訊息緩衝區(或可能有多個 – 請參閱下面的詳細資訊)。有了這個,函式庫可以為應用程式提供更簡單、更直接的訊息傳遞介面,因為它以與使用核心函式庫的應用程式相同的方式處理代理通訊。
第二層只是呼叫第一個的內部執行個體,以執行任何與 ActiveMQ Classic 相關的訊息傳遞任務 – 沒有任何訊息傳遞系統特定的程式碼在核心較低層級的函式庫之外。因此,第二層是一個薄層 – 僅包含執行緒、與傳輸抽象層的交互以及到核心函式庫的功能傳遞。
有第三層 – 需要更高層級、用於訊息傳遞的應用程式框架函式庫,它們可能會從使用者手中取得所有控制流程,並提供最簡單的環境。設計較低層級的目的在於允許第三層設計和需求的最大彈性,而第三層通常是企業特定的,並且不在本文檔的範圍之內。此層級函式庫的一個範例是提供一個框架的函式庫,該框架使撰寫完全基於回呼的應用程式變得容易。由於這要求從一開始就為此設計應用程式,因此在此層級選擇智慧指標實作等設計決策將是適當的。對於特定企業而言,大多數應用程式很可能會使用類似的東西,這將實作為 BrokerSession 函式庫之上的薄包裝函式。
傳輸層抽象概述
與 ActiveMQ Classic 本身類似,我們提供了一個傳輸層抽象,它管理檔案描述符上的連線和 I/O。最初將僅提供 TCP 套接字支援,但其他實作也有價值 – 程序內管道、從檔案重播、UDP 等。此層提供邏輯連線、傳送、接收和關閉操作。傳輸使用 URI 以與其他 ActiveMQ Classic 傳輸相同的方式進行初始化。
核心函式庫概述
函式庫的核心層級提供了一個非常簡單的介面 – 資料輸入,訊息輸出。
當來自傳輸層的 connect() 呼叫傳回的檔案描述符在應用程式的事件迴圈中讀取為 active 時,應用程式會讀取可用的資料並將其傳遞給函式庫。函式庫本身有一個內部緩衝區,當完全累積訊息時,它將傳遞該訊息。這種斷開的目的在於,核心函式庫不會進行任何封鎖的 I/O 呼叫 – 或任何系統呼叫。這滿足了可移植性目標,並確保單執行緒應用程式可以使用此函式庫,而不會有失去對其執行緒的控制權的風險。
使用核心函式庫的傳出訊息的端對端生命週期為
- 使用者呼叫「傳送」或「訂閱」等。
- 核心函式庫會建構對應的 OpenWire 命令物件
- 核心函式庫將此物件編組到緩衝區中,並將該緩衝區傳回給使用者
使用核心函式庫的傳入訊息的端對端生命週期為
- 使用者從 ActiveMQ Classic 取得資料,可能會使用隨附的傳輸函式庫
-
此資料會傳遞到核心函式庫,該函式庫會執行以下兩件事之一
- 如果它尚未接收訊息,它會查看前幾個位元組,並透過檢查 OpenWire 標頭來記下傳入訊息的大小。
- 如果是,它會將傳入的緩衝區新增到內部訊息緩衝區中,並且如果此新增完成傳入訊息,它會將其還原序列化為 OpenWire 命令物件,並採取適當的動作。
這裡值得注意的是訊息接收的行為 – 核心函式庫能夠分段接收訊息,並且僅在完全接收到訊息時才傳遞訊息。
發佈和訂閱以傳統方式運作,但它們會傳回要由傳輸層傳送的資料,而不是直接傳送資料。這允許較高層級確定資料的傳送方式 – 例如,它允許使用非封鎖 I/O。
此應用程式提供兩種訊息接收方式。程式庫透過一個或多個內部「訊息消費者」物件(類似於 JMS MessageConsumer 物件)來實現此功能。有兩種訊息消費者:一種用於同步接收,另一種用於非同步接收。同步消費者 (BlockingMessageConsumer) 提供一個阻塞式接收,多個執行緒可以等待訊息。非同步訊息消費者使用基於事件的介面,透過寫入管道的位元組通知應用程式訊息已準備就緒。NonBlockingMessageConsumer 類別可以分配其事件檔案描述符以及接收到的訊息——非同步 receive() 呼叫是非阻塞的,如果沒有訊息準備就緒則返回 NULL。
BrokerSession 程式庫概述
由於具有處理來自 ActiveMQ Classic Broker 數據的內部執行緒,因此較高層級的程式庫介面為開發人員提供了比第一層更簡單、更直接的 API。
由於 BrokerSession 程式庫包含核心程式庫的副本,因此特定於 ActiveMQ Classic 的程式碼被封裝在那裡。BrokerSession 程式庫中的程式碼負責管理應用程式的核心程式庫和 Broker 通訊。使用背景執行緒執行此操作並將傳入的訊息傳遞給訊息消費者,大大簡化了應用程式開發。
與核心程式庫中一樣,訊息分派由應用程式明確完成,而不是由背景執行緒完成。兩個程式庫都不儲存訊息回呼——不要求必須使用它們。這確保只有呼叫應用程式已知的執行緒才會執行應用程式程式碼——結果是不會強制或要求執行緒安全程式設計(滿足不強制特定執行緒模型的目標)。在前面分層使用特定於企業的程式碼來執行更多客製化的訊息傳遞(例如,可能使用回呼)並不意外。
此程式庫允許類似 JMS 的多執行緒等待訊息佇列的程式設計模型,以及在 C++ 程式中更常見的事件驅動的選擇循環模型。這使其能夠適應新的和現有的 C++ 應用程式的不同需求,這些應用程式具有由訊息程式庫以外的其他因素施加的設計約束。
記錄和錯誤處理
為了符合不強制特定相依性並允許輕鬆進行企業整合的目標,程式庫具有基於回呼的記錄模型。應用程式向程式庫註冊一個記錄器,以便在發生記錄事件時呼叫 - 此記錄器具有用於錯誤記錄、偵錯記錄等的功能回呼。這允許與現有記錄系統最輕鬆地整合,因為這些回呼可以直接呼叫到原生記錄環境(例如 syslog)或記錄到檔案,或任何其他在特定應用程式中適用的內容。
錯誤處理略有不同。原生 C++ 執行錯誤處理的方式是透過例外。例外對於核心程式庫之類的東西非常有效,因此當發生錯誤時,它會向呼叫者擲回 ActiveMQ::Exception 的實例。但是,在 BrokerSession 程式庫中,此類例外可能會被背景執行緒捕獲。在這種情況下,該執行緒將呼叫應用程式回呼來傳遞例外。這確保所有錯誤都到達應用程式並且可以被適當地處理。
例外處理的預設回呼只是將相關訊息列印到標準錯誤輸出。