June 1999
Building a Store-and-Forward Mechanism with Windows 2000 Queued Components |
It's easier to write distributed apps if you do not need all the participants to be running at the same time. COM+ Queued Components provide such an infrastruc-ture, and communication between the client proxy and server stub is handled with Microsoft Message Queue Services. |
This article assumes you're familiar with MSMQ, Visual Basic, and COM |
Code for this article: QueuedComp.exe (17KB) David Platt is president and founder of Rolling Thunder Computing, Inc. (http://www.rollthunder.com). He teaches COM and
COM+ at Harvard University and at companies around the world. This article is based on a chapter from his forthcoming
Microsoft Press® book, Understanding COM+. |
This article was written based on a prerelease version
of Windows 2000 beta 3. Details mentioned here may change prior
to the release of Windows 2000. When was the last time you telephoned someone and found him at his desk the first time you called? Been a while, hasn't it? When you fail to contact him, do you call every five minutes until he answers, getting nothing done for the rest of the day? Of course not. You leave a voice mail message: "Charlie, it's Dave, I'll meet you in the pub on Mass. Ave. at 5:30; please bring the contract and your thirst. Call me at 555-1234 if you have a problem." Charlie picks up his voice mail at his convenience, and either shows up or calls to cancel, probably leaving a message on your voice mail. That's enough for most business purposes most of the time. Think about how many one-way voice mails, emails, and snail-mails you send versus how many two-way face-to-face contacts or direct phone conversations you have. It's easily ten-to-one in favor of the former and increasing each day. The same problem arises in distributed computing. Suppose you are writing an app for a telephone order taker in a mail order store. The order taker talks to the customer on the phone and types the order into his PC workstation. Some parts of this operation need to be completed synchronously, while others don't. For example, checking available stock on the ordered items needs to be done immediately. The customer needs to know what she can have delivered by her deadline. Other parts of the operation do not need to be completed synchronously, as no later parts of the operation depend on them. For example, there's no reason for either customer or order taker to wait while the completed order is sent to the computer in the shipping department. What happens when the shipping department's computer is down for maintenance, or is simply too swamped? You can't tell your customer to call back later, and maybe the computer will be back upshe won't do that, and she won't call your store the next time she wants something; you've lost both the sale and the customer. You can let the customer go and have the order taker wait at the console until the shipping computer comes back up, but that's not great eitheryou only have so many order takers, and the incoming customers will get tired of waiting in the phone queue and hang up, probably cursing your ancestry as they do solost sales and customers again. What you really need is some way for the order taker's computer to leave a message for the shipping computer. You want voice mail or email whose contents are readable by computer programs rather than humans. When the shipping computer comes back up or digs out of its backlog, it will check its messages and handle them at that time. Communication with the shipping department's computer does not need to happen synchronously, therefore it should happen asynchronously. The only thing that the order taker's computer cares about in this case is knowing whether the message did in fact get placed in the outgoing mailbox from which it will eventually get delivered to the shipping department's computer. What advantages would such an architecture have? The most obvious is that you would no longer have to worry about server availability for operations that can be handled asynchronously. Neither client nor server would need to care about the lifetime of the otherjust that there are enough server cycles around to eventually handle all client requests (where "eventually" is defined as soon enough for your business purpose) and enough space in the inbox to hold all pending messages. This is not just important in the case where the server goes down temporarily from a malfunction. Close to half of all new PCs sold are portable models. The user wants to take his PC somewhere, do some work on it, and at some later point connect to a server to process the data generated during the disconnected operation. You already do this with human email messages, creating them offline and sending them when you find a convenient phone line. Extending this communication mechanism to programs as well as humans is not an enormous leap. A disconnected architecture could also improve performance and scalability of server applications by allowing more efficient use of server resources. For example, different server processes could be assigned different priorities. If the same server were used for both the in-stock products database and the shipping department, the administrator could assign a lower priority to the shipping process. That way the server could offer fast response for the in-stock product queries that need to be handled while the customer waits, and catch up on the shipping requests in the middle of the night when telephone customer demand fell off. A second advantage is that the customer's shipping request message would arrive containing all the information that the server needed to process it. Suppose the server handled each new order by creating a new object at the beginning of the order process. The client app would call methods on this object, setting product number, quantity, address, and so on, in response to the user's interaction with the UI. The server would finally process and release the shipping component when the user clicked Send. The server's shipping object would need to exist throughout the human operation, which means you have a lot of objects hanging around, using up memory and other resources, while waiting for humans to process information at human speeds. If the client app sent all of its data in a single message, the object that processed that message would need to live only as long as it took the server to think about its data. All of this adds up to the same thingit's a lot easier to write most distributed applications if you do not need all the participants to be running at the same time. You could certainly write client code that would check for the presence of a server and use it if found, otherwise saving the data in its own database and making the call when it detects a server again, but developing this "store-and-forward" enterprise infrastructure would take time and cost money. It would be much better to inherit such an infrastructure from the operating system. Solution Architecture Such an infrastructure is the Queued Components (hereafter QC, formerly known as "real enough time") service of COM+. A client app creates a queued object the same way it would any other COM object. The queued object exposes a COM interface to its client that looks more or less like any other COM interface. Classic DCOM uses synchronous RPC to squirt the bytes representing a COM call from one box to another box, as shown in Figure 1. With QC, the communication between the client-side proxy and the server-side stub is handled with Microsoft Message Queue Services (MSMQ) instead, as shown in Figure 2. |
Figure 1 COM Using RPC |
When a client activates a queued object, the client is connected not to the actual COM object, but rather to a call recorder. The client app makes COM calls as usual, but they get recorded on the client side instead of being squirted out immediately via RPC. When the client deactivates the component, QC uses MSMQ to asynchronously send the bundle of recorded COM calls to the server on which the actual component lives. The configuration controls when the server reads messages from the queue. The server uses MSMQ to read in the message containing a bundle of COM calls sent by a client. The server activates a player component that creates the actual COM object and plays the recorded calls into it. |
Figure 2 COM Using MSMQ |
You don't have to think about the details of MSMQ; they've been abstracted away. Your client app just makes COM calls the way it's used to doing and the underlying system infrastructure makes the right thing happen, even though the client and server aren't necessarily running at the same time.
Writing queued components is also easy. You just write a COM component the way you are already used to doing, with whatever tools you like (Visual Basic®, ATL, and so on). You will have to make a few changes to the semantics of your interfaces, essentially getting rid of output parameters (as I'll describe in more detail toward the end of this article), but these are relatively minor. Next, install your component in a COM+ application, marking its interfaces as queued via the Component Services snap-in. COM+ will then take care of listening for incoming MSMQ messages from clients and calling your component's methods when they arrive. Again, the nasty details have been abstracted away by the QC service. You write a component incorporating your business logic, and inherit the infrastructure from the operating system. MSMQ is a rich set of runtime services that allows applications on different machines to send and receive asynchronous messages to and from each other. It first appeared in the Windows NT® 4.0 Option Pack in December of 1997, and is an integral part of Windows® 2000. An app that wants to receive messages creates a queue, a persistent administrative structure known to the operating system that is conceptually similar to a mailbox. An app that wants to send a message to another app locates this queue, either through system services or through application-based foreknowledge, and uses the MSMQ service to send a message to it. An MSMQ message can contain plain text or COM objects that support the IPersistStream interface. Messages pass through the network topology configured by the system administrator until they reach the recipient machine. If the recipient queue's machine is not running when the message is sent, the message is buffered by the sending machine's operating system or the intervening network machines until the recipient machine starts running. An application can receive MSMQ messages either through polling its queue or using callback notification. MSMQ also contains a rich set of features to support this basic message delivery service. For example, the sender of a message may specify that the message will expire and be deleted if it does not reach the recipient's queue within a certain time interval, or if it is not read from the queue by the receiving app within a different time interval. The sender may ask to be notified when the message is received, or only if it expires unread, or not at all. For a more detailed description of MSMQ, see "Microsoft Message Queue is a Fast, Efficient Choice for Your Distributed Application" by David Chappell (MSJ, July 1998). Since MSMQ is a good foundation, why do you want the higher level of abstraction provided by QC? There are three reasons. QC is easier to program than raw MSMQ and takes care of the nasty details that are the same in most implementations anyway. Second, you don't have to learn a new programming model. You probably already use COM in many places in your app, for example, using ActiveX® Data Objects to connect to databases. With QC, you don't have to change mindsets to program MSMQ. Third, the COM components that you write can be used either asynchronously through the QC service or synchronously through DCOM if you prefer. So you have the flexibility of using queued access to components where it makes sense and nonqueued access to the same binary component where you need it. It is the client's or caller's choice how to instantiate the component. The main drawback of using QC instead of raw MSMQ is the same drawback you find with any higher level of abstraction: a loss of fine-grained control. You can't do absolutely everything through QC that you could if you were using MSMQ directly, in the same way as you can't get quite everything done in Visual Basic that you can in Visual C++®. You can handle most cases most of the time, and that's usually enough to make lots of money. Since the greatest constraint on any software development project is time to market, you don't really have any economic choice but to use any higher level of abstraction that shortens it. How much programming have you done lately in assembler language? Programming raw MSMQ doesn't take that long. Programming QC is an order of magnitude faster. VBShip Let's look at the QC way of writing a mail order shipping app similar to the one discussed earlier. My sample program is called VBShip. The key point to keep in mind as you follow the example is that, as with so many parts of COM+, neither the client developer nor the component developer needs to do very much to tie into the infrastructure (in this case, queued messaging) inherited from the operating system. In the following example, all the component author has to do is use the Component Services snap-in to mark the component's interfaces as being queued. All the client developer has to do is create an object in one particular way that causes COM+ to treat it as queued. The rest of the complex dance is performed by the operating system. You write your business logic in the form of a COM component, then let go and let COM+.
The administrator first sets up the required applications and components using the Component Services snap-in. The administrator creates a new COM+ server application here called VBShipApp, and marks it as Queued, as shown in Figure 4. This causes COM+ to create message queues for the use of any queued components that may be added to the application. You can see these queues with MSMQ Explorer, as shown in Figure 5. Checking the Listen box (see Figure 4) tells COM+ that when the app is launched, COM+ should activate the listener service that it needs to receive incoming calls via MSMQ. |
Figure 4 Queuing Options |
Next, the administrator adds components to the application. The interfaces of the components also need to be marked as Queued, as shown in Figure 6. This tells COM+ that it should expect to receive incoming calls to this interface via MSMQ. This setting only affects the incoming calls received by the component; it does not affect the outgoing calls made by that component to other components, which can be any mix of queued and nonqueued. The checkbox will be disabled unless the component has been designed in a manner that allows it to be accessed in a queued fashionessentially removing any kind of output parameters, as I'll describe later on. |
Figure 5 Viewing Queues with MSMQ Explorer |
The VBScript used by the base client is shown in Figure 7. The base client does not create the VBShip object via the CreateObject or CoCreateInstance functions; these instruments are too blunt. Instead, it uses the Visual Basic function GetObject, whose C++ equivalent is CoGetObject. This function uses a text string to activate a series of |
Figure 6 Configuring Interfaces |
monikers to create the VBShip object. A moniker is a COM object that knows how to create another COM object based on a text string provided to the moniker. Monikers have been around since the early days of OLE, where they were originally used for compound document linking. For more information on the whys and wherefores of monikers, consult the ActiveX®/COM Q&A column in the July 1997 issue of MSJ. |
Figure 8 The Component Services Snap-in |
By using the moniker instead of calling CreateObject, the client doesn't actually create a VBShip component. Instead, the client is connected to the Queued Component Recorder, a COM+ component that is part of the runtime system. You can see it in the COM+ Utilities application in the Component Services snap-in, as shown in Figure 8. While this technique may sound exotic, there's really nothing new here. You know that when a client creates an object, it often receives a proxy instead of a direct connection to the object. Even creating a proxy at runtime based on type library information is not new; dual interfaces do that all the time. You can simply think of the recorder as a different kind of proxy, one that squirts its calls into MSMQ instead of RPC.
The client now uses the recorder the same way as it would any other COM object. It calls the methods and sets the properties that do the business things the client wants done. In this sample, the client app sets the properties representing the customer ID and order ID, orders a bunch of seasonal items, then calls the Process method to tell the object to process the order as entered. The recorder knows from reading the type library describing the VBShip component what methods and properties it contains and what their parameters are. When the client makes its calls, the recorder catches them and stores them internally until the entire series of COM calls is complete and the component is deactivated. In this sample, this happens when the object goes out of scope at the end of the script. Then the recorder transmits all of its recorded calls in one MSMQ message. MSMQ then transmits the message containing these recorded calls to the machine on which the VBShip component is actually located. Note that there is no way to flush the recorder's call buffer without deactivating the component. However, by using a queued component, the client is saying that it doesn't care exactly when the calls get processed. So this really shouldn't be a problem. |
Figure 9 A Sample Message |
The message containing the client's recorded calls has now been handed over to MSMQ, which will transmit them to the recipient machine. A sample message containing recorded COM calls is shown in Figure 9. The message is transmitted in accordance with the administrative configuration of MSMQ and the network topology of the installation on which the client and server are running. If both are on the same network, running, and not too busy, the message can take a ballpark figure of perhaps 10 milliseconds to arrive at the recipient queue. If the two machines are disconnected, MSMQ will store the message containing the recorded COM call, and automatically forward them the next time the machines are connected. MSMQ uses a transacted protocol in the forwarding process, making sure that messages are acknowledged by the recipient before being erased from the sender queue so that messages don't get lost. Don't forget that you need to worry about the client machine physically disappearing or breaking before the recorded calls get forwarded.
QC provides a utility app called QC.ListenerHelper, shown in the Component Services snap-in in Figure 8. This listener must be activated to receive incoming QC calls. The activation may occur either by the system administrator using the Component Services snap-in, as shown in Figure 4, or by a utility program using the COM+ catalog administrative objects. A Listener component waits for messages to arrive in the application's input queue. When the message arrives, the Listener starts an instance of the QC ListenerHelper object (a transacted component) and the ListenerHelper dequeues the message while in a transaction. The ListenerHelper then creates an object of the QC.Player class. This object creates the actual VBShip object on the server side, and makes the actual calls into it. Again, you will have the right mental model if you think of this player as a COM stub for the VBShip object. Interface Design The logistics of QC are fairly easy and seem to require little thought. That's a misleading view. The direct interaction of your queued components with COM+ is indeed simple and doesn't take much effort, similar to attending synagogue on Saturday. However, living day-to-day life in accordance with the constraints imposed by Judaism, for example, making sure that kosher food is available for your children three times a day, seven days a week, requires advance thought and planning. Designing your queued components to operate successfully within the constraints imposed by COM+ also requires advance thought and planning. All the COM code you've ever written in your life has been written with the assumption that a client was always talking synchronously to the objectperhaps through the unseen mediation of a proxy and stubbut the other guy was there and he was listening when you spoke and would return information to you using output parameters passed in your COM calls. Each COM call had at least the potential for bidirectional communication, through a returned HRESULT if nothing else. The lifetimes of client and object had to overlap. That's no longer the case in QC. All interfaces used in QC need to be unidirectional, so they can be recorded and played back at a later time, except for IUnknown and IDispatch, which are special. This means that you can't use any output or input/output parameters in the interfaces of your QC components, because you don't know when the server will get to your request, or if you'll even still be running then to receive the output. The .IDL flags [out], [in, out], and [retval] are taboo. For example, the following method, standard in synchronous COM, will not work in QC because the last parameter is used for output: |
|
In QC, this method needs to be rewritten to use only input parameters: |
|
The COM+ catalog checks each interface in a component's type library when the component is installed into an application. If the interface meets these criteria, according to the description in the type library, then it is deemed to be queueable and the Queued checkbox is enabled. This allows an administrator to set the interface for queued access if desired. Otherwise, it will be grayed out, and no one will be able to access that interface via QC.
Not only must the parameters of a queued component be unidirectional, they must also be passed by value. Passing parameters by reference is not supported. Persistent COM objects (those objects that support the IPersistStream interface) can also be passed. Writing components in Visual Basic can present a problem here. By default, Visual Basic passes all parameters by reference using the [in, out] attribute, which QC does not allow. If you want to use Visual Basic to write queued components, you must specify the ByVal attribute for passing parameters. The sample code does this. A more subtle limitation of QC is that the series of COM calls transmitted by a client during a session with a component needs to be self-sufficient. Think about the voice mail I left earlier. I stated my phone number and the address of the pub even though Charlie probably ought to know them. Since the communication was asynchronous, he couldn't ask me for them if he didn't. You need to do the same thing with the design of your interfaces: make sure that the client is forced to directly pass everything the server needs to know because the server can't make callbacks to the client to request missing pieces of it later. For example, it is common programming practice for a COM client to pass data sets to an object in the form of collections or enumerator objects. The client then becomes in essence the server of the collection or enumerator object, and the original object becomes the client of it. The server makes a call to the client to fetch each item in the collection. This will not work in QC because the two sides' lifetimes can't be required to overlap. If a client has a set of data values that it needs to pass to the server object, it could use multiple recorded COM calls, as shown in the VBScript sample earlier. Alternatively, you could pass the data as a dimensioned array. Finally, QC component methods may not return application-specific error result codes to their callers. Remember how calls are recorded on the client and played back on the server some indeterminate amount of time later. The code that implements the component's business logic and that would generate that error result code isn't available when the client makes the call and receives the result code. The return value that the client receives indicates only whether the call has been successfully recorded. Receiving Output Obviously, there's only so much you can do with one-way communication. Nobody sells many write-only memory chips. So let's discuss the different mechanisms for receiving output from a QC server. It mostly comes down to your business process requirement. Consider the following three classes of users. An optimist never requires any output as the result of a QC call, other than the return code from the client-side QC recorder saying that the call has been successfully recorded and enqueued. The client's job is finished when the message is recorded, in the same way that writing a thank-you note to your mother is finished when you put it in the mailbox. You don't usually spend much time finding out if she got it. This worldview works well for applications whose only task is input, for example, a grocery store application in which a stock clerk carries a Handheld PC device on her regular rounds to record the number of bags of potato chips on the shelves. The device performs input recording only. Once the inventory has been transmitted, the Handheld PC device doesn't care about them any moreas long as they actually do get delivered eventually, of course, and that's the operating system's problem. The clerk does not require a confirmation number for each input of potato chips. Any later checks that you want to put in the system are carried out by other applications using other channels. A pessimist requires a response for every call, similar to RPC. You shouldn't do this. If you find yourself operating in this mode, you're missing the whole point of QC and need to change your mindset. If something needs to be synchronous, make it synchronous. Vive la difference. A paranoid user requires response messages for some calls, but not for others. You would use this approach when the client has a continuing interest in the successful completion of the downstream business process triggered by the QC call. Think what happens when you order something by phone. The order taker types in your order and gives you an order confirmation number. If your order doesn't arrive by the time it's supposed to, you call customer service and give them the confirmation number so they can track it. That's the sort of thing that a paranoid QC client would like to receive from a QC server. Since the order entry is asynchronous, the order response should also be asynchronous, using a QC response object to communicate with the client machine. The server could certainly be hardwired to always respond to a specific object on a specific machine, as in the VBShip sample, but this isn't very flexible. There are two ways in which the client can tell the server how to respond when it gets around to it. First, the client can provide a string containing its machine name as a parameter to the original QC interface. The server will use the string in its own queue moniker when creating the callback object. Passing data to the moniker in this fashion is described later. Instead of a string, the client app passes an actual pointer to a queued object on its own machine for the server to use as a callback. Although COM objects may not generally be passed as parameters in QC, passing a queued object for use as a callback is supported as a special case. The client app uses a queue moniker to create a QC of the callback class on its local machine and gets a recorder that mimics the callback object. It passes this recorder as a parameter to an outgoing QC call, essentially passing the first recorder to a second recorder that is handling the outgoing call. The outgoing QC call marshals the callback recorder object into its outgoing call buffer, after which the client can release the callback object. When the QC player on the server plays its incoming calls, it will unmarshal the reference to the client-side callback object and create a new recorder on the server side that is pointed at the right queue on the client side. Finally, when considering mechanisms for output, give some thought to avoiding it. For example, in the telephone order taker situation discussed earlier, you might want to generate order numbers on the client PC rather than waiting for them to come back from the server, perhaps using a GUID to ensure that they are unique. As I said before, even though it might seem familiar, QC is a new mindset, and adjusting to it will take some thought. Using Transactions You've seen for several years now how useful transactions are in solving the problems of maintaining data integrity in distributed systems. Many of these same problems also apply to asynchronous operations. Accordingly, queued components are capable of using transactions, both externally to cooperate with other components and internally for its own reliability. MSMQ, which underlies QC, is a resource manager, meaning that it can enlist in transactions. For example, within the context of a transaction components can make changes to tables in SQL Server and enqueue messages with MSMQ. If the transaction commits, the SQL changes will be made permanent and the MSMQ messages will be sent. If the transaction aborts, both will be discarded. For more information on the transactional abilities of the underlying MSMQ system, see "Use MSMQ and MTS to Simplify the Building of Transactional Applications" by Mark Bukovec and Dick Dievendorff (MSJ, July 1998). The transaction support of the underlying MSMQ transport mechanism is available to QC. The QC.Recorder utility component takes on the same transaction attribute as its client. If a client creates a queued component and receives the recorder within the context of an existing transaction, the transaction is propagated to the recorder. When the recorder is deactivated and sends its bundle of calls to MSMQ, it does so within the context of this transaction, so MSMQ buffers the message pending the outcome of the transaction. If the transaction commits, MSMQ transmits the message containing the recorded calls to its recipient. If the transaction aborts, perhaps because a different component was unhappy about something else, MSMQ throws the message away and the recorded COM calls never happen. QC uses transactions on the server side as well. The dequeuing operation is transacted for reliability. Suppose the player dequeued a message and then some idiot knocked the computer's plug out of the wall. If the message queue was not protected by a transaction, the message would be lost. Since the dequeuing operation is transactional, when the machine's power comes back on, the transaction will abort and the message will get put back on the queue. If the downstream components are also transacted, you are assured that the message representing the incoming COM calls is processed exactly once. Transacted playback has other advantages as well. In QC, what happens if an operation fails on the server side? The client isn't around to retry it. The automatic use of transactions by QC can do it for you. QC listener helper on the server side is the root of a transaction. If a server side operation aborts for some reason, the message that triggered it is put back on the queue for a later retry. Maybe the machine that the object needed to do its work will be back up the next time it gets tried. If the operation was not transacted, you would have to write your own retry mechanism. Now you can inherit one from the operating system. Some incoming messages may cause a transaction to abort repeatedly, for example, a message from an older remote client that doesn't have the current database schema. The shipping component tries to enter it into its database, the database complains, the component aborts, and the message goes back on the queue. Next time the component dequeues the same message, the same thing happens, and so on forever. You can't allow your entire enterprise app to hang because of one poisonous message. Fortunately, the automatic retry mechanism contains a built-in solution. When an incoming message aborts, QC moves the message to a different queue so that it doesn't block the others. The QC infrastructure currently creates six private queues for this purpose. It then periodically retries the message. An aborted message starts in queue 0 (in this example, VBShipApp_0) and moves upwards as future retries continue to fail. Higher numbers indicate longer intervals between retries. The number of retries at each level and the time intervals between them have not been finalized at the time of this writing. They will probably not be configurable, at least in the first release of COM+. After using up all of its retries at lower levels, an incoming message will be moved to the DeadQueue, from which it will not be retried. It is up to you to write the administrative code you need to check this queue and deal with its problems in whatever manner is appropriate to your business operation. You might call a human operator to fix the problem, say, turning on the server machine that the downstream operation was trying to reach, and move the messages back to a lower queue. A utility component called the MessageMover is available to help with this operation. For the hypothetical obsolete client that I proposed, you might gather diagnostic information and delete the message. Obviously, dealing with an aborted order for a cup of coffee will require different procedures than dealing with an aborted nuclear launch order. COM+ did its best for you; now you're on your own. The robustness and versatility of using transactions for QC messages comes at the price of slower performance than without transactionsalthough, since all the recording is done in-process, the client will still finish its task more quickly than if it was actually talking to a remote server via DCOM. In a distributed system, you'd usually rather have the reliability, especially since, by using QC, you've said that a response time of "eventually" is acceptable. Sometimes you might want the last possible microsecond of speed instead. In this case, it is somewhat difficult and kludgey, but possible to turn off the transactioning mechanism used by QC. Queues in MSMQ are either transacted or nontransacted. This attribute is set at the time of the queue's creation and cannot be changed later. The queues created by the Component Services snap-in when you mark the application as queued are always transacted. You must use the MSMQ Explorer to create a new nontransacted queue with the name of a COM+ application that hasn't yet been marked as queued. If you then use the Component Services snap-in to mark that application as queued, it will use the existing queue with the same name (the nontransacted one you just created) rather than creating a new transacted one. All of QC's internal operations will then use the nontransacted queues and execute somewhat faster. However, mixing nontransacted queues with components that require transactions raises interesting correctness issues. If you don't care about coordinating your operations with those of other components, and if a power failure wouldn't kill you, and if you don't mind the extra administrative effort, then you might be able pick up a few extra microseconds by turning off transactioning. That's not usually what you're after, so think twice before you do this. Security Requirements A queued component has the same security requirements as a nonqueued component. When a server receives a queued request saying that Dr. Jones wants to prescribe morphine for patient Smith, it still has to answer the same two questions: is it really Dr. Jones making the request, and is Dr. Jones allowed to prescribe morphine? You also have to make sure that no unauthorized person even sees the request for morphinemaybe someone would steal the unconscious patient's wallet. The security mechanism in a disconnected, asynchronous system works on different principles than one in a connected, synchronous system. COM+ uses an enhanced version of the role-based security familiar to users of MTS. If you stick to it, you will find that the same security code will work for both the queued and nonqueued components. If you start delving too deeply under this layer of abstraction, you're going to find it tricky in QC. For example, Kerberos security tickets expire after a certain amount of time, and caring about time is anathema to the QC world. At the very least, you will wind up writing different code for synchronous versus asynchronous security, an effort you don't really want to make. The first question is authenticationis the caller really who he says he is? The challenge-response mechanism used for DCOM doesn't work with MSMQ, because DCOM requires the components to communicate synchronously, which MSMQ cannot guarantee. MSMQ uses a different authentication based on digital signatures that travel with a message. When MSMQ sends a message, it attaches a digital signature to the message. It also calculates and encrypts a hash value of the message's contents to prevent tampering. When the message arrives at the receiving end, MSMQ checks the signature, recalculates the hash value, and places the message in the dead letter queue if either is not correct. When the client creates a queued component, QC turns on the MSMQ authentication mechanism if the server application's settings on the client machine have any security enabled or the client specifically requests authentication by specifying the authlevel attribute in its call to GetObject. This causes MSMQ to send the message containing the bundle of COM calls with a digital signature and encrypted hash value. If its role-based security feature is enabled, the server app requires the client to have sent its message with MSMQ authentication. Any incoming messages that do not carry the signature saying that MSMQ is sure of the sender's identity, or whose hash values indicate that they were tampered with in transit, are placed in the MSMQ system dead letter queue. QC never sees them. The second question is that of authorizationnow that you are sure who the caller is, can the caller do the thing that he's asking to do? COM+ abstracts away this problem quite nicely with the role-based security checking familiar to users of MTS, and QC fully supports this mechanism. Each QC call contains security information about the caller, specifying the roles to which the caller belongs. Any administrative or programmatic role checks that the server components contain will work properly without modification whether the caller is synchronous or queued. What about the transmission of sensitive data, which is essentially all data, on the wire? Unless you can secure every inch of network wire, and essentially nobody can, every piece of data you send could wind up tomorrow on the front page of USA Today. Most apps will want some form of encryption for at least some operations. Synchronous DCOM provided this capability when you set the authentication level to Packet Privacy. MSMQ provides a privacy-level setting. The client must set the PrivLevel attribute to the value MQMSG_PRIV_LEVEL_BODY in the call to GetObject that creates the recorder. Setting this privacy level causes MSMQ to encrypt before transmitting the message containing the bundle calls recorded from that object. The EncryptAlgorithm attribute allows you to specify the algorithm used for this encryption. Using the Queue Moniker MSMQ is a very feature-rich environment, surprisingly so for a version 1.0 product. An important part of designing the QC system was determining which features of the underlying MSMQ would be exposed at the QC abstraction level and how that exposure should be accomplished. The mechanism shown here is the result of much skull sweat. A client creates a queued connection to a component by calling the function CoGetObject or its Visual Basic equivalent, GetObject. An example of the latter was discussed previously, and shown in Figure 7. The line that activated the QC recorder to record calls and transmit them to the server was: |
|
The function GetObject uses monikers to create the recorder that simulates the presence of the actual com-ponent. Using this mechanism instead of the classic CoCreateInstance or GetObject functions solves two problems. First, it lets you specify the initial state of
the objectfor example, the computer name to which the recorder will deliver its messagesat the time you create it. Second, it allows you to
use the same binary component in both queued and nonqueued situations.
A moniker is a type of object that knows how to create other objects. A class factory is another object that knows how to create other objects, but there's no way to pass the class factory any information about the object's initial state. If you think of a moniker as a class factory to which you can pass an initialization string, so that the moniker can return a new object with its initial state already set, you will approach the right mental model. The process by which a moniker creates an object is known as binding. Different types of monikers exist, each of which creates an object based on different types of input data. When the base client calls GetObject, the function looks at the supplied text string and attempts to find a moniker of the type named by the leftmost substring before the colon, in this case, "queue." This name is a ProgID in the registry, as shown in Figure 10. The queue moniker was added to Windows 2000 for the purpose of creating a recorder object to simulate the presence of a queued component. GetObject creates an object of the queue moniker class based on the supplied text string, and tells the moniker to bind. It is up to the queue moniker to make sense of the string and return a Recorder object with the correct properties and simulating the correct component. A full explanation of the moniker creation and binding process is beyond the scope of this article. A good source is the July 1997 ActiveX/COM Q&A column in MSJ. |
Figure 10 Queue Moniker Entry |
The characters between "queue:" and "/" are for the use of the queue moniker. In the previous example, there aren't any. The characters after the slash, in this case, "new:
VBShipProj.VBShip", are the initialization string for another type of moniker. This moniker's name is the new moniker, so called because it simulates the action of the new operator in many programming languages. This type of moniker creates a new COM object based on the ProgID that follows it. On the client side, it tells the queue moniker which class of object to record its calls for. The new moniker is then serialized into a stream and transmitted to the server at the beginning of the bundle of recorded COM calls for this object. That's how the server knows which class of object to create in order to play back the calls.
The new moniker is also the mechanism by which you can create a component in both queued and nonqueued situations. To create a nonqueued component, just call GetObject without the queue moniker in front. The new moniker will properly make you an instance of that class without the recorder intervening. |
|
Since you are passing information to the queue moniker telling it which class of object to record calls for, it makes sense that this should be the place where you pass information to tell the recorder the MSMQ settings you want it to use. You change the default settings by passing additional data in the string used to initialize the queue moniker. For example, to create a QC recorder session that is aimed at a specific machine, you would say: |
|
The attributes of MSMQ that are available to the QC developer are listed in Figure 11. Some of these, such as the ComputerName attribute shown previously, are clearly useful to the application programmer. The utility of others in the QC situation is less clear, primarily because many of them are used at a low level on the recipient server and not yet made available to your actual component.
Conclusion The ability to support disconnected operation is extremely useful in distributed computing. The Queued Components service of Windows 2000 replaces RPC with MSMQ as the underlying transport mechanism for COM objects. Queued Components provides a relatively easy way to obtain disconnected operation without changing your fundamental programming model very much.
|
For related information see: Message Queuing Services (MSMQ) at http://msdn.microsoft.com/library/backgrnd/html/msdn_msmqoverview.htm. Also check http://msdn.microsoft.com/developer/default.htm for daily updates on developer programs, resources and events. |
From the June 1999 issue of Microsoft Systems Journal.