activemq的幾種基本通信方式總結

簡介

activemq是JMS消息通信規範的一個實現。總的來說,消息規範裏面定義最常見的幾種消息通信模式主要有發佈-訂閱、點對點這兩種。另外,通過結合這些模式的具體應用,我們在處理某些應用場景的時候也衍生出來了一種請求應答的模式。下面,我們針對這幾種方式一一討論一下。

基礎流程

在討論具體方式的時候,我們先看看使用activemq需要啓動服務的主要過程。
按照JMS的規範,我們首先需要獲得一個JMS connection factory.,通過這個connection factory來創建connection.在這個基礎之上我們再創建session, destination, producer和consumer。因此主要的幾個步驟如下:

  1. 獲得JMS connection factory. 通過我們提供特定環境的連接信息來構造factory。
  2. 利用factory構造JMS connection
  3. 啓動connection
  4. 通過connection創建JMS session.
  5. 指定JMS destination.
  6. 創建JMS producer或者創建JMS message並提供destination.
  7. 創建JMS consumer或註冊JMS message listener.
  8. 發送和接收JMS message.
  9. 關閉所有JMS資源,包括connection, session, producer, consumer等。

publish-subscribe

發佈訂閱模式有點類似於我們日常生活中訂閱報紙。每年到年尾的時候,郵局就會發一本報紙集合讓我們來選擇訂閱哪一個。在這個表裏頭列了所有出版發行的報紙,那麼對於我們每一個訂閱者來說,我們可以選擇一份或者多份報紙。比如北京日報、瀟湘晨報等。那麼這些個我們訂閱的報紙,就相當於發佈訂閱模式裏的topic。有很多個人訂閱報紙,也有人可能和我訂閱了相同的報紙。那麼,在這裏,相當於我們在同一個topic裏註冊了。對於一份報紙發行方來說,它和所有的訂閱者就構成了一個1對多的關係。這種關係如下圖所示:
這裏寫圖片描述
現在,假定我們用前面討論的場景來寫一個簡單的示例。我們首先需要定義的是publisher.

publisher

publisher是屬於發佈信息的一方,它通過定義一個或者多個topic,然後給這些topic發送消息。
publisher的構造函數如下:

    public Publisher() throws JMSException {  
            factory = new ActiveMQConnectionFactory(brokerURL);  
            connection = factory.createConnection();  
            try {  
            connection.start();  
            } catch (JMSException jmse) {  
                connection.close();  
                throw jmse;  
            }  
            session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);  
            producer = session.createProducer(null);  
        }  

我們按照前面說的流程定義了基本的connectionFactory, connection, session, producer。這裏代碼就是主要實現初始化的效果。
接着,我們需要定義一系列的topic讓所有的consumer來訂閱,設置topic的代碼如下:

    protected void setTopics(String[] stocks) throws JMSException {  
        destinations = new Destination[stocks.length];  
        for(int i = 0; i < stocks.length; i++) {  
            destinations[i] = session.createTopic("STOCKS." + stocks[i]);  
        }  
    }  

這裏destinations是一個內部定義的成員變量Destination[]。這裏我們總共定義了的topic數取決於給定的參數stocks
在定義好topic之後我們要給這些指定的topic發消息,具體實現的代碼如下:


    protected void sendMessage(String[] stocks) throws JMSException {  
        for(int i = 0; i < stocks.length; i++) {  
            Message message = createStockMessage(stocks[i], session);  
            System.out.println("Sending: " + ((ActiveMQMapMessage)message).getContentMap() + " on destination: " + destinations[i]);  
            producer.send(destinations[i], message);  
        }  
    }  

    protected Message createStockMessage(String stock, Session session) throws JMSException {  
        MapMessage message = session.createMapMessage();  
        message.setString("stock", stock);  
        message.setDouble("price", 1.00);  
        message.setDouble("offer", 0.01);  
        message.setBoolean("up", true);  

        return message;  
    }  

