天天看點

【翻譯】DotNetMQ: 一個.NET版完整的消息隊列系統

在一個大型的分布式系統中,消息隊列是不可缺少的中間件,能很好的解決異步消息、應用解耦、均衡并發等問題。在.net中,偶然發現一個效率不錯、安全可靠、功能齊全的消息元件,忍不住翻譯過來,供大家快速預覽。

注:原作者用windows服務啟動消息隊列服務,但是本人在win10上測試出錯,可自行改成控制台啟動消息隊列服務,然後用第三方工具注冊服務(如:SrvanyUI)

原文:http://www.codeproject.com/Articles/193611/DotNetMQ-A-Complete-Message-Queue-System-for-NET

正文: 

 一個新的、獨立的、開源的,完全基于C#和.NET Framework3.5的消息隊列系統

下載下傳源代碼 - 1.28 MB

下載下傳二進制檔案 - 933 KB

下載下傳例子 - 534 KB

文章概要

  • 介紹
  • 什麼是消息傳遞?
  • 什麼是DotNetMQ?
  • 為什麼要一個新的消息代理?
    • 消息代理的必要性
    • 現有的消息代理
  • 安裝、運作DotNetMQ
  • 第一個DotNetMQ程式
    • 注冊應用程式到DotNetMQ
    • 開發Application1
    • 開發Application2
    • 消息屬性:傳送規則(Transmit Rule)
    • 用戶端屬性:通訊方式(CommunicationWay)
    • 用戶端屬性:出錯時重新連接配接伺服器(ReConnectServerOnError)
    • 用戶端屬性:自動确認消息(AutoAcknowledgeMessages)
  • 配置DotNetMQ
    • 服務端
    • 應用程式
    • 路由/負載均衡
    • 其他設定
  • 網絡傳輸消息
    • 一個簡單的應用程式
    • 一個真實的案例:分布式短信處理器(Distributed SMS Processor)
  • 請求/應答式通信
  • 面向服務架構的DotNetMQ
    • 簡單應用程式:短息/郵件發送器
      • 用戶端
    • Web服務支援
  • DotNetMQ性能
  • 曆史
  • 引用

在這篇文章中,我将介紹一個新的、獨立的、開源的,完全基于C#和.NET Framework3.5的消息隊列系統,DotNetMQ是一個消息代理,它包括確定傳輸,路由,負載均衡,伺服器圖等等多項功能。我将從解釋消息的概念和消息代理的必要性講起,然後,我會說明什麼是DotNetMQ,以及如何使用它。

什麼是消息傳遞

消息傳遞是一種異步通信方式,具體就是在同一個或不同的機器上運作的多個應用程式之間可靠的消息傳遞。應用程式通過發送一種叫消息的資料包和其他應用程式通信。

一個消息可以是一個字元串,一個位元組數組,一個對象等等。通常情況下,一個發送者(生産者)程式建立一個消息,并将其推送到一個消息隊列,然後一個接受者(消費者)程式從隊列中擷取這個消息并處理它。發送程式和接受程式不需要同時運作,因為消息傳遞是一個異步過程。這就是所謂的松耦合通信。

另一方面,Web服務方法調用(遠端方法調用)是一種緊耦合的同步通信(這兩個應用程式在整個通信的過程中都必須是運作着并且可用,如果Web服務脫機或在方法調用期間發生錯誤,那麼用戶端應用程式将得到一個異常)。

【翻譯】DotNetMQ: 一個.NET版完整的消息隊列系統

圖 - 1:兩個應用程式間最簡單的消息傳遞。

在上圖中,兩個應用程式通過消息隊列進行松散耦合方式通信。如果接受者處理消息的速度慢于發送者産生消息的速度,那麼隊列裡的消息數就會增加。此外,在發送者發送消息的過程中,接受者可能是離線的。在這種情況下,當接收者上線後,它會從隊列中得到消息(當它開始并加入這個隊列時)。

消息隊列通常由消息代理提供。消息代理是一個獨立的應用程式(一個服務),其他應用程式通過連接配接它發送、接收消息。在消息被接收者接收之前,消息代理負責存儲消息。消息代理可以通過路由多台機器把消息傳送給目标應用程式,在消息被接收者正确處理之前,消息代理會一直嘗試傳送它。有時候消息代理也被稱為面向消息的中間件(Message-Oriented-Middleware MOM)或者簡單的叫消息隊列(Message Queue MQ).

DotNetMQ是一個開源的消息代理,它有以下幾個特點:

  • 持久和非持久的消息發送。
  • 即使在系統崩潰時,也會保證持久消息的傳送。
  • 可在一個機器圖裡自動和手動設定消息的路由。
  • 支援多種資料庫(MS SQL Server,MySQL,SQLite,和一些現有的基于記憶體的存儲)
  • 支援不存儲,直接發送及時消息。
  • 支援請求/應答式的消息。
  • 用用戶端類庫和DotNetMQ消息代理通信很友善
  • 内置的架構,可以輕松地在消息隊列上建構RMI服務。
  • 支援把消息傳送給ASP.NET Web服務。
  • 基于圖形界面的管理和監控工具。
  • 易于安裝,管理和使用。
  • 完全由C#開發(使用.NET Framework 3.5)。

在開始建立它的時候,我更喜歡叫它為MDS(消息傳送系統 Message Delivery System)。因為它不僅是一個消息隊列,而且還是一個直接傳送消息到應用程式的系統和一個提供了建立應用服務架構的環境。我把它叫做DotNetMQ,是因為它完全由.NET開發,而且這個名字也更好記。是以它原來的名字是MDS,以至于源碼裡有許多以MDS為字首的類。

首先,我将示範一個需要消息代理的簡單情況。

在我的業務經曆中,我見到過一些非常糟糕且不尋常的異步企業應用內建解決方案。通常是運作在一台伺服器上的一個程式執行一些任務,并且産生一些資料,然後将結果資料發送到另一台伺服器上的另一個程式。第二個應用在資料上執行其他任務或計算結果(這台伺服器在同一網絡中或是通過網際網路連接配接)。另外,消息資料必須是持久的。即使遠端程式沒有工作或網絡不可用,消息必須第一時間發送過去。

讓我們來看看下面的設計圖:

【翻譯】DotNetMQ: 一個.NET版完整的消息隊列系統

圖 - 2:一個糟糕的內建應用程式解決方案。

