activemq的兩種基本通訊方式的使用及總結

Franson發表於2016-06-15

簡介

     在前面一篇文章裡討論過幾種應用系統整合的方式,發現實際上面向訊息佇列的整合方案算是一個總體比較合理的選擇。這裡,我們先針對具體的一個訊息佇列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對多的關係。這種關係如下圖所示:

     

   

p2p

    p2p的過程則理解起來更加簡單。它好比是兩個人打電話,這兩個人是獨享這一條通訊鏈路的。一方傳送訊息,另外一方接收,就這麼簡單。在實際應用中因為有多個使用者對使用p2p的鏈路,它的通訊場景如下圖所示:

     在p2p的場景裡,相互通訊的雙方是通過一個類似於佇列的方式來進行交流。和前面pub-sub的區別在於一個topic有一個傳送者和多個接收者,而在p2p裡一個queue只有一個傳送者和一個接收者。

這兩種通訊模式的程式碼實現有很多相同之處,下面我們用一個例子來簡單實現這兩種通訊方式:

傳送者 

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.util.StringTokenizer;

import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;

import org.apache.activemq.ActiveMQConnectionFactory;

public class Publisher {
    public static final String url = "tcp://localhost:61616";// 預設埠,如果要改,可在apache-activemq-5.13.3\conf中的activemq.xml中更改埠號
    ConnectionFactory factory;
    Connection connection;
    Session session;
    MessageProducer producer;
    Destination[] destinations;
    ComunicateMode comunicateMode = ComunicateMode.pubsub;

    enum ComunicateMode {
        p2p, pubsub
    }

    public Publisher(ComunicateMode mode) throws JMSException {
        this.comunicateMode = mode;
        factory = new ActiveMQConnectionFactory(url);// 這裡的url也可以不指定,java程式碼將預設將埠賦值為61616
        connection = factory.createConnection();
        try {
            connection.start();
        } catch (JMSException e) {
            connection.close();
            throw e;
        }
        session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        producer = session.createProducer(null);
    }

    protected void setDestinations(String[] stocks) throws JMSException {
        destinations = new Destination[stocks.length];
        for (int i = 0; i < stocks.length; i++) {
            destinations[i] = comunicateMode == ComunicateMode.pubsub ? session
                    .createTopic("Topic." + stocks[i]) : session
                    .createQueue("Queue." + stocks[i]);
        }
    }

    protected void sendMessage(String msg) throws JMSException {
        for (Destination item : destinations) {
            TextMessage msgMessage = session.createTextMessage(msg);
            producer.send(item, msgMessage);
            System.out.println(String.format("成功向Topic為【%s】傳送訊息【%s】",
                    item.toString(), msgMessage.getText()));
        }
    }

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

    public static void main(String[] args) throws JMSException,
            InterruptedException, IOException {
        Publisher publisher = new Publisher(ComunicateMode.p2p);// 這裡可以修改訊息傳輸方式為pubsub
        publisher.setDestinations(new String[] { "1", "2", "3" });
        BufferedReader reader = null;
        String contentString = "";
        do {
            System.out.println("請輸入要傳送的內容(exit退出):");
            reader = new BufferedReader(new InputStreamReader(System.in));
            contentString = reader.readLine();
            if (contentString.equals("exit"))
                break;
            publisher.sendMessage(contentString);
        } while (!contentString.equals("exit"));
        reader.close();
        publisher.close();
    }
}
 

接收者

    

import java.io.IOException;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.Session;
import javax.jms.TextMessage;

import org.apache.activemq.ActiveMQConnectionFactory;

public class Consumer {
    public static final String url = "tcp://localhost:61616";// 預設埠,如果要改,可在apache-activemq-5.13.3\conf中的activemq.xml中更改埠號
    ConnectionFactory factory;
    Connection connection;
    Session session;
    MessageConsumer[] consumers;
    ComunicateMode comunicateMode = ComunicateMode.pubsub;

    enum ComunicateMode {
        p2p, pubsub
    }

    public Consumer(ComunicateMode mode, String[] destinationNames)
            throws JMSException {
        this.comunicateMode = mode;
        factory = new ActiveMQConnectionFactory(url);// 這裡的url也可以不指定,java程式碼將預設將埠賦值為61616
        connection = factory.createConnection();
        connection.start();
        session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        consumers = new MessageConsumer[destinationNames.length];
        for (int i = 0; i < destinationNames.length; i++) {
            Destination destination = comunicateMode == ComunicateMode.pubsub ? session
                    .createTopic("Topic." + destinationNames[i]) : session
                    .createQueue("Queue." + destinationNames[i]);
            consumers[i] = session.createConsumer(destination);
            consumers[i].setMessageListener(new MessageListener() {
                @Override
                public void onMessage(Message message) {
                    try {
                        System.out.println(String.format("收到訊息【%s】",
                                ((TextMessage) message).getText()));
                    } catch (JMSException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

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

    public static void main(String[] args) throws JMSException, IOException {
        Consumer consumer = new Consumer(ComunicateMode.p2p,
                new String[] { "2" });// 這裡可以修改訊息傳輸方式為pubsub
        System.in.read();
        consumer.close();
    }

}

 

 

 

request-response

    和前面兩種方式比較起來,request-response的通訊方式很常見,但是不是預設提供的一種模式。在前面的兩種模式中都是一方負責傳送訊息而另外一方負責處理。而我們實際中的很多應用相當於一種一應一答的過程,需要雙方都能給對方傳送訊息。於是請求-應答的這種通訊方式也很重要。它也應用的很普遍。 

     請求-應答方式並不是JMS規範系統預設提供的一種通訊方式,而是通過在現有通訊方式的基礎上稍微運用一點技巧實現的。下圖是典型的請求-應答方式的互動過程:

     在JMS裡面,如果要實現請求/應答的方式,可以利用JMSReplyTo和JMSCorrelationID訊息頭來將通訊的雙方關聯起來。另外,QueueRequestor和TopicRequestor能夠支援簡單的請求/應答過程。

    現在,如果我們要實現這麼一個過程,在傳送請求訊息並且等待返回結果的client端的流程如下:

Java程式碼  收藏程式碼
 
// 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端的大致流程如下:

 

Java程式碼  收藏程式碼
 
public void onMessage(Message request) {

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

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

 

    這裡我們是用server端註冊MessageListener,通過設定返回資訊的CorrelationID和JMSReplyTo將資訊返回。

    以上就是傳送和接收訊息的雙方的大致程式結構。具體的實現程式碼如下:

 Client:

Java程式碼  收藏程式碼
 
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介面的方式:

Java程式碼  收藏程式碼

 

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來傳送返回的訊息:

Java程式碼  收藏程式碼
 
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的資訊主要在建構函式相關的方法裡實現了:

Java程式碼  
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規範的產品都能夠提供類似的功能。這裡每種方式都不太複雜,主要是建立和管理資源的步驟顯得比較繁瑣。

相關文章