ActiveMQ持久化方式

五柳-先生發表於2015-06-30

訊息永續性對於可靠訊息傳遞來說應該是一種比較好的方法,有了訊息持久化,即使傳送者和接受者不是同時線上或者訊息中心在傳送者傳送訊息後當機了,在訊息中心重新啟動後仍然可以將訊息傳送出去,如果把這種持久化和ReliableMessaging結合起來應該是很好的保證了訊息的可靠傳送。

訊息永續性的原理很簡單,就是在傳送者將訊息傳送出去後,訊息中心首先將訊息儲存到本地資料檔案、記憶體資料庫或者遠端資料庫等,然後試圖將訊息傳送給接收者,傳送成功則將訊息從儲存中刪除,失敗則繼續嘗試。訊息中心啟動以後首先要檢查制定的儲存位置,如果有未傳送成功的訊息,則需要把訊息傳送出去。

ActiveMQ持久化方式:AMQ、KahaDB、JDBC、LevelDB。

1、AMQ

AMQ是一種檔案儲存形式,它具有寫入速度快和容易恢復的特點。訊息儲存在一個個檔案中,檔案的預設大小為32M,如果一條訊息的大小超過了32M,那麼這個值必須設定大一點。當一個儲存檔案中的訊息已經全部被消費,那麼這個檔案將被標識為可刪除,在下一個清除階段,這個檔案被刪除。AMQ適用於ActiveMQ5.3之前的版本。預設配置如下:

  1. <persistenceAdapter>  
  2.    <amqPersistenceAdapter directory="activemq-data"maxFileLength="32mb"/>  
  3. </persistenceAdapter>  

屬性如下:

屬性名稱

預設值

描述

directory

activemq-data

訊息檔案和日誌的儲存目錄

useNIO

true

使用NIO協議儲存訊息

syncOnWrite

false

同步寫到磁碟,這個選項對效能影響非常大

maxFileLength

32Mb

一個訊息檔案的大小

persistentIndex

true

訊息索引的持久化,如果為false,那麼索引儲存在記憶體中

maxCheckpointMessageAddSize

4kb

一個事務允許的最大訊息量

cleanupInterval

30000

清除操作週期,單位ms

indexBinSize

1024

索引檔案快取頁面數,預設為1024,當amq擴充或者縮減儲存時,會鎖定整個broker,導致一定時間的阻塞,所以這個值應該調整到比較大,但是程式碼中實現會動態伸縮,調整效果並不理想。

indexKeySize

96

索引key的大小,key是訊息ID

indexPageSize

16kb

索引的頁大小

directoryArchive

archive

儲存被歸檔的訊息檔案目錄

archiveDataLogs

false

當為true時,歸檔的訊息檔案被移到directoryArchive,而不是直接刪除                    

2、KahaDB

KahaDB是基於檔案的本地資料庫儲存形式,雖然沒有AMQ的速度快,但是它具有強擴充套件性,恢復的時間比AMQ短,從5.4版本之後KahaDB做為預設的持久化方式。預設配置如下:

  1. <persistenceAdapter>  
  2.    <kahaDB directory="activemq-data"journalMaxFileLength="32mb"/>  
  3. </persistenceAdapter>  

KahaDB的屬性如下:

屬性名稱

預設值

描述

directory

activemq-data

訊息檔案和日誌的儲存目錄

indexWriteBatchSize

1000

一批索引的大小,當要更新的索引量到達這個值時,更新到訊息檔案中

indexCacheSize

10000

記憶體中,索引的頁大小

enableIndexWriteAsync

false

索引是否非同步寫到訊息檔案中

journalMaxFileLength

32mb

一個訊息檔案的大小

enableJournalDiskSyncs

true

是否講非事務的訊息同步寫入到磁碟

cleanupInterval

30000

清除操作週期,單位ms

checkpointInterval

5000

索引寫入到訊息檔案的週期,單位ms

ignoreMissingJournalfiles

false

忽略丟失的訊息檔案,false,當丟失了訊息檔案,啟動異常

checkForCorruptJournalFiles

false

檢查訊息檔案是否損壞,true,檢查發現損壞會嘗試修復

checksumJournalFiles

false

產生一個checksum,以便能夠檢測journal檔案是否損壞。