Application -1 和Application -2是可執行程式(或是Windows服務),Sender Service是一個Windows服務。Application -1執行一些任務,産生資料,并調用Server-B伺服器上的Remote Web Service方法來傳輸資料。這個web服務将資料插入到資料表。Application -2定期檢查資料表來獲得新的資料行并處理它們(然後從表中删除它們,或将其标記為已處理,避免處理重複資料)。

如果在調用Web服務時或Web服務處理資料時出錯,資料不能丢失,并且稍後必須重發。但是,Application -1有其他任務要做,是以它不能一次又一次的嘗試重發資料。它隻是将資料插入到資料表。另一個Windows服務(如果Application -1是一直運作的,也可以使裡的一個線程)定期檢查這個表,并嘗試将資料發送到Web服務,直到資料成功發送。

這個解決方案的确是可靠的(消息確定傳送了),但它不是兩個應用程式之間通信的有效方式。該解決方案有一些非常關鍵的問題:

  • 需要很長的開發時間(去編碼)。
  • 要定制所有的消息類型(或遠端方法調用),對于一個新的Web服務方法調用,你必須改變所有的服務、應用程式和資料表。
  • 對每一個相似的服務,必須開發基本上一樣的軟體和結構(或複制,然後修改)。
  • 編碼後需要對服務、程式、資料庫做太多的測試和維護。
  • 一些程式和服務在沒有新消息的時候,還是會定期檢查資料庫(如果資料庫沒有很好的索引和優化,這可能會嚴重消耗系統資源)。

現在用消息代理來做這所有的事情,用最有效的方式負責将消息傳送給遠端應用。同一應用程式內建用上DotNetMQ展示于下圖。

【翻譯】DotNetMQ: 一個.NET版完整的消息隊列系統

圖 - 3:使用DotNetMQ的簡單消息傳遞。

DotNetMQ是一個獨立的Windows服務,分别運作在Server-A和Server-B伺服器上。是以,你隻需編寫代碼和DotNetMQ通信。使用DotNetMQ用戶端類庫,和DotNetMQ服務發送、接收資訊是非常容易和快速的。Application -1準備消息,設定目标,并将消息傳遞給DotNetMQ代理。DotNetMQ代理将以最有效和最快的方式傳遞給Application -2。

