一、點對點模型概覽
當你只需要將消息發佈送給唯一的一個消息消費者是,就應該使用點對點模型。雖然可能或有多個消費者在隊列中偵聽統一消息,但是,只有一個且僅有一個消費者線程會接受到該消息。
在p2p模型中,生產者稱爲發送者,而消費者則稱爲接受者。點對點模型最重要的特性如下:
- 消息通過稱爲隊列的一個虛擬通道來進行交換。隊列是生產者發送消息的目的地和接受者消費消息的消息源。
- 每條消息通僅會傳送給一個接受者。可能會有多個接受者在一個隊列中偵聽,但是每個隊列中的消息只能被隊列中的一個接受者消費。
- 消息存在先後順序。一個隊列會按照消息服務器將消息放入隊列中的順序,把它們傳送給消費者當消息已被消費時,就會從隊列頭部將它們刪除。
- 生產者和消費者之間沒有耦合。接受者和發送者可以在運行時動態添加,這使得系統的複雜性可以隨着時間而增長或降低(這是消息傳送系統的普遍特性)。
點對點消息傳送模型有兩種類型:異步即發即棄(fire-and-forget)處理和異步請求/應答處理。使用即發即棄處理時,消息生產者向某個隊列發送一條消息,而且它並不會期望接受到一個響應(至少不是立刻接收到響應)。這類處理可用於觸發一個事件,或者用於向接受者發出請求來執行一個並不需要響應的特定活動。異步即發即棄處理如圖4-1所示:
>
使用異步請求/應答處理時,消息生產者向隊裏發送一條消息,然後阻塞等待(blocking wait)應答隊列,該應答隊列正在等待來自接受者的響應。請求/應答處理實現了生產者和消費者之間的高度去耦,允許消息生產者和消費者組件採用不同的語言或平臺。異步請求/應答處理如下圖所示:
用於連接、創建、發送和接受的特定p2p接口見表:
公共API 點對點模型API ConnectionFactory QueueConnectionFactory Destination Queue Connection QueueConnection Session QueueSession MessageConsumer QueueSender MessageProducer QueueReceiver
1.1 何時使用點對點消息傳送模型
JMS的初衷是要提供一種公共API的方法,用於訪問現有的消息傳送系統。在提出JMS規範概念的時候,一些消息傳送系統廠商使用的是P2P模型,而另一些廠商使用的則是發佈/訂閱模型。
當你想讓接受者對某個指定的消息進行一次而且僅僅一次處理時,就必須使用點對點模型。這可能是這兩種模型之間的最重要的區別:點對點模型只會保證只有一個消費者來處理一條指定的消息。在消息要移除分別接受處理時,要在多個JMS客戶端之間均衡消息處理的負載,這是極爲重要的。點對點模型的另一優點就是,它所提供的QueueBrowser允許JMS客戶端對隊列進行快照(Snapshot),以查看正在等待被消費的消息。發佈/訂閱模型則沒有這種瀏覽特性。
點對點消息傳送模型的另一個用例是:您需要在組件之間進行同步通信,而那些組件卻是用不同的編程語言編寫的,或者是在不同的技術平臺(如J2EE或.NET)上實現的。
使用點對點消息傳送模型的另一個充分理由是:使用基於消息的負載均衡,可以讓服務端的組件實現更大的吞吐量,特別是對於同構組件來說更是如此。
1.2 QBorrower和Qlender應用程序
爲了說明點對點消息傳送模型是如何工作的,我們將使用一個簡單去耦的請求/應答用例。其中,QBorrower類使用點對點消息傳送,向QLender類發出了一個簡單的抵押貸款申請。QBorrower類使用LoanRequest隊列,向QLender類發送貸款申請,而且根據特定的業務規則,QLender類使用LoanResponseQ隊列向QBorrower類發回一個響應,表明該LoanRequest是被批准還是拒絕。由於QBorrower感興趣的是要馬上弄清楚貸款批准與否,一旦LoanRequest被髮送出去,QBorrower類就會阻塞,並一直等待來自QLender類的響應,無響應就不再繼續進行工作。
1.2.1 配置並運行應用程序
- 安裝下載ActiveMQ並運行,如下圖所示
- 配置發送接收隊列:進入activeMQ的conf目錄中,打開activemq.xml文件,在其中增加如下代碼:
<destinations>
<queue name="LoanRequestQ" physicalName="jms.LoanRequestQ"/>
<queue name="LoanResponseQ" physicalName="jms.LoanResponseQ"/>
</destinations>
重新啓動MQ,啓動成功後打開瀏覽器輸入網址:http://127.0.0.1:8161/,選擇Queues可看到我們剛纔加進來的隊列,如下圖
3.在程序中生成一個配置文件jndi.properties,內容如下:
4.我們再來看一張我們的程序運行示意圖:
1.2.2 QBorrower類
QBorrowerl 類負責向包含工資額和貸款額的一個隊列發送LoanRequest消息。這個類非常簡單:構造函數建立一個到JMS提供者的連接,創建一個QueueSession,並使用JNDI查找獲得請求和響應隊列。
package cn.com.paner.jms.p2p;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.Buffer;
import java.util.StringTokenizer;
import java.util.UUID;
import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueReceiver;
import javax.jms.QueueSender;
import javax.jms.QueueSession;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class QBorrower {
private QueueConnection qConnect = null;
private QueueSession qSession = null;
private Queue responseQ = null;
private Queue requestQ = null;
public QBorrower(String ququecf,String requestQueue,
String responseQueue)
{
try {
//連接提供者並獲取JMS連接
Context ctxContext = new InitialContext();
QueueConnectionFactory qFactory = (QueueConnectionFactory)
ctxContext.lookup(ququecf);
qConnect = qFactory.createQueueConnection();
//創建JMS會話
qSession = qConnect.createQueueSession(false,Session.AUTO_ACKNOWLEDGE);
//查找請求隊列和響應隊列
requestQ = (Queue)ctxContext.lookup(requestQueue);
responseQ = (Queue)ctxContext.lookup(responseQueue);
//現在完成創建,啓動連接
qConnect.start();
} catch (NamingException | JMSException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void sendLoanRequest(double salary,double loanAmt){
try {
//創建JMS消息‘
MapMessage msg = qSession.createMapMessage();
msg.setDouble("Salary", salary);
msg.setDouble("LoanAmount", loanAmt);
msg.setJMSReplyTo(responseQ);
UUID uuid = UUID.randomUUID();
msg.setStringProperty("UUID", uuid.toString());
//創建發送者併發送消息
QueueSender qSender = qSession.createSender(requestQ);
qSender.send(msg);
//等待查看貸款申請被接受或拒絕
//String filter ="JMSCorrelationID = '"+ msg.getJMSMessageID()+"'";
String filter ="JMSCorrelationID='"+ uuid.toString()+"'";
System.out.println(filter);
//String slecector = "CustomerType = 'GOLD' OR JMSPriority BETWEEN 5 AND 9";
QueueReceiver qReceiver = qSession.createReceiver(responseQ,filter);
TextMessage tmsg = (TextMessage) qReceiver.receive(3000);
System.out.println(tmsg);
if (tmsg == null) {
System.out.println("QLender not responding.");
}else {
System.out.println("Loan request was "+tmsg.getText());
}
} catch (JMSException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void exit()
{
try {
qConnect.close();
} catch (JMSException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.exit(0);
}
public static void main(String[] args) {
// TODO Auto-generated method stub
String queuecf = null;
String requestq = null;
String responseq = null;
if (args.length == 3) {
queuecf = args[0];
requestq = args[1];
responseq = args[2];
}else {
System.out.println("Usage:queueFactoy requestQueue responseQueue.");
System.exit(0);
}
QBorrower borrower = new QBorrower(queuecf, requestq, responseq);
try {
BufferedReader stdin = new BufferedReader
(new InputStreamReader(System.in));
System.out.println("QBorrower Application Started");
System.out.println("Press enter to quit application");
System.out.println("Enter : Salary,Loan_Amount");
System.out.println("\neg,g, 5000 , 12000");
while (true) {
System.out.println(">");
String loanRequest = stdin.readLine();
if (loanRequest == null ||
loanRequest.trim().length() <=0) {
borrower.exit();
}
//解析交易說明】
StringTokenizer st = new StringTokenizer(loanRequest, ",");
double salary = Double.valueOf(st.nextToken().trim()).doubleValue();
double loanAmt = Double.valueOf(st.nextToken().trim()).doubleValue();
borrower.sendLoanRequest(salary, loanAmt);
}
} catch (Exception e) {
// TODO: handle exception
}
}
}
QBorrower類的面方法從命令行接收3個參數:隊列連接工廠的JNDI名稱、貸款申請隊列的JNDI名稱,最後是貸款響應隊列的JNDI名稱,這個響應隊列將接收來自QLender類的響應。
java -jar QBorrowe.jar QueueCF LoanRequestQ LoanResponseQ
java -jar QLender.jar QueueCF LoanResponseQ
JMS初始化
在QBorrower類中,所有的JMS初始化邏輯都在構造函數中處理。構造函數要做到第一件事就是:通過創建一個InitalContext,建立一個到JMS提供者的連接:
//連接提供者並獲取JMS連接
Context ctxContext = new InitialContext();
QueueConnectionFactory qFactory = (QueueConnectionFactory)
ctxContext.lookup(ququecf);
qConnect = qFactory.createQueueConnection();
當創建QueueConnection時,該連接最初是處於停止模式的。這就意味着雖然你可以將消息發送給隊列,但是沒有消息消費者能夠從這個連接接受到消息,直到它被啓動爲止。
QueueConnection對象用於創建一個JMS Session對象,該對象時JMS中的一個工作線程和事務性工作單元。
通常來說,應用程序會在應用程序啓動時創建一個單獨的JMS connection,並維護一個Session對象池,供生產或消費者使用。
QueuSession對象通過QueueConnection對象上的工廠對象來創建。關閉Connection很重要,關閉Connection對象也將關閉所有打開的、和該連接有關的Session對象。創建QueueSession的語句如下:
//創建JMS會話
qSession = qConnect.createQueueSession(false,Session.AUTO_ACKNOWLEDGE);請注意:createQueueSession方法使用兩個參數,第一個參數表示QueueSession是否爲事務性的。true表示是事務性的,這意味着,在QueueSession預期試用期內發送到隊列的消息,將不會傳送給接受者,直到QueueSession上調用commit方法爲止。同樣,在QueueSession上調用rollback方法,會刪除事務性回話期間發送的所有消息。第二個參數表示確認模式。
最後一行代碼啓動連接,自此允許在該連接上接受消息。通常來說,在啓動該連接之前執行所有的初始化邏輯,是一個明智之舉。
發送消息和接受消息
JMS消息時通過和消息類型相匹配的一個工廠方法,是從Session對象中創建的。使用new來時實例化一個新的JMS消息將不會奏效;他必須從Session對象中創建。在創建並加載消息對象之後,我們還爲響應隊列設置了JMSReplyTo消息頭屬性,這會進一步解決生產者和消費者之間的耦合。使用請求/應答模型時,在消息生產者中設置JMSReplyTo消息頭屬性,而不是在消息消費者中指定應答隊列,這是一種同行的標準做法。
//創建JMS消息‘
MapMessage msg = qSession.createMapMessage();
msg.setDouble("Salary", salary);
msg.setDouble("LoanAmount", loanAmt);
msg.setJMSReplyTo(responseQ);
UUID uuid = UUID.randomUUID();
msg.setStringProperty("UUID", uuid.toString());
在創建消息之後,接下來我們將創建QueuSender對象,指定希望發送消息的隊列,然後在使用send方法消息;
//創建發送者併發送消息
QueueSender qSender = qSession .createSender(requestQ);
qSender.send(msg );
在QueueSender對象中,有若干種可用的重寫send方法。
一旦消息已被髮送出去,QBorrower類就會被阻塞,並等待QLender關於貸款被批准或拒絕的響應。這個過程的第一步就是去創建一個消息選擇器,以便我們能夠將響應消息和發送的消息關聯起來。這是很有必要的,因爲申請貸款時,可能同時還有許多其他的貸款申請正被髮送到貸款申請隊列,或者從中出發。爲了確保能夠得到準確的響應消息,我們使用一種消息關聯的技術。
在創建QueueReceiver時,我們會指定過濾器,表明只有在JMSCorrelationID和原始的JMSMessageID相等時纔會接受消息。由於有QueueReceiver,我們能夠調用receive方法進行阻塞等待,直到響應消息被接受爲止。
//等待查看貸款申請被接受或拒絕
//String filter ="JMSCorrelationID = '"+ msg.getJMSMessageID()+"'";
String filter ="JMSCorrelationID='"+ uuid.toString()+"'";
System.out.println(filter);
//String slecector = "CustomerType = 'GOLD' OR JMSPriority BETWEEN 5 AND 9";
QueueReceiver qReceiver = qSession.createReceiver(responseQ,filter);
TextMessage tmsg = (TextMessage) qReceiver.receive(3000);
System.out.println(tmsg);
if (tmsg == null) {
System.out.println("QLender not responding.");
}else {
System.out.println("Loan request was "+tmsg.getText());
}
始終未receive方法指定一個合理的延時值,這肯定是一個明智之舉;否則,它將在那裏一直等待。
1.2.3 QLender
QLender類的作用是去偵聽貸款申請隊列上的貸款申請,判斷工資是否滿足必要的商業要求,並最終將結果發回給借方。
package cn.com.paner.jms.p2p;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueReceiver;
import javax.jms.QueueSender;
import javax.jms.QueueSession;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.naming.Context;
import javax.naming.InitialContext;
public class QLender implements MessageListener {
private QueueConnection qConnect = null;
private QueueSession qSession = null;
private Queue requestQ = null;
public QLender(String Queuecf,String requetQueue){
try {
//連接到提供者並獲得JMS連接
Context ctxContext = new InitialContext();
QueueConnectionFactory qFactory = (QueueConnectionFactory) ctxContext.lookup(Queuecf);
qConnect = qFactory.createQueueConnection();
//創建JMS會話
qSession = qConnect.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
//查找申請隊列
requestQ = (Queue) ctxContext.lookup(requetQueue);
//啓動連接
qConnect.start();
//創建消息偵聽器
QueueReceiver qReceiver = qSession.createReceiver(requestQ);
qReceiver.setMessageListener(this);
System.out.println("Waitting for loan request ...");
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
System.exit(0);
}
}
@Override
public void onMessage(Message arg0) {
// TODO Auto-generated method stub
try {
boolean accepted = false;
if (arg0 instanceof MapMessage) {
System.out.println("message type is legal.");
}
//從消息中獲取數據
MapMessage msg = (MapMessage)arg0;
double salary = msg.getDouble("Salary");
double loanAmt = msg.getDouble("LoanAmount");
//決定是否接受或拒絕貸款申請
if (loanAmt < 200000) {
accepted = (salary / loanAmt) > 0.25;
}else {
accepted = (salary / loanAmt) > 0.33;
}
System.out.println("% = "+(salary / loanAmt)+"loan is ?"
+(accepted? "Accepted" : "Declined"));
//將結果返回
TextMessage tmsg = qSession.createTextMessage();
tmsg.setText(accepted? "Accepted" : "Declined");
//tmsg.setJMSCorrelationID(arg0.getJMSMessageID());
// System.out.println("JMSCorrelationID = "+arg0.getStringProperty("UUID"));
tmsg.setJMSCorrelationID(arg0.getStringProperty("UUID"));
//創建發送者併發送消息
QueueSender qSender =
qSession.createSender((Queue)arg0.getJMSReplyTo());
qSender.setTimeToLive(30*60*1000);
qSender.send(tmsg);
System.out.println("\nWaiting for loan requests...");
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
System.exit(0);
}
}
private void exit()
{
try {
qConnect.close();
} catch (JMSException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.exit(0);
}
public static void main(String argv[])
{
String queuecf = null;
String requestq = null;
if (argv.length == 2) {
queuecf = argv[0];
requestq = argv[1];
}else {
System.out.println("Invalid arguments,Should be:");
System.out.println("java OLender factory request_queue");
System.exit(0);;
}
QLender lender = new QLender(queuecf, requestq);
try {
//
BufferedReader stdin = new BufferedReader(
new InputStreamReader(System.in));
System.out.println("OLender application stared");
System.out.println("Press enter to quit application.");
stdin.readLine();
lender.exit();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
QLender類之所以稱爲一個異步消息偵聽器,意味着它和前面的QBorrower類不同,他在等待消息時,不會阻塞。從QLender類實現MessageListener接口和重寫OnMessage方法的事實來看,這一點是顯而易見的。
一旦啓動連接,QLender類就可以開始接受消息。不過在它能夠接受消息之前,必須由QueueReceiver註冊一個消息偵聽器:
//創建消息偵聽器
QueueReceiver qReceiver = qSession.createReceiver(requestQ);
qReceiver.setMessageListener(this);
至此,已經啓動了一個單獨的偵聽線程。該線程將一直等待,直到接受到一個消息爲止,而且一旦它接受到一條消息,就會調用偵聽器類的onMessage方法。我們可以很容易地將消息傳送工作委託給實現了MessageListener接口的另一個類:
qReceiver.setMessageListener(otherclass);
在createReceiver方法指定的隊列中接受一條消息時,偵聽器線程將異步調用偵聽器類的OnMessage方法。OnMessage方法首先將消息造型成一個MapMessage。
爲了使它更安全,最好是在另一種類型的消息正被髮送到該隊列的情況下,再使用關鍵字instanceof檢查一下JMS的消息類型:
if (arg0 instanceof MapMessage) {
System. out.println("message type is legal." );
}
一旦貸款申請已被分析並作出決定,QLenderl類就須向借方發回響應。爲了完成這個工作,它首先創建一條要發送的JMS消息。響應消息無須和QLender接收的貸款申請消息時相同類型的JMS消息。
1.3 動態隊列對受管隊列
動態隊列是通過使用廠商特定API的應用程序源代碼創建的隊列。受管隊列則是在JMS提供者配置文件或管理工具中定義的隊列。
動態隊列的生成和配置往往取決於特定的廠商。一個隊列可以由一個消費者專用,也可以被多個消費者共享。根據內存共享和溢出到磁盤選項的不同,它可能還會有容量大小的限制。
JMS不會試圖爲一個隊列的所有可能選項定義一組API,而是用廠商特定 的管理方式來管理地設置這些選項,這樣應該是可能的。爲了運行時管理隊列,大多數廠商都會提供命令行管理工具、圖形界面管理工具或API。
JMS提供了一個QueueSession.createQueue(string queueName)方法,打這個方法並不是要在消息傳送系統中定義一個新的隊列。它的設計目的使用用於返回代表某個現有隊列的Queue對象。另外還有一個JMS定義的方法,即QueueSession.createTemporaryQueue()方法,JMS客戶端可以使用這個方法創建一個臨時隊裏,而這個臨時隊裏也只能由該JMS客戶單消費。
如果你有很多隊列,它們的數量還可能會隨時間而增多,那麼創建動態隊列就大有用處。