5.4版本之後有效的屬性:

 

 

archiveDataLogs

false

當為true時,歸檔的訊息檔案被移到directoryArchive,而不是直接刪除

directoryArchive

null

儲存被歸檔的訊息檔案目錄

databaseLockedWaitDelay

10000

在使用負載時,等待獲得檔案鎖的延遲時間,單位ms

maxAsyncJobs

10000

同個生產者產生等待寫入的非同步訊息最大量

concurrentStoreAndDispatchTopics

false

當寫入訊息的時候,是否轉發主題訊息

concurrentStoreAndDispatchQueues

true

當寫入訊息的時候,是否轉發佇列訊息

5.6版本之後有效的屬性:

 

 

archiveCorruptedIndex

false

是否歸檔錯誤的索引

每個KahaDB的例項都可以配置單獨的介面卡,如果沒有目標佇列提交給filteredKahaDB,那麼意味著對所有的佇列有效。如果一個佇列沒有對應的介面卡,那麼將會丟擲一個異常。配置如下:

  1. <persistenceAdapter>  
  2.   <mKahaDBdirectorymKahaDBdirectory="${activemq.base}/data/kahadb">  
  3.     <filteredPersistenceAdapters>  
  4.       <!-- match all queues -->  
  5.       <filteredKahaDBqueuefilteredKahaDBqueue=">">  
  6.         <persistenceAdapter>  
  7.           <kahaDBjournalMaxFileLengthkahaDBjournalMaxFileLength="32mb"/>  
  8.         </persistenceAdapter>  
  9.       </filteredKahaDB>  
  10.        
  11.       <!-- match all destinations -->  
  12.       <filteredKahaDB>  
  13.         <persistenceAdapter>  
  14.           <kahaDBenableJournalDiskSyncskahaDBenableJournalDiskSyncs="false"/>  
  15.         </persistenceAdapter>  
  16.       </filteredKahaDB>  
  17.     </filteredPersistenceAdapters>  
  18.   </mKahaDB>  
  19. </persistenceAdapter>  

如果filteredKahaDB的perDestination屬性設定為true,那麼匹配的目標佇列將會得到自己對應的KahaDB例項。配置如下:

  1. <persistenceAdapter>  
  2.   <mKahaDBdirectorymKahaDBdirectory="${activemq.base}/data/kahadb">  
  3.     <filteredPersistenceAdapters>  
  4.       <!-- kahaDB per destinations -->  
  5.       <filteredKahaDB perDestination="true">  
  6.         <persistenceAdapter>  
  7.           <kahaDBjournalMaxFileLengthkahaDBjournalMaxFileLength="32mb" />  
  8.         </persistenceAdapter>  
  9.       </filteredKahaDB>  
  10.     </filteredPersistenceAdapters>  
  11.   </mKahaDB>  
  12. </persistenceAdapter>  

3、JDBC

可以將訊息儲存到資料庫中,例如:Mysql、SQL Server、Oracle、DB2。

配置JDBC介面卡:

  1. <persistenceAdapter>  
  2.     <jdbcPersistenceAdapterdataSourcejdbcPersistenceAdapterdataSource="#mysql-ds" createTablesOnStartup="false" />  
  3. </persistenceAdapter>  