很顯然,在內建應用程式中消息代理是有必要的。我網上搜尋,查找書籍,想找一個免費的(最好也是開源的)而且是.Net用起來很容易的消息代理。讓我們看看我找到了什麼:

  • Apache ActiveMQ(http://activemq.apache.org):它是開源的,并且實作了JMS(Java Message Service,java消息服務在java世界裡是一個标準的消息傳輸API)。它也有一個.NET用戶端類庫。我為了更加了解,讀完了“ActiveMQ in Action”整本書,并且開發了一些簡單的應用。即使我通讀了這本書,我沒有看到一個簡單可靠的方式來建構一個共同合作和路有消息的ActiveMQ服務圖。我也沒有看到如何給一個消息設定目标伺服器。它自動路由消息,但我不能有效的控制路由的路徑。我的了解是,它通常和Apache Camel(http://camel.apache.org)一起使用來實作常見的應用內建模式。Apache Camel也是另一個需要去了解的領域,更糟的是,它隻使用Java。最後,我認為它不夠簡單易用,特别是配置,監控和管理。于是我放棄了對ActiveMQ的研究。
  • MSMQ(http://msdn.microsoft.com/en-us/library/ms711472(VS.85).aspx):這是來自微軟的解決方案,是.NET應用程式最合适的架構。它很容易使用和學習,而且它有工具看檢測隊列和消息。它尤其适用于那些運作在同一台機器上,或可以直接連接配接到同一台機器的應用程式間的異步通信。但我無法找到一個内置的解決方案,建構一個MSMQ伺服器圖來路由消息。因為路由是我的出發點,是以我隻好淘汰掉這個消息代理。
  • RabbitMQ(http://www.rabbitmq.com):它是由Erlang(有愛立信開發的一種程式設計語言)開發的。你需要先安裝Erlang。我花了很多時間來安裝,配置,并寫了一個示例程式。它有一個.NET用戶端,但當我試圖開發并運作一個簡單的程式是,出現很多錯誤。很難安裝,很難使不同伺服器上的兩個RabbitMQ協同工作。過了幾天,我就放棄了,因為我覺得學習并開始開發程式不應該那麼難。
  • OpenAMQ(http://www.openamq.org),ZeroMQ(http://www.zeromq.org):我總體研究了這兩個消息代理,但我發現我不能輕易做我想用.NET想做的事。
  • 其他:我還發現了一些其他的項目,但它們缺失一些重要的功能如路由,持久消息傳遞,請求/應答消息...等。

如你所見,在上面的清單中沒有哪一個消息代理是完全由.NET開發的。

從使用者角度來看,我隻是想通過“消息資料,目标伺服器和應用程式名稱”來定位我的代理。其他的我都不關心。他将會根據需要在網絡上多次路由一個消息,最後發送到目标伺服器的目标程式上。我的消息傳送系統必須為我提供這個便利。這是我的出發點。我根據這一點大概設計了消息代理的結構。下圖顯示了我想要的。

【翻譯】DotNetMQ: 一個.NET版完整的消息隊列系統

圖 - 4:自動路由消息的消息代理伺服器圖。

Application -1 傳遞一個消息到本地伺服器(Server-A)上的消息代理:

  • 目标伺服器:Server-D
  • 目标應用程式:Application -2
  • 消息資料:應用程式特定的資料

Server-A沒有直接和Server-D連接配接。是以,消息代理在伺服器間轉發消息(這個消息依次通過Server-A,Server-B,Server-C,Server-D),消息最後到達Server-D上的消息代理,然後傳遞給Application -2。注意在Server-E上也有一個Application-2在運作,但是它不會收到這個消息,因為消息的目标伺服器是Server-D。

DotNetMQ提供了這種功能和便利。它在伺服器圖上找到最佳的(最短的)路徑把消息從原伺服器轉發到目标伺服器。

經過這種全面的介紹會,讓我們看看如果在實踐中使用DotNetMQ。

現在還沒有實作自動安裝,不過安裝DotNetMQ是非常容易的。下載下傳并解壓文章開始提供的二進制檔案。隻需将所有的東西複制到C:\Progame Files\DotNetMQ\下,然後運作INSTALL_x86.bat(如果你用的是64位系統,那麼将執行INSTALL_x64)。

你可以檢查Windows服務,看看DotNetMQ是否已經安裝并正常工作。

讓我們看看實際中的DotNetMQ。為了使第一個程式足夠簡單,我假設是同一台機器上的兩個控制台應用程式(實際上,就像我們待會在文章中看到的那個,和在兩台機器上的兩個應用程式是沒什麼顯著差異的,隻是需要設定一下消息的目标伺服器名字而已)。

  • Application1:從使用者輸入那裡得到一個字元串消息,并将其發送到Application2.
  • Application2:在控制台上列印出傳入的消息。

我們的應用程式為了使用DotNetMQ,要先注冊一下,隻需操作一次,是一個非常簡單的過程。運作DotNetMQ管理器(DotNETMQ檔案夾下的MDSManager.exe,如上所訴,預設是在C:\Programe Files\DotNetMQ\檔案夾下),并在Applications菜單中打開Application類表。點選Add New Appliction按鈕,輸入應用程式名稱。

如上所述,添加Application1和Application2到DotNetMQ。最後,你的應用程式清單應該像下面這樣。

【翻譯】DotNetMQ: 一個.NET版完整的消息隊列系統

圖 - 5:DotNetMQ管理工具的應用程式清單界面。

在Visual Studio中建立一個名稱為Application1的控制台應用程式,并添加MDSCommonLib.dll引用,這個dll檔案裡提供了連接配接到DotNetMQ必需的一些類。然後在Program.cs檔案中寫上下面的代碼:

using System;
using System.Text;
using MDS.Client;

namespace Application1
{
    class Program
    {
        static void Main(string[] args)
        {
            //Create MDSClient object to connect to DotNetMQ
            //Name of this application: Application1
            var mdsClient = new MDSClient("Application1");

            //Connect to DotNetMQ server
            mdsClient.Connect();

            Console.WriteLine("Write a text and press enter to send " + 
               "to Application2. Write 'exit' to stop application.");

            while (true)
            {
                //Get a message from user
                var messageText = Console.ReadLine();
                if (string.IsNullOrEmpty(messageText) || messageText == "exit")
                {
                    break;
                }

                //Create a DotNetMQ Message to send to Application2
                var message = mdsClient.CreateMessage();
                //Set destination application name
                message.DestinationApplicationName = "Application2";
                //Set message data
                message.MessageData = Encoding.UTF8.GetBytes(messageText);

                //Send message
                message.Send();
            }

            //Disconnect from DotNetMQ server
            mdsClient.Disconnect();
        }
    }
}      

在建立MDSClient對象時,我們把要連接配接的應用程式名稱傳給構造函數,用這個構造函數,我們将用預設端口(10905)連接配接本地伺服器(127.0.0.1)上的DotNetMQ。重載的構造函數可以用于連接配接其他伺服器和端口。

MDSClient的CreateMessage方法傳回一個IOutgoingMessage的對象。對象的MessageData屬性是實際發送給目标應用程式的資料,它是一個位元組數組。我們使用UTF8編碼把使用者輸入的文本轉換成位元組數組。對象的DestinationApplicationName和DestinationServerName屬性是用于設定消息的目标位址。如果我們沒有指定目标伺服器,預設就是本地伺服器。最後,我們發送這個消息對象。

在Visual Studio裡建立一個新的控制台應用程式,命名為Application2,添加MDSCommonLib.dll并寫下以下代碼:

using System;
using System.Text;
using MDS.Client;

namespace Application2
{
    class Program
    {
        static void Main(string[] args)
        {
            //Create MDSClient object to connect to DotNetMQ
            //Name of this application: Application2
            var mdsClient = new MDSClient("Application2");

            //Register to MessageReceived event to get messages.
            mdsClient.MessageReceived += MDSClient_MessageReceived;

            //Connect to DotNetMQ server
            mdsClient.Connect();

            //Wait user to press enter to terminate application
            Console.WriteLine("Press enter to exit...");
            Console.ReadLine();

            //Disconnect from DotNetMQ server
            mdsClient.Disconnect();
        }

        /// <summary>
        /// This method handles received messages from other applications via DotNetMQ.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e">Message parameters</param>
        static void MDSClient_MessageReceived(object sender, MessageReceivedEventArgs e)
        {
            //Get message
            var messageText = Encoding.UTF8.GetString(e.Message.MessageData);

            //Process message
            Console.WriteLine();
            Console.WriteLine("Text message received : " + messageText);
            Console.WriteLine("Source application    : " + e.Message.SourceApplicationName);

            //Acknowledge that message is properly handled
            //and processed. So, it will be deleted from queue.
            e.Message.Acknowledge();
        }
    }
}      

我們用和Application1相似的方法建立一個MDSClient對象,不同的就是連接配接應用程式的名稱是Application2。為了接收消息,需要給MDSClient對象注冊MessageReceived事件。然後我們連接配接DotNetMQ,直到使用者輸入Enter才斷開。

當一個消息發送給Application2是,MDSClient_MessageReceived方法就會被調用來處理消息。我們從MessageReceivedEventArgs參數對象的Message屬性可以得到發送過來的消息。這個消息的類型是IIncomingMessage。IIncomingMessage對象的MessageData屬性實際包含了由Application1發送的消息資料。由于它是一個位元組數組,我們用UTF8編碼把它轉換成字元串。然後把文本消息列印到控制台上。

【翻譯】DotNetMQ: 一個.NET版完整的消息隊列系統

圖 - 6:Application1通過DotNetMQ發送兩個消息到Application2。

處理傳入消息之後,還需要來确認這個消息。這表示消息已經正确接收并處理。然後DotNetMQ将從消息隊列中把消息删除。我們也可以用Reject方法拒絕一個消息(如果在出錯的情況下我們不能處理這個消息)。在這種情況下,該消息将回到消息隊列,稍後再試着發到目标應用程式(如果在同一個伺服器上存在另一個Application2的實體,也可能發到另一個上)。這是DotNetMQ系統的一個強大機制。是以,可以確定消息不會丢失并絕對可以被處理。如果你不确認或拒絕一個消息,系統假設是被拒絕的。是以,即使你的應用程式崩潰了,在你的應用程式正常運作後,還是會收到消息的。

如果你在同一台伺服器上運作多個Application2的執行個體,哪一個會收到消息呢?在這種情況下,DotNetMQ會把消息順序地發給這多個執行個體。是以你可以建立多發送/接收的系統。一個消息隻能被一個執行個體接收(執行個體接收互相不同的消息)。DotNetMQ提供這所有功能和同步。

在發送一個消息之前,你可以像這樣設定一個消息的Transmit Rule屬性:

message.TransmitRule = MessageTransmitRules.NonPersistent;      

傳送規則有三種類型:

  • StoreAndForward:這個是預設傳送規則,消息是持久的,不會丢失的,并且使確定傳送的。如果Send方法沒有抛出異常,就表明消息已被DotNetMQ接收,而且存儲到了資料庫。直到目标應用程式接收并确認了它,這個消息會一直存儲在資料庫裡。
  • NonPersistent:消息不會存儲到資料庫,這是發送消息最快的方式。僅在DotNetMQ服務停止工作,消息才會丢失。
  • DirectlySend:這個是DotNetMQ獨有的功能。這種類型的消息直接發送給目标應用程式。在接收者确認一個消息之前,發送者程式是一直被阻塞的。是以,如果發送者在調用Send方法的過程中沒有發生異常,就意味着該消息被接受者正确接收并确認。如果在傳送消息時發生錯誤,或接受者處于脫機狀态,或者接受者拒絕了消息,發送者在調用Send方法時都會得到一個異常。即使應用程式是在不同的伺服器上(更即使在應用程式之間有許多伺服器要路由),這個規則依然能正常工作。

由于預設的傳送規則是StoreAndForward,讓我們試試下面這些:

  • 運作Application1(這時Application2沒有運作),輸入一些消息,然後關閉程式。
  • 運作Application2,你将看到消息沒有丢失,而是被Application2接收了。

即使在Application1發送過消息後,你停止了DotNetMQ服務,你的消息也是不會丢失的,這就叫持久化。

預設情況下,一個應用程式可以通過MDSClient發送和接收消息(CommunicationWays.SendAndReceive)。如果一個應用程式不需要接收消息,可以設定MDSClient的CommunicationWay為CommunicationWays.Send。這個屬性在連接配接DotNetMQ之前或在和DotNetMQ通信中都可以改變。

預設情況下,MDSClient由于某種原因斷開DotNetMQ時會自動重連。是以,即使你重新開機DotNetMQ服務,也不用重新開機你的應用程式。你可以把ReconnectServerOnError設定為false來禁用自動重連。

預設情況下,你必須在MessageReceived事件中顯式的确認消息。否則,系統将認為消息是被拒絕了。如果你想改變這種行為,你需要把AutoAcknowledgeMessages屬性設為true。在這種情況下,如果你的MessageReceived事件處理程式沒有抛出異常,你也沒有顯式确認和拒絕一個消息,系統将自動确認該消息(如果抛出異常,該消息将被拒絕)。

有兩種方式可以配置DotNetMQ:通過XML配置檔案或用DotNetMQ管理工具(一個Windows Forms程式),這裡我分别示範這兩種方法,有些配置是及時生效的,而有些則需要重新開機DotNetMQ。

你可以隻在一台伺服器上運作DotNetMQ,在這種情況下,是不需要為伺服器配置任何東西的。但如果你想在多台伺服器上運作DotNetMQ并使它們互相通信,你就需要定義伺服器圖了。

一個伺服器圖包含兩個或更多個節點,每一個節點都是一個具有IP位址和TCP端口(被DotNetMQ用的那個)的伺服器。你可以用DotNetMQ管理器配置/設計一個伺服器圖。

【翻譯】DotNetMQ: 一個.NET版完整的消息隊列系統

圖 - 8:DotNetMQ伺服器圖管理。

在上圖中,你看到了一個包含5個節點的伺服器圖。紅色節點表示目前伺服器(目前伺服器就是你用DotNetMQ管理器連接配接的那個)。直線表示兩個節點(它們互為相鄰節點)是可連接配接的(它們可以發送/接收消息)。伺服器/節點圖形中的名稱是很重要的,它被用來向該伺服器發送消息。

你可以輕按兩下圖形中的一個伺服器來編輯它的屬性。為了連接配接兩個伺服器,你要按住Ctrl鍵,點選第一個再點選第二個(斷開連接配接也是相同的操作)。你可以通過點選右鍵,選擇Set as this server來設定管理器連接配接該伺服器。你可以從圖中删除一個伺服器或通過右鍵菜單添加一個新的伺服器。最後,你可以通過拖拽添加或移除伺服器。

當你設計好伺服器圖之後,你必須點選Save & Update Graph按鈕來儲存這些修改。這些修改将儲存在DotNetMQ安裝目錄的MDSSettings.xml檔案裡。你必須重新開機DotNetMQ才能應用這些修改。

對于上面的伺服器圖,對應的MDSSettings.xml設定如下:

<?xml version="1.0" encoding="utf-8"?>
<MDSConfiguration>
  <Settings>
    ...
  </Settings>
  <Servers>
    <Server Name="halil_pc" IpAddress="192.168.10.105" 
       Port="10099" Adjacents="emre_pc" />
    <Server Name="emre_pc" IpAddress="192.168.10.244" Port="10099" 
       Adjacents="halil_pc,out_server,webserver1,webserver2" />
    <Server Name="out_server" IpAddress="85.19.100.185" 
       Port="10099" Adjacents="emre_pc" />
    <Server Name="webserver1" IpAddress="192.168.10.263" 
       Port="10099" Adjacents="emre_pc,webserver2" />
    <Server Name="webserver2" IpAddress="192.168.10.44" 
       Port="10099" Adjacents="emre_pc,webserver1" />
  </Servers>
  <Applications>
    ...
  </Applications>
  <Routes>
    ...
  </Routes>
</MDSConfiguration>      

當然,這個配置是要根據你實際的網絡進行的。你必須在圖中所有伺服器上安裝DotNetMQ。此外,還必須在所有伺服器上配置相同的伺服器圖(你可以很容易地從XML檔案複制伺服器節點到其他伺服器上)。

DotNetMQ采用段路徑算法發送消息(沒有在XML配置檔案裡手動定義路由的情況下)。考慮這個情景,運作在halil_pc的Application A發送一個消息到webserver2上的Application B,路徑是很簡單的:Application A -> halil_pc -> emre_pc -> webserver2 -> Application B。halil_pc通過伺服器圖定義知道下一個要轉發到的伺服器(emre_pc)。

最後,MDSSettings.design.xml包含了伺服器圖的設計資訊(節點在螢幕上的位置)。這個檔案隻是用于DotNetMQ管理器的伺服器圖窗體,運作時的DotNetMQ服務是不需要的。

就像圖 - 5顯示的那樣,你可以把和DotNetMQ關聯的應用程式作為消息代理來添加/删除。對于這些修改是不需要重新開機DotNetMQ的。應用程式的配置也儲存在MDSSettings.xml檔案裡,就像下面這樣:

<?xml version="1.0" encoding="utf-8"?>
<MDSConfiguration>
  ...
  <Applications>
    <Application Name="Application1" />
    <Application Name="Application2" />
  </Applications>
  ...
</MDSConfiguration>      

一個應用程式必須在這個清單裡才能和DotNetMQ連接配接。如果你直接修改xml檔案,你必須重新開機DotNetMQ服務才能生效。

DotNetMQ的有一個路由功能。現在路由設定隻能通過MDSSettings.xml設定。你可以看到下面檔案裡有兩種路由設定:

<?xml version="1.0" encoding="utf-8" ?>
<MDSConfiguration>
  ...
  <Routes>

    <Route Name="Route-App2" DistributionType="Sequential" >
      <Filters>
        <Filter DestinationServer="this" DestinationApplication="Application1" />
      </Filters>
      <Destinations>
        <Destination Server="Server-A" Application="Application1" RouteFactor="1" />
        <Destination Server="Server-B" Application="Application1" RouteFactor="1" />
        <Destination Server="Server-C" Application="Application1" RouteFactor="1" />
    </Destinations>
    </Route>

    <Route Name="Route-App2" DistributionType="Random" >
      <Filters>
        <Filter DestinationServer="this" DestinationApplication="Application2" /> 
        <Filter SourceApplication="Application2" TransmitRule="StoreAndForward" /> 
    </Filters>
      <Destinations>
        <Destination Server="Server-A" Application="Application2" RouteFactor="1" />
        <Destination Server="Server-B" Application="Application2" RouteFactor="3" />
      </Destinations>
    </Route>
    
  </Routes>
  ...
</MDSConfiguration>      

每個路由節點有兩個屬性:Name屬性是對使用者友好的顯示(不影響路由功能),DistributionType是路由的政策。這裡有兩種類型的路由政策:

  • Sequential:消息依次順序的路由到目标伺服器。Destination的RouteFactor是分發因子。
  • Random:消息随機的路由到目标伺服器。選擇Server-A伺服器的機率是:(Server-A的RouteFactor)/(Destinations裡所有RouteFactor的總和)。

Filters用于決定消息使用哪個路由。如果一個消息的屬性和其中一個過濾器比對,該消息就會被路由。這有5個條件(XML的5個屬性)來定義一個過濾器:

  • SourceServer:消息的第一個源伺服器,可以用this表示目前伺服器。
  • SourceApplication:發現消息的應用程式。
  • DestinationServer:消息的最終目标伺服器,可以用this表示目前伺服器。
  • DestinationApplication:接收消息的應用程式。
  • TransmitRule:消息傳送規則的一種(StoreAndForward,DirectlySend,NonPersistent)。

過濾消息時,不會考慮沒有定義的條件。是以,如果所有的條件都是空的(或直接沒定義),那麼所有的消息都适合這個過濾器。隻有所有的條件都比對時,一個過濾器才适合這個消息。如果一個消息正确比對(至少是過濾器定義的都比對)一個路由中的一個過濾器,那麼這個路由将被選擇并使用。

Destinations是用來将消息路由到其他伺服器用的。一個目标伺服器被選中是根據Route節點的DistributionType屬性(前面解釋過)決定的。一個destination節點必須定義三個屬性:

  • Server:目标伺服器,可以用this表示目前伺服器。
  • Application:目标應用程式,目标應用程式通常和消息的原目标程式是一樣的,不過這裡你可以重定向到另一個應用程式。
  • RouteFactor:這個屬性用于表明一個目标伺服器被選中的相對比率,可以用來做負載均衡。如果你想把消息平均分發到所有伺服器上,你可以把所有目标伺服器的FouteFactor屬性都設為1。但是如果你有兩台伺服器,其中一台比另一台性能強大的多,你可以通過設定這個路由因子來達到選擇第一台伺服器的機率是第二台的兩倍以上。

修改路由配置,必須重新開機DotNetMQ才會生效。

目前DotNetMQ支援3中存儲類型:SQLite(預設),MySQL和記憶體(譯者注:根據下面内容,還支援MSSQL)。你可以在MDSSettings.xml修改存儲類型。

  • SQLite:使用SQLite資料庫系統。這個是預設存儲類型,使用(DotNetMQ安裝目錄\SqliteDB\MDS.s3db)檔案作為資料庫。
  • MSSQL:使用微軟SQL Server資料庫,你需要提供ConnectionString屬性作為連接配接字元串(下面會說到)。
  • MySQL-ODBC:通過ODBC使用MySQL資料庫,你需要提供ConnectionString資料作為連接配接字元串。
  • MySQL-Net:通過.NET Adapter(.NET擴充卡)使用MySQL資料庫,你需要提供ConnectionString資料作為連接配接字元串。
  • Memory:使用記憶體作為儲存設備。在這種情況下,如果DotNetMQ停止了,持久性消息會丢失。

下面是一個使用MySQL-ODBC作為存儲的簡單配置:

<Settings>
    <Setting Key="ThisServerName" Value="halil_pc" />
    <Setting Key="StorageType" Value="MySQL-ODBC" />
    <Setting Key="ConnectionString" 
       Value="uid=root;server=localhost;driver={MySQL ODBC 3.51 Driver};database=mds" />
  </Settings>      

你可以在Setup\Databases檔案夾(這個檔案夾在DotNetMQ的安裝目錄)找到所需的檔案,然後建立資料庫和資料表,以供DotNetMQ使用。如果你有什麼問題,可以随時問我。

還有一個設定是定義"current/this"這個名稱代表哪台伺服器的,這個值必須是Servers節點裡的一個伺服器名。如果你用DotNetMQ管理器編輯伺服器圖,這個值是自動設定的。

向一個網絡伺服器的應用程式發消息是和向同一個伺服器的應用程式發消息一樣簡單的。

讓我們考慮下面這個網絡:

【翻譯】DotNetMQ: 一個.NET版完整的消息隊列系統

圖 - 8:兩個應用程式通過DotNetMQ在網絡上通信。

運作在ServerA上的Application1想發消息到ServerC上的Application2,由于防火牆的規則,ServerA和ServerC不能直接連接配接。讓我們修改一下在第一個DotNetMQ程式裡開發的程式。

Application2甚至一點有不用修改,隻要把Application2上ServerC上運作并等待傳入的消息即可。

Application1隻是在如何發消息的地方稍微改動一點,就是設定DestinationServerName(目标伺服器名)為ServerC。

var message = mdsClient.CreateMessage();
message.DestinationServerName = "ServerC"; //Set destination server name here!
message.DestinationApplicationName = "Application2";
message.MessageData = Encoding.UTF8.GetBytes(messageText);
message.Send();      

就這樣,就完事兒了。你不需要知道ServerC在哪裡,也不需要直接連接配接ServerC...這些全部定義在DotNetMQ設定裡。注意:如果你不給一個消息設定DestinationServerName,系統假設目标伺服器就是"current/this"指定的那台伺服器,DotNetMQ也将把消息發送到同一台伺服器上的應用程式。另外,如果你定義了必要的路由,你就不必設定目标伺服器了,DotNetMQ會自動地路由消息。

當然,DotNetMQ的設定必須根據伺服器間的連接配接(伺服器圖)來設定,并且Application1和Application2必須像配置DotNetMQ部分說的那樣注冊到DotNetMQ伺服器。

正如你已看到的那樣,DotNetMQ可以用于建構分布式,負載均衡應用系統。在本節中,我将讨論一個生活中真實的場景:一個分布式消息處理系統。

假定有一個用于音樂比賽投票的短消息(MSM)服務。所有競賽者唱過他們的歌曲後,觀衆給他們最喜歡的歌手投票,會發一條像"VOTE 103"這樣的短信到我們的短息伺服器。并假定這次投票會在短短的30分鐘完成,大約有五百萬人發短息到我們的服務。

我們将會接收每一條短息,處理它(格式化短息文本,修改資料庫,以便增加選手的票數),并要發送确認消息給發送者。我們從兩台伺服器接收消息,在四台伺服器上處理消息,然後從兩台伺服器上發送确認消息。我們總共有八台伺服器。讓我們看看完整的系統示意圖:

【翻譯】DotNetMQ: 一個.NET版完整的消息隊列系統

圖 - 9:分布式短信處理系統

這裡有三種類型的應用:接受者,處理器,和發送者。在這種情況下,你就可以使用DotNetMQ作為消息隊列和負載均衡器,通過配置伺服器圖和路由(就像配置DotNetMQ小節中描述的那樣),來建構一個分布式的,可擴充的消息處理系統。

在許多情況下,一個應用發一個消息到另一個應用,然後得到一個應答消息。DotNetMQ對這種通信方式有内置的支援。考慮這樣一個服務:用于查詢庫存的狀态。這裡有兩種消息類型:

[Serializable]
public class StockQueryMessage
{
    public string StockCode { get; set; }
}

[Serializable]
public class StockQueryResultMessage
{
    public string StockCode { get; set; }
    public int ReservedStockCount { get; set; }
    public int TotalStockCount { get; set; }
}      

下面展示了一個簡單的庫存服務。

using System;
using MDS;
using MDS.Client;
using StockCommonLib;

namespace StockServer
{
    class Program
    {
        static void Main(string[] args)
        {
            var mdsClient = new MDSClient("StockServer");
            mdsClient.MessageReceived += MDSClient_MessageReceived;

            mdsClient.Connect();

            Console.WriteLine("Press enter to exit...");
            Console.ReadLine();

            mdsClient.Disconnect();
        }

        static void MDSClient_MessageReceived(object sender, 
                    MessageReceivedEventArgs e)
        {
            //Get message
            var stockQueryMessage = 
                GeneralHelper.DeserializeObject(e.Message.MessageData) 
                as StockQueryMessage;
            if (stockQueryMessage == null)
            {
                return;
            }

            //Write message content
            Console.WriteLine("Stock Query Message for: " + 
                              stockQueryMessage.StockCode);

            //Get stock counts from a database...
            int reservedStockCount;
            int totalStockCount;
            switch (stockQueryMessage.StockCode)
            {
                case "S01":
                    reservedStockCount = 14;
                    totalStockCount = 80;
                    break;
                case "S02":
                    reservedStockCount = 0;
                    totalStockCount = 25;
                    break;
                default: //Stock does not exists!
                    reservedStockCount = -1;
                    totalStockCount = -1;
                    break;
            }

            //Create a reply message for stock query
            var stockQueryResult = new StockQueryResultMessage
                                       {
                                           StockCode = stockQueryMessage.StockCode,
                                           ReservedStockCount = reservedStockCount,
                                           TotalStockCount = totalStockCount
                                       };
            
            //Create a MDS response message to send to client
            var responseMessage = e.Message.CreateResponseMessage();
            responseMessage.MessageData = 
               GeneralHelper.SerializeObject(stockQueryResult);

            //Send message
            responseMessage.Send();

            //Acknowledge the original request message.
            //So, it will be deleted from queue.
            e.Message.Acknowledge();
        }
    }
}      

這個庫存服務監聽進來的StockQueryMessage消息對象,然後把StockQueryResultMessage消息對象發送給查詢者。為了簡單起見,我沒有從資料庫查詢庫存。應答消息對象是由傳入消息對象的CreateResponseMessage()方法建立的。最後,發出回應消息後要确認進入的消息。現在,我展示一個簡單的庫存用戶端從伺服器查詢庫存的示例:

using System;
using MDS;
using MDS.Client;
using MDS.Communication.Messages;
using StockCommonLib;

namespace StockApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Press enter to query a stock status");
            Console.ReadLine();

            //Connect to DotNetMQ  
            var mdsClient = new MDSClient("StockClient");
            mdsClient.MessageReceived += mdsClient_MessageReceived;
            mdsClient.Connect();
            //Create a stock request message 
            var stockQueryMessage = new StockQueryMessage { StockCode = "S01" }; 
            //Create a MDS message 
            var requestMessage = mdsClient.CreateMessage(); 
            requestMessage.DestinationApplicationName = "StockServer"; 
            requestMessage.TransmitRule = MessageTransmitRules.NonPersistent; 
            requestMessage.MessageData = GeneralHelper.SerializeObject(stockQueryMessage); 
            //Send message and get response 
            var responseMessage = requestMessage.SendAndGetResponse(); 
            //Get stock query result message from response message 
            var stockResult = (StockQueryResultMessage) GeneralHelper.DeserializeObject(responseMessage.MessageData); 
            //Write stock query result 
            Console.WriteLine("StockCode = " + stockResult.StockCode); 
            Console.WriteLine("ReservedStockCount = " + stockResult.ReservedStockCount); 
            Console.WriteLine("TotalStockCount = " + stockResult.TotalStockCount); 
            //Acknowledge received message 
            responseMessage.Acknowledge(); 
            Console.ReadLine(); 
            //Disconnect from DotNetMQ server. 
            mdsClient.Disconnect(); 
       } 
       static void mdsClient_MessageReceived(object sender, MessageReceivedEventArgs e) { 
            //Simply acknowledge other received messages 
            e.Message.Acknowledge(); 
       } 
   } 
}


      

在上面的示例中,為了示範目的TransmitRule設定成了NonPersistent(非持久)。當然,你可以發送StoreAndForward(持久性)消息。這個是程式運作的截圖:

【翻譯】DotNetMQ: 一個.NET版完整的消息隊列系統

圖 - 10:請求/應答式的通信應用。

SOA(面向服務的架構)是以個流行多年的概念了。Web服務和WCF是兩個主要的SOA解決方案。一般情況下,一個消息隊列系統是不會預期支援SOA的。同時,消息通信是異步的,松耦合的過程,而Web服務方法調用則通常是同步的,緊耦合的。即使(正如你在前面示例程式中看到的那樣)消息通信并不如調用一個遠端方法一樣簡單,但是當你的消息數增加,你的應用變複雜以至于難以維護時就不一樣了。DotNetMQ支援持久性和非持久性的遠端調用機制,所有你可以異步地調用一個遠端方法,DotNetMQ會確定調用成功。

在這裡我們将開發一個簡單的服務,可用于發送短信和郵件。也許沒有必要專門寫一個服務來發送短信和郵件,這些功能都可以在應用自身實作,但是想象一下你有很多應用都要發郵件,在發送時如果郵件服務出問題了怎麼辦?在可以成功發送郵件之前,應用程式必須一直嘗試。是以你必須在你的應用程式中建立一個郵件隊列機制,用于一次又一次的嘗試發送。在最壞的情況下,你的應用程式可能隻運作很短的時間(如Web服務)或者必須在發送完郵件前關閉。但是在郵件伺服器上線後,你還必須發送,不允許郵件丢失。

在這種情況下,你可以開發一個單獨的郵件/短信服務,它将嘗試發送直到成功。你可以通過DotNetMQ開發一個郵件服務,僅當郵件發送成功時确認請求,如果發送失敗,隻要不确認(或拒絕)消息就行了,它稍後會重試。

首先,我們開發短信/郵件的服務部分。為了實作這個,我們必須定義一個派生自MDSService的類型:

using System;
using MDS.Client.MDSServices;

namespace SmsMailServer
{
    [MDSService(Description = "This service is a " + 
              "sample mail/sms service.", Version = "1.0.0.0")]
    public class MyMailSmsService : MDSService
    {
        //All parameters and return values can be defined.
        [MDSServiceMethod(Description = "This method is used send an SMS.")]
        public void SendSms(
            [MDSServiceMethodParameter("Phone number to send SMS.")] string phone,
            [MDSServiceMethodParameter("SMS text to be sent.")] string smsText)
        {
            //Process SMS
            Console.WriteLine("Sending SMS to phone: " + phone);
            Console.WriteLine("Sms Text: " + smsText);

            //Acknowledge the message
            IncomingMessage.Acknowledge();
        }

        //You do not have to define any parameters
        [MDSServiceMethod]
        public void SendEmail(string emailAddress, string header, string body)
        {
            //Process email
            Console.WriteLine("Sending an email to " + emailAddress);
            Console.WriteLine("Header: " + header);
            Console.WriteLine("Body  : " + body);

            //Acknowledge the message
            IncomingMessage.Acknowledge();
        }

        // A simple method just to show return values.
        [MDSServiceMethod]
        [return: MDSServiceMethodParameter("True, if phone number is valid.")]
        public bool IsValidPhone([MDSServiceMethodParameter(
               "Phone number to send SMS.")] string phone)
        {
            //Acknowledge the message
            IncomingMessage.Acknowledge();
            
            //Return result
            return (phone.Length == 10);
        }
    }
}      

如你所見,它隻是一個帶有特性(Attribute)的一個正常C#類。MDSService和MDSServiceMethod兩個特性是必須的,其他的特性是可選的(不過寫上去是最好了,你将很快會看到什麼會用這些特性)。你提供服務的方法必須有MDSServiceMehod特性,如果你不想公開一些方法,隻要不加MDSServiceMethod特性就行了。

你還必須在你的服務方法中确認消息,否則,這個消息(引起這個服務方法調用的那個)就不會從消息隊列中删除,而我們的服務方法将會被再次調用。如果我們不能處理這個消息(比如,如果郵件服務沒有工作,我們沒辦法發送時)我們也可以拒絕它。如果我們拒絕了這個消息,它稍後還會發送給我們(很可靠)。你可以通過MDSService類的IncomingMessage屬性得到原消息,另外,你也可以通過RemoteApplication屬性得到遠端應用程式的資訊。

建立了正确的服務類後,我們必須建立一個應用來運作它,下面是用一個簡單的控制台程式運作我們的MyMailSmsService服務:

using System;
using MDS.Client.MDSServices;

namespace SmsMailServer
{  
    class Program
    {
        static void Main(string[] args)
        {
            using (var service = new MDSServiceApplication("MyMailSmsService"))
            {
                service.AddService(new MyMailSmsService());
                service.Connect();

                Console.WriteLine("Press any key to stop service");
                Console.ReadLine();
            }
        }
    }
}      

如你所見,隻需要3行代碼就可以建立并運作服務,由于MDSService是可銷毀的,是以你可以uing語句,另外,你也可以使用MDSServiceApplication的Disconnect方法手動關閉服務。你可以通過AddService方法在一個MDSServiceApplication中運作多個服務。

為了開發一個使用DotNetMQ服務的應用,你必須建立一個服務代理(就像Web服務和WCF那樣)。為了建立代理,你可以用MDSServiceProxyGenerator工具。首先,編譯你的服務項目,然後運作MDSServiceProxyGenerator.exe(在DotNetMQ安裝目錄).

【翻譯】DotNetMQ: 一個.NET版完整的消息隊列系統

圖 - 11:為DotNetMQ服務生成代理類。

選擇你的服務程式集(在這個簡單的例子中是指SmsMailServer.exe)。你可以選擇服務類或生成這個程式集裡所有服務的代理。輸入一個命名空間和一個目标檔案夾,然後生成代理類。生成玩後,你就可以把它加到你的項目裡了。

我就不展示這個代理類了,但你必須了解它(你可以看源碼,它是一個很簡單的類)。你方法/參數上的特性用來生成這個代理類的注釋。

在我們的項目裡添加這個代理類後,我們就可以想簡單方法調用那樣向服務發消息了。

using System;
using MDS.Client;
using MDS.Client.MDSServices;
using SampleService;

namespace SmsMailClient
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Press enter to test SendSms method");
            Console.ReadLine();

            //Application3 is name of an application that sends sms/email.
            using (var serviceConsumer = new MDSServiceConsumer("Application3"))
            {
                //Connect to DotNetMQ server
                serviceConsumer.Connect();

                //Create service proxy to call remote methods
                var service = new MyMailSmsServiceProxy(serviceConsumer, 
                    new MDSRemoteAppEndPoint("MyMailSmsService"));

                //Call SendSms method
                service.SendSms("3221234567", "Hello service!");
            }
        }
    }
}      

