脈絡:兩個應用程式透過訊息傳遞溝通時,通訊是單向的。但它們可能想要一場雙向對話。

單向通訊的困境#

訊息傳遞提供的是應用程式之間的單向通訊:訊息在 Message Channel 上朝一個方向前進,從寄件端到收件端。這種非同步傳輸讓遞送更可靠,也讓收發雙方解耦。

問題在於,元件之間的通訊往往需要雙向

  • 程式呼叫函式時,會收到回傳值
  • 執行查詢時,會收到查詢結果
  • 一個元件通知另一個變更時,可能想收到確認

幾個行不通的想法:

  • 讓收發雙方同時共享一則訊息,各自往裡加資訊給對方讀——但訊息傳遞不是這樣運作的:訊息是先送出、再接收,雙方無法同時存取同一則訊息。
  • 讓寄件端保留訊息的參考,等接收端把回應放進訊息後再把它拉回來——這對晾衣繩上夾的紙條或許管用,但 Message Channel 不是這樣:通道只朝一個方向傳輸訊息

我們需要的是雙向通道上的雙向訊息

解法#

Request-Reply 有兩個參與者:

  1. Requestor(請求方)——送出請求訊息並等待回覆訊息
  2. Replier(回覆方)——接收請求訊息並以回覆訊息回應

請求通道可以是 Point-to-Point Channel 或 Publish-Subscribe Channel,差別在於請求該不該廣播給所有有興趣的一方、還是只由單一消費者處理。

回覆通道則幾乎一律是點對點——把回覆廣播出去通常沒有意義,它們只該回到請求方手上。

請求方接收回覆的兩種方式#

呼叫端執行遠端程序呼叫時,執行緒必須阻塞等待回應。用 Request-Reply 時,請求方有兩種作法:

同步阻塞(Synchronous Block)#

呼叫端用單一執行緒送出請求訊息,接著阻塞(成為一個 Polling Consumer)等待回覆訊息,收到後處理它。

請求方一旦崩潰,就很難重建那條被阻塞的執行緒。而且「用請求執行緒等回應」意味著同時只能有一個未完成的請求,或者這個請求的回覆通道是該執行緒私有的。

非同步回呼(Asynchronous Callback)#

呼叫端用一條執行緒送出請求訊息並設好回覆的回呼;另一條獨立的執行緒監聽回覆訊息。回覆訊息抵達時,回覆執行緒喚起對應的回呼,由它重建呼叫端的脈絡並處理回覆。

代價:多了「必須重建呼叫端脈絡」的回呼機制這層複雜度。

這兩則訊息代表什麼#

光是兩個應用程式互送請求與回覆並不有趣,有趣的是這兩則訊息代表什麼

  1. Messaging RPC——用訊息傳遞實作遠端程序呼叫。請求是描述「回覆方該喚起哪個函式」的 Command Message,回覆是含有該函式回傳值或例外的 Document Message。
  2. Messaging Query——用訊息傳遞執行遠端查詢。請求是含有查詢的 Command Message,回覆是查詢結果,可能是一個 Message Sequence
  3. Notify/Acknowledge——用訊息傳遞做「帶確認的事件通知」。請求是提供通知的 Event Message,回覆是確認該通知的 Document Message;這則確認本身也可能又是一個請求,用來索取事件的細節。

回覆的三種可能#

請求就像一次方法呼叫,因此回覆是三者之一:

  1. Void——只是通知呼叫端方法已結束,好讓它繼續往下走
  2. 結果值——單一個物件,也就是方法的回傳值
  3. 例外——單一個例外物件,指出方法在成功完成前中止了,以及為什麼

請求應包含 Return Address,告訴回覆方該把回覆送到哪;回覆應包含 Correlation Identifier,指明這是哪一個請求的回覆。

範例:SOAP 與 JMS Requestor 物件

SOAP 1.1 訊息#

SOAP 訊息成對出現:一則 SOAP 請求訊息指出寄件端想在接收端喚起的服務,一則 SOAP 回應訊息則含有該次服務喚起的結果——結果值,或一個 fault(SOAP 中相當於例外的東西)。

SOAP 1.2 的 Response Message Exchange Pattern#

SOAP 1.1 有回應訊息,但描述得很鬆散;SOAP 1.2 引入了明確的 Request-Response Message Exchange Pattern,描述一個對 SOAP 請求的、獨立且可能是非同步的回應。

JMS Requestor 物件#

JMS 提供了幾項可用來實作 Request-Reply 的特性。

TemporaryQueue 是一個可以用程式建立的 Queue,其壽命與建立它的 Connection 相同。只有由同一個 connection 建立的 MessageConsumer 能從中讀取,因此它實際上是該 connection 私有的

MessageProducer 要怎麼知道這個剛建立的私有佇列?請求方會建立一個臨時佇列,並在請求訊息的 reply-to 屬性中指定它(見 Return Address)。行為良好的回覆方會把回覆送回那個指定的佇列——一個若不是靠請求訊息的屬性告知、回覆方根本不會知道的佇列。這是請求方確保回覆一定回到自己手上的簡單作法。

臨時佇列的缺點是:Connection 一關閉,佇列與其中的訊息就被刪除。臨時佇列也無法提供 Guaranteed Delivery——訊息系統一崩潰,連線就中斷,佇列與訊息隨之消失。

JMS 也提供 QueueRequestor,一個用來送請求、收回覆的簡單類別。它內含一個送請求的 QueueSender 與一個收回覆的 QueueReceiver;每個 requestor 各自建立自己的臨時佇列來接收回覆,並在請求的 reply-to 屬性中指定它:

QueueConnection connection = // obtain the connection
Queue requestQueue = // obtain the queue
Message request = // create the request message
QueueSession session = connection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
QueueRequestor requestor = new QueueRequestor(session, requestQueue );
Message reply = requestor.request(request);

request單一個方法就送出請求訊息並阻塞,直到收到回覆訊息。