dataSource指定持久化資料庫的bean,createTablesOnStartup是否在啟動的時候建立資料表,預設值是true,這樣每次啟動都會去建立資料表了,一般是第一次啟動的時候設定為true,之後改成false。

  1. Mysql持久化bean:  
  2. <bean id="mysql-ds" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">  
  3.     <property name="driverClassName" value="com.mysql.jdbc.Driver"/>  
  4.     <property name="url" value="jdbc:mysql://localhost/activemq?relaxAutoCommit=true"/>  
  5.     <property name="username" value="activemq"/>  
  6.     <property name="password" value="activemq"/>  
  7.     <property name="poolPreparedStatements" value="true"/>  
  8. </bean>  
  9. SQL Server持久化bean:  
  10. <bean id="mssql-ds" class="net.sourceforge.jtds.jdbcx.JtdsDataSource" destroy-method="close">  
  11.    <property name="serverName" value="SERVERNAME"/>  
  12.    <property name="portNumber" value="PORTNUMBER"/>  
  13.    <property name="databaseName" value="DATABASENAME"/>  
  14.    <property name="user" value="USER"/>  
  15.    <property name="password" value="PASSWORD"/>  
  16. </bean>  
  17. Oracle持久化bean:  
  18. <bean id="oracle-ds" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">  
  19.     <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>  
  20.     <property name="url" value="jdbc:oracle:thin:@10.53.132.47:1521:activemq"/>  
  21.     <property name="username" value="activemq"/>  
  22.     <property name="password" value="activemq"/>  
  23.     <property name="maxActive" value="200"/>  
  24.     <property name="poolPreparedStatements" value="true"/>  
  25. </bean>  
  26. DB2持久化bean:  
  27. <bean id="db2-ds" class="org.apache.commons.dbcp.BasicDataSource"  destroy-method="close">  
  28.       <property name="driverClassName" value="com.ibm.db2.jcc.DB2Driver"/>  
  29.       <property name="url" value="jdbc:db2://hndb02.bf.ctc.com:50002/activemq"/>  
  30.       <property name="username" value="activemq"/>  
  31.       <property name="password" value="activemq"/>  
  32.       <property name="maxActive" value="200"/>  
  33.       <property name="poolPreparedStatements" value="true"/>  
  34.   </bean>  

4、LevelDB

這種檔案系統是從ActiveMQ5.8之後引進的,它和KahaDB非常相似,也是基於檔案的本地資料庫儲存形式,但是它提供比KahaDB更快的永續性。與KahaDB不同的是,它不是使用傳統的B-樹來實現對日誌資料的提前寫,而是使用基於索引的LevelDB。

預設配置如下:

  1. <persistenceAdapter>  
  2.       <levelDBdirectorylevelDBdirectory="activemq-data"/>  
  3. </persistenceAdapter>  

屬性如下:

屬性名稱

預設值

描述

directory

"LevelDB"

資料檔案的儲存目錄

readThreads

10

系統允許的併發讀執行緒數量

sync

true

同步寫到磁碟

logSize

104857600 (100 MB)

日誌檔案大小的最大值

logWriteBufferSize

4194304 (4 MB)

日誌資料寫入檔案系統的最大快取值

verifyChecksums

false

是否對從檔案系統中讀取的資料進行校驗

paranoidChecks

false

儘快對系統內部發生的儲存錯誤進行標記

indexFactory

org.fusesource.leveldbjni.JniDBFactory, org.iq80.leveldb.impl.Iq80DBFactory

在建立LevelDB索引時使用

indexMaxOpenFiles

1000

可供索引使用的開啟檔案的數量

indexBlockRestartInterval

16

Number keys between restart points for delta encoding of keys.

indexWriteBufferSize

6291456 (6 MB)

記憶體中索引資料的最大值

indexBlockSize

4096 (4 K)

每個資料塊的索引資料大小

indexCacheSize

268435456 (256 MB)

使用快取索引塊允許的最大記憶體

indexCompression

snappy

適用於索引塊的壓縮型別

logCompression

none

適用於日誌記錄的壓縮型別

5、  下面詳細介紹一下如何將訊息持久化到Mysql資料庫中

Ø        需要將mysql的驅動包放置到ActiveMQ的lib目錄下

Ø        修改activeMQ的配置檔案:

  1. <persistenceAdapter>  
  2. <jdbcPersistenceAdapter dataDirectory="${activemq.base}/data" dataSource="#mysql-ds"createTablesOnStartup="false"/>  
  3. </persistenceAdapter>  

在配置檔案中的broker節點外增加:

  1. <beanidbeanid="mysql-ds"class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">  
  2.       <propertynamepropertyname="driverClassName" value="com.mysql.jdbc.Driver"/>  
  3.       <property name="url"value="jdbc:mysql://localhost:3306/activemq?relaxAutoCommit=true"/>  
  4.       <property name="username"value="root"/>  
  5.       <property name="password" value="root"/>  
  6.       <property name="maxActive"value="200"/>  
  7.       <propertynamepropertyname="poolPreparedStatements" value="true"/>  
  8. </bean>  

從配置中可以看出資料庫的名稱是activemq,需要手動在MySql中建立這個資料庫。

