上一篇讲了搭建VMware虚拟机实现与宿主机相互通信,环境已经就绪,现在就可以做MSMQ的分布式开发了。
本篇准备分四点介绍MSMQ:
1.MSMQ简介
2.MSMQ的安装
3.MSMQ编程开发
4.Demo下载
一、MSMQ简介
MSMQ本质上是一种消息传递协议,它允许在单独的服务端/客户端运行的应用程序间已可靠的方式通信。队列用来临时存储消息,服务器端向队列发送消息,客户端从队列读取消息。这就使得即使服务器端和客户端不在同一网络中,不能直接访问彼此,也能通过队列进行通信。相比之下,sockets和其他网络协议就不能做到这一点,它们只能在服务器端和客户端能够直接相互通信的情况下才有效。
MSMQ诞生于1997年,它已经被广泛使用在企业级软件中。Microsoft已经把MSMQ合并到它的消息技术框架WCF当中。在WCF中,MSMQ被用于提供安全,可靠的传输和一个统一的编程模型,同时还兼容通信标准。
MSMQ负责企业内部和外部应用程序之间可靠地传递信息。MSMQ会把发送失败的消息放置在一个队列里面,一旦目标可以访问,MSMQ会重新发送消息到目标中。它也支持消息的安全性和优先级机制。如果要消息超时或者因为其他原因失败,可以创建死信队列进行查看。
MSMQ也支持事务。它允许对多个队列的多个操作在一个事务中进行,从而保证要么全不执行,要么全部不执行。微软分布式事务处理协调器(MSDTC)支持MSMQ和其他资源的事务性访问。
微软消息队列使用以下端口:
各个操作系统中使用的MSMQ版本如下:
二、MSMQ的安装
本篇MSMQ部署在Windows Server 2008 R2中。win7/Windows Server 2008 R2 支持的MSMQ的Feature看表格:
Feature (Windows 7 / Windows Server 2008 R2) | Description |
Microsoft Message Queue (MSMQ) Server Core / MSMQ Services | This is the core service used for sending and receiving messages. |
MSMQ Active Directory Domain Services Integration / Directory Service Integration | This feature enables publishing of queue properties to Active Directory Domain Services (AD DS), default authentication and encryption of messages using certificates registered in AD DS, and routing of messages across Windows sites. MSMQ Active Directory Integration requires the computer to be joined to a domain. |
MSMQ HTTP Support / HTTP Support | This feature enables the sending of messages over Hypertext Transfer Protocol (HTTP). MSMQ HTTP support requires that Internet Information Services (IIS) be installed on the local computer. |
MSMQ Triggers / Message Queuing Triggers | This feature enables the invocation of a Component Object Model (COM) component or an executable file, depending on the filters that you define for the incoming messages in a given queue. |
MSMQ DCOM Proxy / Message Queuing DCOM Proxy | This feature enables Message Queuing applications to use a MSMQ COM application programming interface (API) to connect to a remote Message Queuing server. |
Multicasting Support / Multicasting Support | This feature supports multicasting messages to a multicast IP address and associating a queue with a multicast IP address. |
Routing Service | This feature routes messages between different sites and within a site. (note:The Routing Service feature is only available on Windows Server 2008 R2.) |
MSMQ作为功能组件被集成在系统当中,在Window Server 2008 R2 安装MSMQ非常简单,点击【开始\控制面板\管理工具\服务器管理器\功能\添加功能\依次展开MSM\MSMQ服务】。注意:要使用【路由服务】,服务器必须加入域。如图:
三、MSMQ编程开发
在讲MSMQ编程开发之前,有必要先了解一下MSMQ的队列。在MSMQ中,队列用来临时存储不同类型的消息。队列在逻辑上被划分为两组:应用程序队列和系统队列。应用程序队列可以被应用程序创建,系统队列由MSMQ创建。
应用程序队列包括消息队列,管理队列,响应队列和报告队列。
消息队列,应用程序间可以通过消息队列里面的消息进行通信,应用程序可以通过消息队列发送和接收消息。注意:应用程序创建队列后,在计算机管理单元中的消息队列中总是显示小写。然而,在MSMQ中,消息队列的名称是大小写敏感的,所以在代码中使用队列名称的时候要十分注意。例如,应用程序创建了一个名为MYQUEUE的队列,在计算机的管理单元中显示为myqueue。在应用程序中访问这个队列时,名称一定要使用大写的MYQUEUE,如果使用小写myqueue,就会抛出一个异常。
管理队列,在发送消息的应用程序中可以指定管理队列,管理队列存储MSMQ发送的确认消息,确认消息标识应用程序发送的消息是否成功。
响应队列,发送消息的应用程序可以指定响应队列,接收消息的应用程序发送响应消息到响应队列。
报告队列,消息每次通过MSMQ路由服务器传递,MSMQ会发送一条报告消息进行跟踪,报告消息存储在报告队列中。报告队列由发送程序指定和启用。
系统队列由MSMQ或者MSMQ管理员创建,包括日志队列和死信队列。
日志队列,当应用程序队列被创建后,MSMQ自动创建一条日志消息跟踪被读取的消息。
死信队列,保存未能被正确发送的消息。MSMQ提供两种死信队列,一种是非事务死信队列,一种是事务性死信队列。
队列的基本概念讲完了,下面就开始讲讲具体的使用。园子里已经有很多很好的文章介绍MSMQ消息队列的创建,消息的发送和消息的接收,我就不再重复造轮子了。但是在真实使用场景中,如果消息发送不成功,而消息又非常重要每个消息都必须处理,这种情况该怎么使用MSMQ,园子里介绍的文章倒不是很多,所以这里我就重点介绍一下管理队列和死信队列的用法。
1. 新建消息发送控制台应用程序TestAck,黏贴一下代码:
public class MyNewQueue { static void Main(string[] args) { // Create a new instance of the class. MyNewQueue myNewQueue = new MyNewQueue(); // Create new queues. CreateQueue(".\\private$\\myQueue"); CreateQueue(".\\private$\\myAdministrationQueue"); // Send messages to a queue. myNewQueue.SendMessage(); Console.ReadLine(); } //************************************************** // Creates a new queue. //************************************************** public static void CreateQueue(string queuePath) { try { if (!MessageQueue.Exists(queuePath)) { MessageQueue.Create(queuePath); } else { Console.WriteLine(queuePath + " already exists."); } } catch (MessageQueueException e) { Console.WriteLine(e.Message); } } //************************************************** // Sends a string message to a queue. //************************************************** public void SendMessage() { // Connect to a queue on the local computer. MessageQueue myQueue = new MessageQueue($".\\private$\\myQueue");; myQueue.Label = "label1"; // Create a new message. Message myMessage = new Message("Original Message"); myMessage.AdministrationQueue = new MessageQueue(".\\private$\\myAdministrationQueue"); myMessage.AcknowledgeType = AcknowledgeTypes.NegativeReceive; myMessage.UseDeadLetterQueue = true; myMessage.TimeToBeReceived = TimeSpan.FromSeconds(2); myMessage.Label = "label1"; // Send the Order to the queue. Thread.Sleep(TimeSpan.FromSeconds(3)); myQueue.Send(myMessage); return; } }
myMessage.AdministrationQueue = new MessageQueue(".\\private$\\myAdministrationQueue");
指定消息的管理队列,管理队列的创建很普通消息队列是一样的。
myMessage.UseDeadLetterQueue = true;
指定消息使用死信队列,消息发送不成功时,系统会发送消息的副本到死信队列。
myMessage.AcknowledgeType = AcknowledgeTypes.NegativeReceive;
指定当消息队列未能接收消息时,发送确认消息到管理队列。AcknowledgeTypes是一个枚举类型,枚举值:
PositiveArrival |
一个掩码,用于在原始消息到达队列时请求肯定确认。 |
PositiveReceive |
一个掩码,用于在成功从队列检索到原始消息时请求肯定确认。 |
NegativeReceive |
一个掩码,用于当未能从队列接收原始消息时请求否定确认。 |
None |
一个掩码,用于请求不发送任何确认消息(无论是肯定的还是否定的)。 |
NotAcknowledgeReachQueue |
一个掩码,用于在原始消息不能到达队列时请求否定确认。当到达队列时间计时器过期时或不能对消息进行身份验证时,可能请求否定确认。 |
NotAcknowledgeReceive |
一个掩码,用于当发生错误时请求否定确认,防止在其接收时间计时器过期前从队列接收原始消息。 |
FullReachQueue |
一个掩码,用于在原始消息到达队列时请求肯定确认,或者用于到达队列时间计时器过期后请求否定确认,或者用于不能对原始消息进行身份验证时请求否定确认。 |
FullReceive |
一个掩码,用于在接收时间计时器过期前从队列收到原始消息时请求肯定确认,否则请求否定确认。 |
备注:确认消息是系统生成的非事务性消息,它们标识由应用程序发送的消息是否成功发送到目标队列,也可以标识是消息否被应用程序成功读取。
myMessage.TimeToBeReceived = TimeSpan.FromSeconds(2);
Thread.Sleep(TimeSpan.FromSeconds(3));
myQueue.Send(myMessage);
模拟消息发送超时,消息队列未能接收消息的情况。消息的TimeToBeReceived属性指定队列接收消息的超时时间。
代码执行后,查看计算机管理单元中的消息队列情况,由于发送消息超时,所以消息队列myQueue中没有接收到消息;管理队列被指定为当消息发送不成功时接收确认消息,所以管理队列中由系统自动生成了一条确认消;消息指定使用死信队列,所以系统同时发送消息的副本到死信队列。如下图:
2.新建接收消息控制台应用程序AckClient,黏贴以下代码
class ReceiveClient { static void Main(string[] args) { new ReceiveClient().ReceiveAckMessage(); new ReceiveClient().ReceiveDeadLetterMessage(); Console.ReadLine(); } private void CreateQueue(string path) { try { if (!MessageQueue.Exists(path)) { MessageQueue.Create(path); } else { Console.WriteLine("该队列已存在!"); } } catch (MessageQueueException ex) { throw; } } private void ReceiveAckMessage() { MessageQueue myQueue = new MessageQueue(".\\private$\\myAdministrationQueue"); myQueue.MessageReadPropertyFilter.CorrelationId = true; myQueue.MessageReadPropertyFilter.Acknowledgment = true; myQueue.Formatter = new XmlMessageFormatter(new Type[] { typeof(string) }); MessageQueue mySendQueue = new MessageQueue(".\\private$\\myQueue"); mySendQueue.Formatter = new XmlMessageFormatter(new Type[] { typeof(string) }); try { Message myMessage = myQueue.Receive(TimeSpan.FromSeconds(3)); Console.WriteLine("_______________________________"); Console.WriteLine("Ack Message body: " + myMessage.Body.ToString()); Console.WriteLine("Ack Message Id: " + myMessage.Id); Console.WriteLine("Correlation Id: " + myMessage.CorrelationId); Console.WriteLine("Acknowledgment Type: " + myMessage.Acknowledgment.ToString()); Console.WriteLine("_______________________________"); } catch (MessageQueueException ex) { throw; } } private void ReceiveDeadLetterMessage() { MessageQueue myQueue = new MessageQueue("FormatName:Direct=os:.\\System$;DEADLETTER"); myQueue.Formatter = new XmlMessageFormatter(new Type[] { typeof(string) }); myQueue.MessageReadPropertyFilter.CorrelationId = true; try { Message myMessage = myQueue.Receive(TimeSpan.FromSeconds(3)); Console.WriteLine("__________________________________"); Console.WriteLine("Dead Letter Id: " + myMessage.Id); Console.WriteLine("Dead Letter Body: " + myMessage.Body.ToString()); Console.WriteLine("__________________________________"); } catch (MessageQueueException ex) { throw; } } }
ReceiveAckMessage()方法从管理队列接收确认消息。
myMessage.CorrelationId: 确认消息的CorrelationId属性标识原始消息的消息ID
myMessage.Acknowledgment: 确认消息的Acknowledgment属性标识确认消息的类型。
ReceiveDeadLetterMessage()方法从死信队列接收死信消息。
myMessage.Body: 死信消息时原始消息的副本,死信消息的body属性值与原始消息的body属性值相同。
myMessage.Id: 死信消息的消息ID与原始消息的消息ID相同。
执行效果如图:
四、Demo下载
TestAck.zip
总结:有了上面的知识,我们就可以处理未发送成功的消息。解决方案是在发送消息的程序中使用死信队列;在读取消息的程序中,读取死信队列的死信消息,然后重新处理死信消息。
结尾:示例代码是在本机创建专用队列,可以正常执行。如果在远程计算机中,不能直接使用Create方法创建队列,也不能使用Exsits方法判断队列是否存在,我的做法是在远程计算机中手动创建队列,然后配置队列的读取权限。注意,还要把【消息队列】\【属性】\【服务器安全性】\【禁用未经身份验证的RPC调用】取消。感谢园友【一个人的长征】的提醒。
结束语:关于MSMQ的知识点,感觉有好多要讲,比如队列的访问方式,事务队列的使用,日志队列的使用。如果以后有时间,我计划重新整理本篇,做成一个系列,把每个知识点都讲一讲。好了,就像写程序一样,不可能第一版就做的很完美,需要一个迭代的过程,不断的去重构,不断的去优化。