前面的代碼很簡單,在sendMessage方法裏我們遍歷每個topic,然後給每個topic發送定義的Message消息。
在定義好前面發送消息的基礎之後,我們調用他們的代碼就很簡單了:

    public static void main(String[] args) throws JMSException {  
        if(args.length < 1)  
            throw new IllegalArgumentException();  

            // Create publisher       
            Publisher publisher = new Publisher();  

            // Set topics  
        publisher.setTopics(args);  

        for(int i = 0; i < 10; i++) {  
            publisher.sendMessage(args);  
            System.out.println("Publisher '" + i + " price messages");  
            try {  
                Thread.sleep(1000);  
            } catch(InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
        // Close all resources  
        publisher.close();  
    }  

調用他們的代碼就是我們遍歷所有topic,然後通過sendMessage發送消息。在發送一個消息之後先sleep1秒鐘。要注意的一個地方就是我們使用完資源之後必須要使用close方法將這些資源關閉釋放。close方法關閉資源的具體實現如下:

    public void close() throws JMSException {  
        if (connection != null) {  
            connection.close();  
         }  
    }  

consumer

Consumer的代碼也很類似,具體的步驟無非就是1.初始化資源。 2. 接收消息。 3. 必要的時候關閉資源。
初始化資源可以放到構造函數裏面:

    public Consumer() throws JMSException {  
            factory = new ActiveMQConnectionFactory(brokerURL);  
            connection = factory.createConnection();  
            connection.start();  
            session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);  
        }  

接收和處理消息的方法有兩種,分爲同步和異步的,一般同步的方式我們是通過MessageConsumer.receive()方法來處理接收到的消息。而異步的方法則是通過註冊一個MessageListener的方法,使用MessageConsumer.setMessageListener()。這裏我們採用異步的方式實現:

    public static void main(String[] args) throws JMSException {  
        Consumer consumer = new Consumer();  
        for (String stock : args) {  
        Destination destination = consumer.getSession().createTopic("STOCKS." + stock);  
        MessageConsumer messageConsumer = consumer.getSession().createConsumer(destination);  
        messageConsumer.setMessageListener(new Listener());  
        }  
    }  

    public Session getSession() {  
        return session;  
    }  

在前面的代碼裏我們先找到同樣的topic,然後遍歷所有的topic去獲得消息。對於消息的處理我們專門通過Listener對象來負責。
Listener對象的職責很簡單,主要就是處理接收到的消息:

    public class Listener implements MessageListener {  

        public void onMessage(Message message) {  
            try {  
                MapMessage map = (MapMessage)message;  
                String stock = map.getString("stock");  
                double price = map.getDouble("price");  
                double offer = map.getDouble("offer");  
                boolean up = map.getBoolean("up");  
                DecimalFormat df = new DecimalFormat( "#,###,###,##0.00" );  
                System.out.println(stock + "\t" + df.format(price) + "\t" + df.format(offer) + "\t" + (up?"up":"down"));  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        }  

    }  

它實現了MessageListener接口,裏面的onMessage方法就是在接收到消息之後會被調用的方法。
現在,通過實現前面的publisherconsumer我們已經實現了pub-sub模式的一個實例。仔細回想它的步驟的話,主要就是要兩者設定一個共同的topic,有了這個topic之後他們可以實現一方發消息另外一方接收。另外,爲了連接到具體的message server,這裏是使用了連接tcp://localhost:16161作爲定義ActiveMQConnectionFactory的路徑。在publisher端通過session創建producer,根據指定的參數創建destination,然後將消息和destination作爲producer.send()方法的參數發消息。在consumer端也要創建類似的connection, session。通過session得到destination,再通過session.createConsumer(destination)來得到一個MessageConsumer對象。有了這個MessageConsumer我們就可以自行選擇是直接同步的receive消息還是註冊listener了。

p2p

p2p的過程則理解起來更加簡單。它好比是兩個人打電話,這兩個人是獨享這一條通信鏈路的。一方發送消息,另外一方接收,就這麼簡單。在實際應用中因爲有多個用戶對使用p2p的鏈路,它的通信場景如下圖所示:
這裏寫圖片描述
我們再來看看一個p2p的示例:
在p2p的場景裏,相互通信的雙方是通過一個類似於隊列的方式來進行交流。和前面pub-sub的區別在於一個topic有一個發送者和多個接收者,而在p2p裏一個queue只有一個發送者和一個接收者。

發送者

和前面的示例非常相似,我們構造函數裏需要初始化的內容基本上差不多:

    public Publisher() throws JMSException {  
        factory = new ActiveMQConnectionFactory(brokerURL);  
        connection = factory.createConnection();  
        connection.start();  
        session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);  
        producer = session.createProducer(null);  
    }  

發送消息的方法如下:

    public void sendMessage() throws JMSException {  
        for(int i = 0; i < jobs.length; i++)  
        {  
            String job = jobs[i];  
            Destination destination = session.createQueue("JOBS." + job);  
            Message message = session.createObjectMessage(i);  
            System.out.println("Sending: id: " + ((ObjectMessage)message).getObject() + " on queue: " + destination);  
            producer.send(destination, message);  
        }  
    }  

這裏我們定義了一個jobs的數組,通過遍歷這個數組來創建不同的job queue。這樣就相當於建立了多個點對點通信的鏈路。
消息發送者的啓動代碼如下:

    public static void main(String[] args) throws JMSException {  
        Publisher publisher = new Publisher();  
        for(int i = 0; i < 10; i++) {  
            publisher.sendMessage();  
            System.out.println("Published " + i + " job messages");  
        try {  
                Thread.sleep(1000);  
            } catch (InterruptedException x) {  
            e.printStackTrace();  
            }  
        }  
        publisher.close();  
    }  

我們在這裏發送10條消息,當然,在每個sendMessage的方法裏實際上是針對每個queue發送了10條。

接收者

接收者的代碼很簡單,一個構造函數初始化所有的資源:

    public Consumer() throws JMSException {  
            factory = new ActiveMQConnectionFactory(brokerURL);  
            connection = factory.createConnection();  
            connection.start();  
            session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);  
        }  

