文章概要
介绍
在这篇文章中,我将介绍一个新的、独立的、开源的,完全基于C#和.NET Framework3.5的消息队列系统,DotNetMQ是一个消息代理,它包括确保传输,路由,负载均衡,服务器图等等多项功能。我将从解释消息的概念和消息代理的必要性讲起,然后,我会说明什么是DotNetMQ,以及如何使用它。
什么是消息传递
消息传递是一种异步通信方式,具体就是在同一个或不同的机器上运行的多个应用程序直接可靠的消息传递。应用程序通过发送一种叫消息的数据包和其他应用程序通信。
一个消息可以是一个字符串,一个字节数组,一个对象等等。通常情况下,一个发送者(生产者)程序创建一个消息,并将其推送到一个消息队列,然后一个接受者(消费者)程序从队列中获取这个消息并处理它。发送程序和接受程序不需要同时运行,因为消息传递是一个异步过程。这就是所谓的松耦合通信。
另一方面,Web服务方法调用(远程方法调用)是一种紧耦合的同步通信(这两个应用程序在整个通信的过程中都必须是运行着并且可用,如果Web服务脱机或在方法调用期间发生错误,那么客户端应用程序将得到一个异常)。
图 - 1:两个应用程序间最简单的消息传递。
在上图中,两个应用程序通过消息队列进行松散耦合方式通信。如果接受者处理消息的速度慢于发送者产生消息的速度,那么队列里的消息数就会增加。此 外,在发送者发送消息的过程中,接受者可能是离线的。在这种情况下,当接收者上线后,它会从队列中得到消息(当它开始并加入这个队列时)。
消息队列通常由消息代理提供。消息代理是一个独立的应用程序(一个服务),其他应用程序通过连接它发送、接收消息。在消息被接收者接收之前,消息代 理负责存储消息。消息代理可以通过路由多台机器把消息传送给目标应用程序,在消息被接收者正确处理之前,消息代理会一直尝试传送它。有时候消息代理也被称 为面向消息的中间件(Message-Oriented-Middleware MOM)或者简单的叫消息队列(Message Queue MQ).
什么是DotNetMQ?
DotNetMQ是一个开源的消息代理,它有以下几个特点:
在开始创建它的时候,我更喜欢叫它为MDS(消息传送系统 Message Delivery System)。因为它不仅是一个消息队列,而且还是一个直接传送消息到应用程序的系统和一个提供了建立应用服务框架的环境。我把它叫做 DotNetMQ,是因为它完全由.NET开发,而且这个名字也更好记。所以它原来的名字是MDS,以至于源码里有许多以MDS为前缀的类。
为什么要一个新的消息代理?
消息代理的必要性
首先,我将演示一个需要消息代理的简单情况。
在我的业务经历中,我见到过一些非常糟糕且不寻常的异步企业应用集成解决方案。通常是运行在一台服务器上的一个程序执行一些任务,并且产生一些数 据,然后将结果数据发送到另一台服务器上的另一个程序。第二个应用在数据上执行其他任务或计算结果(这台服务器在同一网络中或是通过互联网连接)。另外, 消息数据必须是持久的。即使远程程序没有工作或网络不可用,消息必须第一时间发送过去。
让我们来看看下面的设计图:
图 - 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服务,直到数据成功发送。
这个解决方案的确是可靠的(消息确保传送了),但它不是两个应用程序之间通信的有效方式。该解决方案有一些非常关键的问题:
图 - 3:使用DotNetMQ的简单消息传递。
DotNetMQ是一个独立的Windows服务,分别运行在Server-A和Server-B服务器上。因此,你只需编写代码和 DotNetMQ通信。使用DotNetMQ客户端类库,和DotNetMQ服务发送、接收信息是非常容易和快速的。Application -1准备消息,设置目标,并将消息传递给DotNetMQ代理。DotNetMQ代理将以最有效和最快的方式传递给Application -2。
现有的消息代理
很显然,在集成应用程序中消息代理是有必要的。我网上搜索,查找书籍,想找一个免费的(最好也是开源的)而且是.Net用起来很容易的消息代理。让我们看看我找到了什么:
如你所见,在上面的列表中没有哪一个消息代理是完全由.NET开发的。
从用户角度来看,我只是想通过“消息数据,目标服务器和应用程序名称”来定位我的代理。其他的我都不关心。他将会根据需要在网络上多次路由一个消 息,最后发送到目标服务器的目标程序上。我的消息传送系统必须为我提供这个便利。这是我的出发点。我根据这一点大概设计了消息代理的结构。下图显示了我想 要的。
图 - 4:自动路由消息的消息代理服务器图。
Application -1 传递一个消息到本地服务器(Server-A)上的消息代理:
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
现在还没有实现自动安装,不过安装DotNetMQ是非常容易的。下载并解压文章开始提供的二进制文件。只需将所有的东西复制到 C:\Progame Files\DotNetMQ\下,然后运行INSTALL_x86.bat(如果你用的是64位系统,那么将执行INSTALL_x64)。
你可以检查Windows服务,看看DotNetMQ是否已经安装并正常工作。
第一个DotNetMQ程序
让我们看看实际中的DotNetMQ。为了使第一个程序足够简单,我假设是同一台机器上的两个控制台应用程序(实际上,就像我们待会在文章中看到的那个,和在两台机器上的两个应用程序是没什么显著差异的,只是需要设置一下消息的目标服务器名字而已)。
注册应用程序到DotNetMQ
我们的应用程序为了使用DotNetMQ,要先注册一下,只需操作一次,是一个非常简单的过程。运行DotNetMQ管理器(DotNETMQ文件 夹下的MDSManager.exe,如上所诉,默认是在C:\Programe Files\DotNetMQ\文件夹下),并在Applications菜单中打开Application类表。点击Add New Appliction按钮,输入应用程序名称。
如上所述,添加Application1和Application2到DotNetMQ。最后,你的应用程序列表应该像下面这样。
图 - 4:自动路由消息的消息代理服务器图。
Application -1 传递一个消息到本地服务器(Server-A)上的消息代理:
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
现在还没有实现自动安装,不过安装DotNetMQ是非常容易的。下载并解压文章开始提供的二进制文件。只需将所有的东西复制到 C:\Progame Files\DotNetMQ\下,然后运行INSTALL_x86.bat(如果你用的是64位系统,那么将执行INSTALL_x64)。
你可以检查Windows服务,看看DotNetMQ是否已经安装并正常工作。
第一个DotNetMQ程序
让我们看看实际中的DotNetMQ。为了使第一个程序足够简单,我假设是同一台机器上的两个控制台应用程序(实际上,就像我们待会在文章中看到的那个,和在两台机器上的两个应用程序是没什么显著差异的,只是需要设置一下消息的目标服务器名字而已)。
注册应用程序到DotNetMQ
我们的应用程序为了使用DotNetMQ,要先注册一下,只需操作一次,是一个非常简单的过程。运行DotNetMQ管理器(DotNETMQ文件 夹下的MDSManager.exe,如上所诉,默认是在C:\Programe Files\DotNetMQ\文件夹下),并在Applications菜单中打开Application类表。点击Add New Appliction按钮,输入应用程序名称。
如上所述,添加Application1和Application2到DotNetMQ。最后,你的应用程序列表应该像下面这样。
图 - 5:DotNetMQ管理工具的应用程序列表界面。
开发Application1
在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属性是用于设置消息的目标地址。如果我们没有指定目 标服务器,默认就是本地服务器。最后,我们发送这个消息对象。
开发Application2
在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编码把它转换成字符串。然后把文本消息打印到控制台上。
图 - 6:Application1通过DotNetMQ发送两个消息到Application2。
处理传入消息之后,还需要来确认这个消息。这表示消息已经正确接收并处理。然后DotNetMQ将从消息队列中把消息删除。我们也可以用 Reject方法拒绝一个消息(如果在出错的情况下我们不能处理这个消息)。在这种情况下,该消息将回到消息队列,稍后再试着发到目标应用程序(如果在同 一个服务器上存在另一个Application2的实体,也可能发到另一个上)。这是DotNetMQ系统的一个强大机制。因此,可以确保消息不会丢失并 绝对可以被处理。如果你不确认或拒绝一个消息,系统假设是被拒绝的。所以,即使你的应用程序崩溃了,在你的应用程序正常运行后,还是会收到消息的。
如果你在同一台服务器上运行多个Application2的实例,哪一个会收到消息呢?在这种情况下,DotNetMQ会把消息顺序地发给这多个实 例。所以你可以创建多发送/接收的系统。一个消息只能被一个实例接收(实例接收相互不同的消息)。DotNetMQ提供这所有功能和同步。
消息属性:传送规则(Transmit Rule)
在发送一个消息之前,你可以像这样设置一个消息的Transmit Rule属性:
message.TransmitRule = MessageTransmitRules.NonPersistent;
传送规则有三种类型:
由于默认的传送规则是StoreAndForward,让我们试试下面这些:
即使在Application1发送过消息后,你停止了DotNetMQ服务,你的消息也是不会丢失的,这就叫持久化。
客户端属性:通讯方式(CommunicationWay)
默认情况下,一个应用程序可以通过MDSClient发送和接收消息(CommunicationWays.SendAndReceive)。如果 一个应用程序不需要接收消息,可以设置MDSClient的CommunicationWay为CommunicationWays.Send。这个属性 在连接DotNetMQ之前或在和DotNetMQ通信中都可以改变。
客户端属性:出错时重新连接服务器(ReConnectServerOnError)
默认情况下,MDSClient由于某种原因断开DotNetMQ时会自动重连。所以,即使你重启DotNetMQ服务,也不用重启你的应用程序。你可以把ReconnectServerOnError设置为false来禁用自动重连。
客户端属性:自动确认消息(AutoAcknowledgeMessages)
默认情况下,你必须在MessageReceived事件中显式的确认消息。否则,系统将认为消息是被拒绝了。如果你想翻转这种行为,你必须把 AutoAcknowledgeMessages属性设为true。在这种情况下,如果你的MessageReceived事件处理程序没有抛出异常,你 也没有显式确认和拒绝一个消息,系统将自动确认该消息(如果抛出异常,该消息将被拒绝)。
配置DotNetMQ
有两种方式可以配置DotNetMQ:通过XML配置文件或用DotNetMQ管理工具(一个Windows Forms程序),这里我分别演示这两种方法,有些配置是及时生效的,而有些则需要重启DotNetMQ。
服务端
你可以只在一台服务器上运行DotNetMQ,在这种情况下,是不需要为服务器配置任何东西的。但如果你想在多台服务器上运行DotNetMQ并使它们相互通信,你就需要定义服务器图了。
一个服务器图包含两个或更多个节点,每一个节点都是一个具有IP地址和TCP端口(被DotNetMQ用的那个)的服务器。你可以用DotNetMQ管理器配置/设计一个服务器图。
图 - 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是路由的策略。这里有两种类型的路由策略:
Filters用于决定消息使用哪个路由。如果一个消息的属性和其中一个过滤器匹配,该消息就会被路由。这有5个条件(XML的5个属性)来定义一个过滤器:
过滤消息时,不会考虑没有定义的条件。所以,如果所有的条件都是空的(或直接没定义),那么所有的消息都适合这个过滤器。只有所有的条件都匹配是, 一个过滤器才适合这个消息。如果一个消息正确匹配(至少是过滤器定义的都匹配)一个路由中的一个过滤器,那么这个路由将被选择并使用。
Destinations是用来将消息路由到其他服务器用的。一个目标服务器被选中是根据Route节点的DistributionType属性(前面解释过)决定的。一个destination节点必须定义三个属性:
修改路由配置,必须重启DotNetMQ才会生效。
其他设置
目前DotNetMQ支持3中存储类型:SQLite(默认),MySQL和内存(译者注:根据下面内容,还支持MSSQL)。你可以在MDSSettings.xml修改存储类型。
下面是一个使用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管理器编辑服务器图,这个值是自动设置的。
网络传输消息
向一个网络服务器的应用程序发消息是个向同一个服务器的应用程序法消息一样简单的。
一个简单的应用程序
让我们考虑下面这个网络:
图 - 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服务器。
一个真实的案例:分布式短信处理器(Distributed SMS Processor)
正如你已看到的那样,DotNetMQ可以用于构建分布式,负载均衡应用系统。在本节中,我将讨论一个生活中真实的场景:一个分布式消息处理系统。
假定有一个用于音乐比赛投票的短消息(MSM)服务。所有竞赛者唱过他们的歌曲后,观众给他们最喜欢的歌手投票,会发一条像"VOTE 103"这样的短信到我们的短息服务器。并假定这次投票会在短短的30分钟完成,大约有五百万人发短息到我们的服务。
我们将会接收每一条短息,处理它(格式化短息文本,修改数据库,以便增加选手的票数),并要发送确认消息给发送者。我们从两台服务器接收消息,在四台服务器上处理消息,然后从两台服务器上发送确认消息。我们总共有八台服务器。让我们看看完整的系统示意图:
图 - 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(持久性)消息。这个是程序运行的截图:
图 - 10:请求/应答式的通信应用。
面向服务架构的DotNetMQ
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安装目录).
图 - 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服务支持
当然,你可以在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传送消息的测试结果:
消息传送:
方法调用(在DotNetMQ服务里)
测试平台: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年)。
历史