你也可以調用服務的其他方法,會得到像正常方法那樣的傳回值。實際上,你的方法調用被轉換成了可靠的消息,比如,即使你的遠端應用程式(MyMailSmsService)在方法調用時沒有運作,在服務啟動後也會被調用,是以你的方法調用是一定會被調用的。

你可以通過改變服務代理的TransmitRule屬性來改變消息傳輸的規則。如果服務方法傳回void,那麼他的預設傳輸規則是StoreAndForward。如果服務方法有個一傳回值,那麼方法調用将會不可靠(因為方法調用時同步的,要等待一個結果的),它的規則是DiretlySend。你可以選擇任何類型作為方法的參數,如果參數類型是基元類型(string,int,byte...),就不需要附加的設定,但是如果你想用你自定義的類型作為方法參數,這個類型必須标記為Serializable,因為DotNetMQ會用二進制序列化參數。

注意:你在運作這個例子前必須在DotNetMQ裡注冊MyMailSmsService和Application3。

當然,你可以在Web服務裡連接配接DotNetMQ,因為把本身還是一個.Net應用程式。但是,為什麼你要寫一個ASP.NET Web方法為應用程式處理消息(而且可以在同一個上下文中回複消息)呢?Web服務更适合這樣請求/應答式的方法調用。

DotNetMQ支援ASP.NET web服務并可以傳遞消息到web服務。這裡有個web服務的模闆樣品(在下載下傳檔案中)來實作這一目标。它的定義如下:

using System;
using System.Web.Services;
using MDS.Client.WebServices;

[WebService(Namespace = "http://www.dotnetmq.com/mds")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class MDSAppService : WebService
{
    /// <summary>
    /// MDS server sends messages to this method.
    /// </summary>
    /// <param name="bytesOfMessage">Byte array form of message</param>
    /// <returns>Response message to incoming message</returns>
    [WebMethod(Description = "Receives incoming messages to this web service.")]
    public byte[] ReceiveMDSMessage(byte[] bytesOfMessage)
    {
        var message = WebServiceHelper.DeserializeMessage(bytesOfMessage);
        try
        {
            var response = ProcessMDSMessage(message);
            return WebServiceHelper.SerializeMessage(response);
        }
        catch (Exception ex)
        {
            var response = message.CreateResponseMessage();
            response.Result.Success = false;
            response.Result.ResultText = 
              "Error in ProcessMDSMessage method: " + ex.Message;
            return WebServiceHelper.SerializeMessage(response);
        }
    }

    /// <summary>
    /// Processes incoming messages to this web service.
    /// </summary>
    /// <param name="message">Message to process</param>
    /// <returns>Response Message</returns>
    private IWebServiceResponseMessage 
            ProcessMDSMessage(IWebServiceIncomingMessage message)
    {
        //Process message

        //Send response/result
        var response = message.CreateResponseMessage();
        response.Result.Success = true;
        return response;
    }
}      

如上所述,你不需要改變ReceiveMDSMessage方法,而且必須在ProcessMDSMessage方法裡處理消息。另外,你需要向下面這樣在MDSSettings.xml裡定義你的web服務位址,你也可以用DotNetMQ管理工具添加web服務。

... 
  <Applications>
    <Application Name="SampleWebServiceApp">
      <Communication Type="WebService" 
        Url="http://localhost/SampleWebApplication/SampleService.asmx" />
    </Application>
  </Applications>
  ...       

DotNetMQ的性能

這是一些通過DotNetMQ傳送消息的測試結果:

消息傳送:

  • 持久地 10,000個消息大約需要25秒(約每秒400個消息)。
  • 非持久地 10,000個消息大約需要3.5秒(約每秒2850個消息)。

方法調用(在DotNetMQ服務裡)

  • 持久地 10,000個方法調用大約需要25秒(約每秒400個)。
  • 非持久地 10,000個方法調用大約需要8.7秒(約每秒1150個)。

測試平台:Intel Core 2 Duo 3,00 GHZ CPU.2 GB RAM PC。消息傳送和方法調用是在同一台電腦上的兩個應用程式之間進行的。

書籍:Enterprise Integration Patterns: Designing,Building,and Deploying Messaging Solutions .作者 Gregor Hohpe,Bobby Woolf(艾迪生韋斯利出版,2003年)。

  • 2011-05-23(DotNetMQ v0.9.1.0)
    • 添加對微軟SQL Server資料庫的支援。
    • 把MySQLConnectionString設定改成ConnectionString。
    • 修改源碼。
    • 根據修改更新了文章。
  • 2011-05-16 (DotNetMQ v0.9.0.0)
    • 添加web服務模闆的下載下傳。
    • 對文章做了一些修改和添加。
  • 2011-05-09(DotNetMQ v0.9.0.0)
    • 第一次釋出。