還有一個就是註冊消息處理的對象:

    public static void main(String[] args) throws JMSException {  
            Consumer consumer = new Consumer();  
            for (String job : consumer.jobs) {  
                Destination destination = consumer.getSession().createQueue("JOBS." + job);  
                MessageConsumer messageConsumer = consumer.getSession().createConsumer(destination);  
                messageConsumer.setMessageListener(new Listener(job));  
            }  
        }  

        public Session getSession() {  
            return session;  
        }  

具體註冊的對象處理方法和前面還是類似,實現MessageListener接口就可以了。


    import javax.jms.Message;  
    import javax.jms.MessageListener;  
    import javax.jms.ObjectMessage;  

    public class Listener implements MessageListener {  

        private String job;  

        public Listener(String job) {  
            this.job = job;  
        }  

        public void onMessage(Message message) {  
            try {  
                //do something here  
                System.out.println(job + " id:" + ((ObjectMessage)message).getObject());  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        }  

    }  

這裏代碼和前面pub-sub的具體實現代碼非常相似,就不再贅述。
現在如果我們比較一下pub-sub和p2p模式的具體實現步驟的話,我們會發現他們基本的處理流程都是類似的,除了在pub-sub中要通過createTopic來設置topic,而在p2p中要通過createQueue來創建通信隊列。他們之間存在着很多的重複之處,在具體的開發過程中,我們是否可以進行一些工程上的優化呢?別急,後面我們會討論到的。

request-response

和前面兩種方式比較起來,request-response的通信方式很常見,但是不是默認提供的一種模式。在前面的兩種模式中都是一方負責發送消息而另外一方負責處理。而我們實際中的很多應用相當於一種一應一答的過程,需要雙方都能給對方發送消息。於是請求-應答的這種通信方式也很重要。它也應用的很普遍。
請求-應答方式並不是JMS規範系統默認提供的一種通信方式,而是通過在現有通信方式的基礎上稍微運用一點技巧實現的。下圖是典型的請求-應答方式的交互過程:
這裏寫圖片描述
在JMS裏面,如果要實現請求/應答的方式,可以利用JMSReplyToJMSCorrelationID消息頭來將通信的雙方關聯起來。另外,QueueRequestorTopicRequestor能夠支持簡單的請求/應答過程。
現在,如果我們要實現這麼一個過程,在發送請求消息並且等待返回結果的client端的流程如下:

    // client side  
    Destination tempDest = session.createTemporaryQueue();  
    MessageConsumer responseConsumer = session.createConsumer(tempDest);  
    ...  

    // send a request..  
    message.setJMSReplyTo(tempDest)  
    message.setJMSCorrelationID(myCorrelationID);  

    producer.send(message);  

client端創建一個臨時隊列並在發送的消息裏指定了發送返回消息的destination以及correlationID。那麼在處理消息的server端得到這個消息後就知道該發送給誰了。Server端的大致流程如下:

    public void onMessage(Message request) {  

      Message response = session.createMessage();  
      response.setJMSCorrelationID(request.getJMSCorrelationID())  

      producer.send(request.getJMSReplyTo(), response)  
    }  

這裏我們是用server端註冊MessageListener,通過設置返回信息的CorrelationID和JMSReplyTo將信息返回。

以上就是發送和接收消息的雙方的大致程序結構。具體的實現代碼如下:
Client:

    public Client() {  
            ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");  
            Connection connection;  
            try {  
                connection = connectionFactory.createConnection();  
                connection.start();  
                Session session = connection.createSession(transacted, ackMode);  
                Destination adminQueue = session.createQueue(clientQueueName);  

                //Setup a message producer to send message to the queue the server is consuming from  
                this.producer = session.createProducer(adminQueue);  
                this.producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);  

                //Create a temporary queue that this client will listen for responses on then create a consumer  
                //that consumes message from this temporary queue...for a real application a client should reuse  
                //the same temp queue for each message to the server...one temp queue per client  
                Destination tempDest = session.createTemporaryQueue();  
                MessageConsumer responseConsumer = session.createConsumer(tempDest);  

                //This class will handle the messages to the temp queue as well  
                responseConsumer.setMessageListener(this);  

                //Now create the actual message you want to send  
                TextMessage txtMessage = session.createTextMessage();  
                txtMessage.setText("MyProtocolMessage");  

                //Set the reply to field to the temp queue you created above, this is the queue the server  
                //will respond to  
                txtMessage.setJMSReplyTo(tempDest);  

                //Set a correlation ID so when you get a response you know which sent message the response is for  
                //If there is never more than one outstanding message to the server then the  
                //same correlation ID can be used for all the messages...if there is more than one outstanding  
                //message to the server you would presumably want to associate the correlation ID with this  
                //message somehow...a Map works good  
                String correlationId = this.createRandomString();  
                txtMessage.setJMSCorrelationID(correlationId);  
                this.producer.send(txtMessage);  
            } catch (JMSException e) {  
                //Handle the exception appropriately  
            }  
        }  