然後重新啟動activeMQ,會發現activemq多了三張表:

1activemq_acks

2activemq_lock

3activemq_msgs

Ø        點到點型別

Sender類:

  1. import javax.jms.Connection;  
  2. import javax.jms.ConnectionFactory;  
  3. import javax.jms.DeliveryMode;  
  4. import javax.jms.Destination;  
  5. import javax.jms.JMSException;  
  6. import javax.jms.MessageProducer;  
  7. import javax.jms.Session;  
  8. import javax.jms.TextMessage;  
  9. import org.apache.activemq.ActiveMQConnection;  
  10. import org.apache.activemq.ActiveMQConnectionFactory;  
  11. public class Sender {  
  12. private static final int SEND_NUMBER = 2000;  
  13.     public static void main(String[] args) {  
  14.        // ConnectionFactory :連線工廠,JMS用它建立連線  
  15.        ConnectionFactory connectionFactory;  
  16.        // Connection :JMS客戶端到JMS Provider的連線  
  17.        Connection connection = null;  
  18.         // Session:一個傳送或接收訊息的執行緒  
  19.        Session session;  
  20.        // Destination :訊息的目的地;訊息傳送給誰.  
  21.        Destination destination;  
  22.        // MessageProducer:訊息傳送者  
  23.        MessageProducer producer;  
  24.         // TextMessage message;  
  25.         // 構造ConnectionFactory例項物件,此處採用ActiveMq的實現  
  26.        connectionFactory = new ActiveMQConnectionFactory(  
  27.               ActiveMQConnection.DEFAULT_USER,  
  28.               ActiveMQConnection.DEFAULT_PASSWORD,  
  29.               "tcp://localhost:61616");  
  30.        try{  
  31.            // 構造從工廠得到連線物件  
  32.            connection = connectionFactory.createConnection();  
  33.            //啟動  
  34.            connection.start();  
  35.            //獲取操作連線  
  36.            session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);  
  37.            //獲取session,FirstQueue是一個伺服器的queue                destination = session.createQueue("FirstQueue");  
  38.            // 得到訊息生成者【傳送者】  
  39.            producer = session.createProducer(destination);  
  40.            //設定不持久化  
  41.            producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);  
  42.            //構造訊息  
  43.            sendMessage(session, producer);  
  44.            //session.commit();  
  45.            connection.close();  
  46.        }  
  47.        catch(Exception e){  
  48.            e.printStackTrace();  
  49.        }finally{  
  50.            if(null != connection){  
  51.               try {  
  52.                   connection.close();  
  53.               } catch (JMSException e) {  
  54.                   // TODO Auto-generatedcatch block  
  55.                   e.printStackTrace();  
  56.               }  
  57.            }      
  58.        }  
  59.     }  
  60.     public static void sendMessage(Session session, MessageProducer producer)throws Exception{  
  61.        for(int i=1; i<=SEND_NUMBER; i++){  
  62.            TextMessage message = session.createTextMessage("ActiveMQ傳送訊息"+i);  
  63.            System.out.println("傳送訊息:ActiveMQ傳送的訊息"+i);  
  64.            producer.send(message);  
  65.        }  
  66.     }  
  67. }  

