Let's shift gears now and look at another interoperability technology, MSMQ. MSMQ provides services that let you manage queues of messages and route messages between queues. While the messages have a standard format, the body of each message is application-specific. MSMQ can be used to build applications that interoperate with other messaging applications running on mainframe computers or with other time-independent applications.
The basic application model for messaging applications is straightforward. An application creates a message and sends it to a queue. Another application—or another part of the same application—reads the message from the queue and processes it. If necessary, the receiver can respond by sending its own message. The sending application is not required to wait for a response from the reader. In fact, the sending and receiving applications don't even need to be running at the same time. Sent messages are stored in the queue until they are retrieved by a receiver. The persistent storage of unread messages makes message queuing extremely useful for scenarios in which the applications are time-independent, in which machines might be disconnected from the network, or in which receivers might not be able to process requests as quickly as they are generated.
MSMQ supports three types of machine configurations: server, independent client, and dependent client. Servers can use all the features of MSMQ. Independent clients can create and modify local queues and can send and receive messages just like MSMQ servers. Local queues and messages can be created even when an MSMQ server is unavailable. However, independent clients do not have the intermediate store-and-forward capability of MSMQ servers, nor do they store information from the distributed MSMQ database. Dependent clients require synchronous access to a MSMQ server and cannot be used in disconnected scenarios.
MSMQ queues can be transactional, meaning that sending a message to or receiving a message from a queue is an atomic operation that participates in a transaction. These actions can be combined with the work required to create or process the message to ensure that the entire operation of creating and sending or of receiving and processing a message succeeds or fails as a whole. Because the acts of sending and receiving messages can occur over a long period of time, these operations are part of two different transactions. If a transaction on the receiver fails, it might be necessary to provide a compensating transaction on the sender to undo the effect of the already completed send transaction. In addition, the act of waiting for a message to be received can be a lengthy operation and is typically nontransactional. Thus, the receiving action is usually divided into two parts. A long-lived object, application, or server running outside MTS, known as the listener, is used to wait for messages coming into the queue. When a message arrives, the listener calls a method on a transactional receiver component that retrieves the message from the queue and processes it.
MSMQ supports both public and private message queues. Public queues are registered in the MSMQ Information Store (MQIS) so that they can be located by any MSMQ application. Public queues are persistent, and their registration information can be backed up; thus, these queues are good for long-term use. Private queues are registered on a local computer and usually cannot be seen by other applications. Information about private queues is stored in the local queue storage (LQS) directory on the local computer. The advantage of private queues is that they have no MQIS overhead, which means they are faster to create, have no latency, and do not need to be replicated. In addition, they can be used when MQIS is not available. Private queues can be exposed to another application by sending the queue's location to the other application.
Queue properties are defined when the queue is created. The properties can be set by MSMQ or by the application. Properties include ways to identify the queue and ways to specify who has access to the queue, whether the queue is transactional, whether a journal of queue operations should be maintained, and more. You can also use the queue properties as filters to locate public queues that have particular characteristics.
Although the properties of the queue and the default properties of messages are defined at queue creation time, some properties can be specified for individual messages. Most of these properties have to do with "level of service." For example, you can set the type of acknowledgement messages MSMQ should send when messages are received at their final destination, security properties, whether the message should be transacted or recoverable, whether the message should be stored in the journal, and the priority of the message. Another property helps you correlate response messages with the original message that was sent.
MSMQ provides two ways to access MSMQ functionality. First, it provides a set of API functions. These functions are most easily used from C or C++ programs. The MSMQ API includes functions for creating, opening, and deleting queues; functions for locating existing queues and messages in queues; functions for sending messages and reading them in queues; and functions for setting and retrieving properties.
Second, MSMQ provides a set of Automation components that can be used from any language that supports Automation, including scripting languages. These components do not support all the functionality of the MSMQ API, but they do support the functions most often needed by applications: queue lookup, queue management, queue administration, message management, and transaction support. We will focus here on these Automation components.
The basic design pattern we will discuss for building MSMQ applications is shown in Figure 15-4. A send component is defined that exposes a COM interface, IMyInterface, in which each method has only input parameters and causes an MSMQ message to be sent. This send component is usually run within MTS, using MTS automatic transactions. It is much like any other business or data object we've discussed, except that each method opens a queue, builds a message, sends the message, and closes the queue. A listener application is created to watch for new messages in the queue. When a message is detected, the listener calls a receiver component. The receiver component exposes the same IMyInterface interface as the send component as well as an interface for triggering message reads, IReadQueue, which has one method, MessageArrived. The listener component calls MessageArrived, which receives the message from the queue, decodes the message, and calls the appropriate method of IMyInterface. The implementation of each IMyInterface method does the actual processing of the method. The receiver component is also usually a transaction component running within MTS.
Figure 15-4. A design pattern for using MSMQ in a three-tier application.
IMyInterface and IReadQueue are representative interface names used to describe this design pattern. You should define equivalent interfaces in your component, with unique IIDs, to implement this pattern.
The Automation components provided by MSMQ are listed in the following table.
|MSMQQueueInfo||Provides queue management|
|MSMQQueue||Represents an instance of an open queue|
|MSMQMessage||Represents a single message|
|MSMQQuery||Provides the ability to search for public queues|
|MSMQQueueInfos||Represents a collection of MSMQQueueInfo objects returned by MSMQQuery|
|MSMQEvent||Provides asynchronous notifications of message arrival|
|MSMQApplication||Provides a way to retrieve the machine identifier of a computer|
|MSMQTransaction||Provides a way to commit and abort transactions|
|MSMQTransactionDispenser||Creates a transaction object for internal transactions|
|MSMQCoordinatedTransactionDispenser||Creates a transaction object for external transactions coordinated by the MS DTC|
The MSMQQueueInfo component can be used to create a new queue, open an existing queue, change a queue's properties, or delete a queue. There is one MSMQQueueInfo object for each queue. MSMQQueueInfo objects can be created directly or returned by a query.
An MSMQQueue object represents an open instance of a queue. This object is created when you open a queue using an MSMQQueueInfo object, and there can be multiple MSMQQueue objects per queue. The MSMQQueue component provides cursorlike behavior for moving through the messages in a queue. At any given time, the object is positioned at a single message.
An MSMQMessage object represents an individual message. You can use this component to create and send messages. The body of a message can be a string, an array of bytes, or any persistent COM object that supports IDispatch and IPersistStream or IPersistStorage. Each MSMQQueue object contains an implicit set of MSMQMessage objects that are accessed via the MSMQQueue Peek and Receive methods.
The MSMQQuery component can be used to retrieve a set of public queues that meet particular criteria. The set of queues is returned as an MSMQQueueInfos collection of MSMQQueueInfo objects.
The MSMQEvent component is used to implement an event handler that receives notifications when messages are received at a queue or when certain error conditions occur. Each queue can be associated with one MSMQEvent object and the object can be associated with multiple queues, which lets you write a single generic event handler to perform common tasks. A reference to an MSMQQueue object is passed to the event handler, so it can perform application-specific processing as necessary.
The remaining components provide utility functions for MSMQ applications. The MSMQApplication component simply provides a method to retrieve a machine identifier. MSMQTransaction objects are created by either MSMQTransactionDispenser or MSMQCoordinatedTransactionDispenser objects when MSMQ applications want to programmatically control transaction boundaries, either using internal transactions or using the MS DTC, rather than using MTS automatic transactions.
Every component or application that needs to send or receive messages will need to create or open a queue and then close the queue when it has finished its work. All queues are created by calling the MSMQQueueInfo Create method—you create an MSMQQueueInfo object, set the queue properties you want, and then call Create.
The only required property for creating a queue is PathName. The PathName property has the format <machine name>\<local queue name> for public queues or <machine name>\Private$\<local queue name> for private queues, where <machine name> indicates where the queue's messages should be stored and <local queue name> specifies the queue name. For example, the following code snippet creates a private queue named MyQueue on the local computer and returns a reference to the MSMQQueueInfo object:
Function CreatePrivateQueue() As MSMQQueueInfo Dim qinfo as New MSMQQueueInfo qinfo.PathName = ".\Private$\MyQueue" qinfo.Label = "My Private Queue" qinfo.Create Set CreatePrivateQueue = qinfo End Function
Once the queue is created, the MSMQQueueInfo object's FormatName property can be used to open the queue. Queues are opened by calling the MSMQQueueInfo Open method. You can get the MSMQQueueInfo object by creating the queue or by using an MSMQQuery object to locate an existing queue. When you open a queue, you specify the type of access you want and the type of sharing you want. You can obtain access to send messages, receive messages, or peek at messages in the queue. If you have receive access, you can also peek at messages. However, if you have peek access, you cannot receive messages. If you want receive access to the queue, you should also indicate whether anyone else can receive messages from the queue at the same time. For example, the following code snippet opens a queue once for sending messages and once for receiving messages with no sharing:
Dim qinfo as MSMQQueueInfo Dim qSend as New MSMQQueue Dim qReceive as New MSMQQueue Set qinfo = CreatePrivateQueue() Set qSend = qinfo.Open(MQ_SEND_ACCESS, MQ_DENY_NONE) If qSend.IsOpen then ' The queue was successfully opened for sending messages. Set qReceive = qinfo.Open(MQ_RECEIVE_ACCESS, _ MQ_DENY_RECEIVE_SHARE) If qReceive.IsOpen then ' The queue was successfully opened for receiving messages. End If End If
When you have finished using a queue, you simply call the MSMQQueue Close method to close the queue.
Sending messages is a straightforward process. After opening a queue for sending, you create an MSMQMessage object, set its properties, and call the Send method. If you have created a transactional queue, the message will participate in MTS automatic transactions by default. The Body property of the object is used to specify the contents of the message. This property can be set to any intrinsic Variant type, array of bytes, or a persistent object. The MSMQMessage object figures out what type of data is provided by inspecting the Variant data assigned to the Body property. Be aware that when you assign an object to the Body property, you should not use the Microsoft Visual Basic Set operator because the actual contents of the Variant data are copied into the message. The code snippet on the following page demonstrates how to send a string in a message using the default message properties.
Dim mSend as MSMQMessage mSend.body = "This is the message body." mSend.Send qSend
If your applications send multiple types of messages to the same queue, you will need to include some identifying information in the message body so that your receiving application can properly decode the message. You will also need to document the format of each type of message body so that receiving applications know how to process it.
Receiving messages is slightly more complicated. You can read messages synchronously or asynchronously. When a message is received synchronously, execution of the receiver is blocked until the message is available or a time-out period expires. When a message is received asynchronously, execution of the receiver continues until Arrived or ArrivedError events are received by an MSMQEvent object registered with the queue. However, even with asynchronous reads, the act of receiving a message might occur over a long period of time. Thus, it is usually impossible to wait for messages to be received within a transactional component.
To work around this issue, you can use a long-lived listener object, application, or server that opens the queue with peek access and waits for messages to be received. When a message is received, the listener calls a method on a transactional receiver object, which then reads the message from the queue and processes it. The following code fragment demonstrates how a simple listener might be coded, using the design pattern described above and assuming that the MSMQQueueInfo object has already been initialized:
Dim qPeek As MSMQQueue Dim msgPeek As New MSMQMessage Dim myReceiveObj As IReadQueue Set qPeek = qinfo.Open(MQ_PEEK_ACCESS, MQ_DENY_NONE) Set myReceiveObj = CreateObject("MyReceiveObject") On Error Resume Next do while (TRUE) Set msgPeek = qPeek.Peek(60000) If (Err = MQ_ERROR_IO_TIMEOUT) Then Loop ElseIf (Err <> 0) Then ' Handle error. For now, just exit. Exit Do Else ' Let receiver know there's a message. myReceiveObj.MessageArrived End If Loop . . .
The receiver component's implementation of IReadQueue MessageArrived could use synchronous reads to retrieve messages from the queue, which it opens for receive access, as shown in the following code fragment:
Dim qRead as MSMQQueue Dim msgRead as New MSMQMessage Set qRead = qinfo.Open(MQ_RECEIVE_ACCESS, MQ_DENY_RECEIVE_SHARE) Set msgRead = qRead.Receive If msgRead is Nothing Then ' Process error. GetObjectContext.SetAbort Else DecodeMessage msgRead ' Assume no errors (would call SetAbort on error). GetObjectContext.SetComplete End If
The DecodeMessage method would contain the code to parse the message and call the appropriate method of IMyInterface.
Once you have created send and receive components and a listener application, you will need to deploy these as part of your distributed application. If you want message sends and receives to participate in transactions, you should install the send and receive components in MTS packages, as described in Chapter 10. Since the send and receive components are likely to be used on separate machines, you should put them in separate components. The components should be marked as "requires a transaction" to ensure correct transactional behavior. The listener application should not be installed in MTS.