這裏的代碼除了初始化構造函數裏的參數還同時設置了兩個destination,一個是自己要發送消息出去的destination,在session.createProducer(adminQueue);這一句設置。另外一個是自己要接收的消息destination, 通過Destination tempDest = session.createTemporaryQueue(); responseConsumer = session.createConsumer(tempDest); 這兩句指定了要接收消息的目的地。這裏是用的一個臨時隊列。在前面指定了返回消息的通信隊列之後,我們需要通知server端知道發送返回消息給哪個隊列。於是txtMessage.setJMSReplyTo(tempDest);指定了這一部分,同時txtMessage.setJMSCorrelationID(correlationId);方法主要是爲了保證每次發送回來請求的server端能夠知道對應的是哪個請求。這裏一個請求和一個應答是相當於對應一個相同的序列號一樣。
同時,因爲client端在發送消息之後還要接收server端返回的消息,所以它也要實現一個消息receiver的功能。這裏採用實現MessageListener接口的方式:


    public void onMessage(Message message) {  
            String messageText = null;  
            try {  
                if (message instanceof TextMessage) {  
                    TextMessage textMessage = (TextMessage) message;  
                    messageText = textMessage.getText();  
                    System.out.println("messageText = " + messageText);  
                }  
            } catch (JMSException e) {  
                //Handle the exception appropriately  
            }  
        }  

Server:

這裏server端要執行的過程和client端相反,它是先接收消息,在接收到消息後根據提供的JMSCorelationID來發送返回的消息:


    public void onMessage(Message message) {  
            try {  
                TextMessage response = this.session.createTextMessage();  
                if (message instanceof TextMessage) {  
                    TextMessage txtMsg = (TextMessage) message;  
                    String messageText = txtMsg.getText();  
                    response.setText(this.messageProtocol.handleProtocolMessage(messageText));  
                }  

                //Set the correlation ID from the received message to be the correlation id of the response message  
                //this lets the client identify which message this is a response to if it has more than  
                //one outstanding message to the server  
                response.setJMSCorrelationID(message.getJMSCorrelationID());  

                //Send the response to the Destination specified by the JMSReplyTo field of the received message,  
                //this is presumably a temporary queue created by the client  
                this.replyProducer.send(message.getJMSReplyTo(), response);  
            } catch (JMSException e) {  
                //Handle the exception appropriately  
            }  
        }  

前面,在replyProducer.send()方法裏,message.getJMSReplyTo()就得到了要發送消息回去的destination。

另外,設置這些發送返回信息的replyProducer的信息主要在構造函數相關的方法裏實現了:


    public Server() {  
            try {  
                //This message broker is embedded  
                BrokerService broker = new BrokerService();  
                broker.setPersistent(false);  
                broker.setUseJmx(false);  
                broker.addConnector(messageBrokerUrl);  
                broker.start();  
            } catch (Exception e) {  
                //Handle the exception appropriately  
            }  

            //Delegating the handling of messages to another class, instantiate it before setting up JMS so it  
            //is ready to handle messages  
            this.messageProtocol = new MessageProtocol();  
            this.setupMessageQueueConsumer();  
        }  

        private void setupMessageQueueConsumer() {  
            ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(messageBrokerUrl);  
            Connection connection;  
            try {  
                connection = connectionFactory.createConnection();  
                connection.start();  
                this.session = connection.createSession(this.transacted, ackMode);  
                Destination adminQueue = this.session.createQueue(messageQueueName);  

                //Setup a message producer to respond to messages from clients, we will get the destination  
                //to send to from the JMSReplyTo header field from a Message  
                this.replyProducer = this.session.createProducer(null);  
                this.replyProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);  

                //Set up a consumer to consume messages off of the admin queue  
                MessageConsumer consumer = this.session.createConsumer(adminQueue);  
                consumer.setMessageListener(this);  
            } catch (JMSException e) {  
                //Handle the exception appropriately  
            }  
        }  

總體來說,整個的交互過程並不複雜,只是比較繁瑣。對於請求/應答的方式來說,這種典型交互的過程就是Client端在設定正常發送請求的Queue同時也設定一個臨時的Queue。同時在要發送的message裏頭指定要返回消息的destination以及CorelationID,這些就好比是一封信裏面所帶的回執。根據這個信息人家才知道怎麼給你回信。對於Server端來說則要額外創建一個producer,在處理接收到消息的方法裏再利用producer將消息發回去。這一系列的過程看起來很像http協議裏面請求-應答的方式,都是一問一答。
一些應用和改進

回顧前面三種基本的通信方式,我們會發現,他們都存在着一定的共同點,比如說都要初始化ConnectionFactory, Connection, Session等。在使用完之後都要將這些資源關閉。如果每一個實現它的通信端都這麼寫一通的話,其實是一種簡單的重複。從工程的角度來看是完全沒有必要的。那麼,我們有什麼辦法可以減少這種重複呢?

一種簡單的方式就是通過工廠方法封裝這些對象的創建和銷燬,然後簡單的通過調用工廠方法的方式得到他們。另外,既然基本的流程都是在開頭創建資源在結尾銷燬,我們也可以採用Template Method模式的思路。通過繼承一個抽象類,在抽象類裏提供了資源的封裝。所有繼承的類只要實現怎麼去使用這些資源的方法就可以了。Spring中間的JMSTemplate就提供了這種類似思想的封裝。具體的實現可以參考這篇文章。
總結

activemq默認提供了pub-sub, p2p這兩種通信的方式。同時也提供了一些對request-response方式的支持。實際上,不僅僅是activemq,對於所有其他實現JMS規範的產品都能夠提供類似的功能。這裏每種方式都不太複雜,主要是創建和管理資源的步驟顯得比較繁瑣。

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