Receiver類:

  1. import javax.jms.Connection;  
  2. import javax.jms.ConnectionFactory;  
  3. import javax.jms.Destination;  
  4. import javax.jms.MessageConsumer;  
  5. import javax.jms.Session;  
  6. import javax.jms.TextMessage;  
  7. import org.apache.activemq.ActiveMQConnection;  
  8. import org.apache.activemq.ActiveMQConnectionFactory;  
  9. public class Receiver {  
  10.     public static void main(String[] args) {  
  11.        // ConnectionFactory :連線工廠,JMS用它建立連線  
  12.         ConnectionFactory connectionFactory;  
  13.         // Connection :JMS客戶端到JMS Provider的連線  
  14.         Connection connection = null;  
  15.         // Session:一個傳送或接收訊息的執行緒  
  16.         Session session;  
  17.         // Destination :訊息的目的地;訊息傳送給誰.  
  18.         Destination destination;  
  19.         // 消費者,訊息接收者  
  20.         MessageConsumer consumer;  
  21.         connectionFactory = newActiveMQConnectionFactory(  
  22.                 ActiveMQConnection.DEFAULT_USER,  
  23.                 ActiveMQConnection.DEFAULT_PASSWORD,  
  24.                 "tcp://localhost:61616");  
  25.         try {  
  26.             //得到連線物件  
  27.             connection =connectionFactory.createConnection();  
  28.             // 啟動  
  29.             connection.start();  
  30.             // 獲取操作連線  
  31.             session = connection.createSession(false,  
  32.                     Session.AUTO_ACKNOWLEDGE);  
  33.             // 建立Queue  
  34.            destination = session.createQueue("FirstQueue");  
  35.             consumer =session.createConsumer(destination);          
  36.             while(true){  
  37.               //設定接收者接收訊息的時間,為了便於測試,這裡定為100s  
  38.               TextMessagemessage = (TextMessage)consumer.receive(100000);  
  39.               if(null != message){  
  40.                  System.out.println("收到訊息" +message.getText());  
  41.               }else break;  
  42.             }  
  43.         }catch(Exception e){  
  44.         e.printStackTrace();  
  45.         }finally {  
  46.             try {  
  47.                 if (null != connection)  
  48.                     connection.close();  
  49.             } catch (Throwable ignore) {  
  50.             }  
  51.         }  
  52.     }  
  53. }  

測試:

測試一:

A、 先執行Sender類,待執行完畢後,執行Receiver類

B、 在此過程中activemq資料庫的activemq_msgs表中沒有資料

C、 再次執行Receiver,消費不到任何資訊

測試二:

A、  先執行Sender類

B、 重啟電腦

C、 執行Receiver類,無任何資訊被消費

測試三:

A、   把Sender類中的producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);改為producer.setDeliveryMode(DeliveryMode.PERSISTENT);

B、   先執行Sender類,待執行完畢後,執行Receiver類

C、   在此過程中activemq資料庫的activemq_msgs表中有資料生成,執行完Receiver類後,資料清除

測試四:

A、    把Sender類中的producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);改為producer.setDeliveryMode(DeliveryMode.PERSISTENT);

B、    執行Sender類

C、    重啟電腦

D、    執行Receiver類,有訊息被消費

結論:   

通過以上測試,可以發現,在P2P型別中當DeliveryMode設定為NON_PERSISTENCE時,訊息被儲存在記憶體中,而當DeliveryMode設定為PERSISTENCE時,訊息儲存在broker的相應的檔案或者資料庫中。而且P2P中訊息一旦被Consumer消費就從broker中刪除。

Ø        釋出/訂閱型別

Sender類:

  1. import javax.jms.Connection;  
  2. import javax.jms.ConnectionFactory;  
  3. import javax.jms.DeliveryMode;  
  4. import javax.jms.Destination;  
  5. import javax.jms.JMSException;  
  6. import javax.jms.MessageProducer;  
  7. import javax.jms.Session;  
  8. import javax.jms.TextMessage;  
  9. import javax.jms.Topic;  
  10. import org.apache.activemq.ActiveMQConnection;  
  11. import org.apache.activemq.ActiveMQConnectionFactory;  
  12. public class Sender {  
  13.     private static final int SEND_NUMBER = 100;  
  14.     public static void main(String[] args) {  
  15.        // ConnectionFactory :連線工廠,JMS用它建立連線  
  16.        ConnectionFactory connectionFactory;  
  17.        // Connection :JMS客戶端到JMS Provider的連線  
  18.        Connection connection = null;  
  19.         // Session:一個傳送或接收訊息的執行緒  
  20.        Session session;  
  21.        // MessageProducer:訊息傳送者  
  22.        MessageProducer producer;  
  23.         // TextMessage message;  
  24.         // 構造ConnectionFactory例項物件,此處採用ActiveMq的實現  
  25.        connectionFactory = new ActiveMQConnectionFactory(  
  26.               ActiveMQConnection.DEFAULT_USER,  
  27.                ActiveMQConnection.DEFAULT_PASSWORD,  
  28.               "tcp://localhost:61616");  
  29.        try{  
  30.            //得到連線物件  
  31.            connection = connectionFactory.createConnection();  
  32.            //啟動  
  33.            connection.start();  
  34.            //獲取操作連線  
  35.            session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);          
  36.            Topic topic = session.createTopic("MQ_test");         
  37.            // 得到訊息生成者【傳送者】  
  38.            producer = session.createProducer(topic);  
  39.            //設定持久化  
  40.            producer.setDeliveryMode(DeliveryMode.PERSISTENT);  
  41.            //構造訊息  
  42.            sendMessage(session, producer);  
  43.            //session.commit();  
  44.            connection.close();  
  45.        }  
  46.        catch(Exception e){  
  47.            e.printStackTrace();  
  48.        }finally{  
  49.            if(null != connection){  
  50.               try {  
  51.                   connection.close();  
  52.               } catch (JMSException e) {  
  53.                   // TODO Auto-generatedcatch block  
  54.                   e.printStackTrace();  
  55.               }  
  56.            }      
  57.        }  
  58.     }  
  59.     public static void sendMessage(Session session, MessageProducer producer)throws Exception{  
  60.        for(int i=1; i<=SEND_NUMBER; i++){  
  61.            TextMessage message = session.createTextMessage("ActiveMQ傳送訊息"+i);  
  62.            System.out.println("傳送訊息:ActiveMQ傳送的訊息"+i);  
  63.            producer.send(message);  
  64.        }  
  65.     }  
  66. }  

