JMS消息服務器(二)——點對點消息傳送模型

一、點對點模型概覽

當你只需要將消息發佈送給唯一的一個消息消費者是,就應該使用點對點模型。雖然可能或有多個消費者在隊列中偵聽統一消息,但是,只有一個且僅有一個消費者線程會接受到該消息

在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 配置並運行應用程序

  1. 安裝下載ActiveMQ並運行,如下圖所示
    這裏寫圖片描述
  2. 配置發送接收隊列:進入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客戶單消費。

如果你有很多隊列,它們的數量還可能會隨時間而增多,那麼創建動態隊列就大有用處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章