Receiver類:

  1. import javax.jms.Connection;  
  2. import javax.jms.ConnectionFactory;  
  3. import javax.jms.Destination;  
  4. import javax.jms.MessageConsumer;  
  5. import javax.jms.Session;  
  6. import javax.jms.TextMessage;  
  7. import javax.jms.Topic;  
  8.    
  9. import org.apache.activemq.ActiveMQConnection;  
  10. import org.apache.activemq.ActiveMQConnectionFactory;  
  11. public class Receiver {  
  12.     public static void main(String[] args) {  
  13.        // ConnectionFactory :連線工廠,JMS用它建立連線  
  14.         ConnectionFactory connectionFactory;  
  15.         // Connection :JMS客戶端到JMS Provider的連線  
  16.         Connection connection = null;  
  17.         // Session:一個傳送或接收訊息的執行緒  
  18.         Session session;   
  19.         // 消費者,訊息接收者  
  20.         MessageConsumer consumer;  
  21.         connectionFactory = newActiveMQConnectionFactory(  
  22.                ActiveMQConnection.DEFAULT_USER,  
  23.                 ActiveMQConnection.DEFAULT_PASSWORD,  
  24.                 "tcp://localhost:61616");  
  25.         try {  
  26.             // 構造從工廠得到連線物件  
  27.             connection =connectionFactory.createConnection();  
  28.               
  29.             connection.setClientID("clientID001");  
  30.             // 啟動  
  31.             connection.start();  
  32.             // 獲取操作連線  
  33.             session = connection.createSession(false,  
  34.                     Session.AUTO_ACKNOWLEDGE);  
  35.             // 獲取session  
  36.            Topic topic = session.createTopic("MQ_test");         
  37.            // 得到訊息生成者【傳送者】  
  38.            consumer = session.createDurableSubscriber(topic, "MQ_sub");  
  39.              
  40.             while(true){  
  41.               //設定接收者接收訊息的時間,為了便於測試,這裡誰定為100s  
  42.               TextMessagemessage = (TextMessage)consumer.receive(100000);  
  43.               if(null != message){  
  44.                  System.out.println("收到訊息" +message.getText());  
  45.               }else break;  
  46.             }  
  47.         }catch(Exception e){  
  48.         e.printStackTrace();  
  49.         }finally {  
  50.             try {  
  51.                 if (null != connection)  
  52.                     connection.close();  
  53.             } catch (Throwable ignore) {  
  54.             }  
  55.         }  
  56.     }  
  57.    
  58. }  

測試:

測試一:

A、先啟動Sender類

B、再啟動Receiver類

C、結果無任何記錄被訂閱

測試二:

A、先啟動Receiver類,讓Receiver在相關主題上進行訂閱

B、停止Receiver類,再啟動Sender類

C、待Sender類執行完成後,再啟動Receiver類

D、結果發現相應主題的資訊被訂閱

原文地址: http://blog.csdn.net/xyw_blog/article/details/9128219

相關文章