Three Useful Server Interface Techniques

Ken Bergmann

June 9, 1997

Click to copy the files in the FORUM sample application for this technical article.

Introduction

The sample that accompanies this article is a simple Forum server. It's a chat room kind of service. It's not too simple, but not too complex either. And like most samples, it is just a medium I'm using to make some (hopefully) good points. The three cool things I will discuss in the article as I cover the sample are:

Oh yeah! I saw some raised eyebrows there, especially at the performance one. I'm looking forward to seeing what you think when I've said my bit.

Up front, I must disclose that the concepts I'll be covering are Component Object Model (COM) concepts, not language concepts. For simplicity, I'll be using Microsoft® Visual Basic® version 5.0 in the sample code. This article will assume you have a basic understanding of writing and using COM interfaces.

Even though I'll stick to covering the sample, don't be sidetracked. The real interesting stuff is in the techniques used to create the interfaces.

Setting Up the Packager Problem

Distributed applications. We all want them. We all flirt with building them. Performance is always a concern. Of course, the idea of centralized business servers isn't exactly new. Over time, a few good general guidelines have emerged that we all try to follow when building these distributed servers. For example:

Use methods with lists of parameters, instead of several property calls.

Don't pass collections across the wire; send arrays or variants.

Don't pass objects around to be interrogated; send the data directly.

Of course, there are others, but you get my drift. The unfortunate thing about guidelines such as these is the programming model you end up with when you pay attention to them. For example, if you are going to send data in an array instead of a collection, the user has to pay attention to offsets or index values. If your users must send multiple parameters to a method instead of setting properties on an object, the users must also be able to hold onto that data locally, in either variables or controls or some other storage. In both cases, you've created more work for the user of your interface. Now more work isn't bad, but this particular type of work can be significantly more prone to errors than its alternative.

So what alternative is there? Either you follow good guidelines and keep performance or you create a clean, user-friendly, robust programming model and suffer performance heartaches. This isn't exactly the best of spots to be in—unless you are willing to create a packager for your system.

Packagers

A packager is a conceptual entity whose entire purpose is to create and manage local versions of remote objects. A big job, but it boils down to some very simple things. Mainly, it uses high-performance techniques to extract data from a private remote interface, and then it exposes this data as a local interface for the client. The interface exposed to the client makes use of many different kinds of developer candy, such as collection objects, lots of properties, hard data types, data-only classes, simple methods, and garbage-collection techniques. The idea here is to provide the easiest interface for the client so that they have to perform the least amount of work when using the objects. It sounds like it would be a lot of work, but as you investigate the sample, you will find it's really quite simple.

The Forum Sample

l needed an idea for a service that performed work for a variety of clients, and I came up with a simple forum service. Basically, clients can get a seat in a forum on the server and send text back and forth. Text messages can be broadcast to all users of the forum, or from one user to another, like a simple chat or BBS system. I'm not writing a product, so I don't mind if it's something that you can buy off the shelf. I'm just using it to make some points.

Detailing the Pieces

Here are the basic entities in the system.

What you should be getting from the above list is that there are really only four entities in the system: Admin, Forum, Seat, and Message. If that doesn't jump out at you, it may be helpful to check out the sample code. The class definitions between private and local objects are very similar.

Why It Looks This Way

The trick to creating the local client for the system was to trim down the worker interfaces from the server and expose them as data classes on the client. So instead of duplicating a Forum on the client, we push the data for a Forum into a package to the Admin on the client and the Admin on the client creates a local copy of the Forum using the data. The Forum is lighter in weight but can still be examined easily using a robust programming model on the client.

That sounded confusing the first time I read it, so let me draw a little something on the whiteboard for you to take a look at (Figure 1).

Figure 1: The Forum service

Here you have the two different machines with three or more different processes. The AdminPrivate interface exposes the entire list of Forums it knows about as a variant array. The local Admin (which is the only piece the client knows about) translates that into a collection of Forum objects. It's basically the same for other objects as well. The working versions are on the server. The public data for each object is pulled down to the clients as they need it to be examined locally.

Does It Have to Look This Way?

No. In this particular sample, I defined a simplistic model so that it would be easier to understand. An interesting thing to note is that while in this sample there is only one public object per private object, most likely this will not be the case. A private object might have more than one public object representing it locally. For example, one type of local server might expose certain information about a Forum, and another type of local server might expose different or overlapping information about the same Forum. It depends on the need of the local packager object.

What Does It Look Like on the Client?

The client creates an Admin object and then proceeds to query the Admin for available Forums. When the client finds a Forum it likes, it gets a Seat from the Forum. As a Seat is made available to the client, it is also mirrored in the private server interfaces. The private Forum on the server begins to deliver mail to that Seat right away.

The interesting part is the programming model on the client. By using the packager objects (data-only objects that are created from data sent remotely), the system can route traffic in a high-performance way but still expose a robust programming model on the client. The servers don't interoperate through collections, they use variant arrays. But each local client works with only easy-to-use (and good-performance) collections. Likewise, instead of exposing messages as a structure or as an array and requiring the client to work with offsets, the client works only with light-weight objects in-process.

In code, the difference becomes very obvious. Instead of this:

If m_oAdmin.GetForums(vForums) Then
    For lCurForum = LBound(vForums, 1) To UBound(vForums, 1)
        'Add forum to the tree
        tvMain.Nodes.Add Key:=vForums(lCurForum, 0), _
                         Text:=vForums(lCurForum, 1)

        'Now get seats for a forum
        If m_oAdmin.GetForumSeats(vForums(lCurForum, 0), vSeats) Then
            For lCurSeat = LBound(vSeats, 1) To UBound(vSeats, 1)
                'Add seats to the tree
                tvMain.Nodes.Add Parent:=vForums(lCurForum, 0), _
                                 Relationship:=tvwChild, _
                                 Key:=vSeats(lCurSeat, 0), _
                                 Text:=vSeats(lCurSeat, 1)
            Next lCurSeat
        End if
    Next lCurForum
End If

Your clients get to write it this way:

'Miscellaneous error handling
For Each oForum In m_oAdmin.Forums
    'Add forum to the tree
    tvMain.Nodes.Add Key:=oForum.Alias, Text:=oForum.DisplayName

    For Each oSeat In oForum.Seats
        'Add seats to the tree
        tvMain.Nodes.Add Parent:=oForum.Alias, _
                         Relationship:=tvwChild, _
                         Key:=oSeat.Alias, _
                         Text:=oSeat.DisplayName
    Next oSeat
Next oForum

Notice how many local variables are needed in the first example. At a minimum, you need four just to hold the arrays and counters. You need two in the second example. Now, that's not a space savings or anything. But in terms of readability, writability, maintainability, and margin for error, I would definitely rather write the second instead of the first. At the very least, in the second example, you don't have to worry that your offsets are correct!

Is There a Catch?

Not really. Well, there are several tricky issues involving creation and destruction of objects. For example, in the sample, Seat objects needed to be globally unique. They needed an identifying number generated when they were created, and then the value of that number couldn't change for the lifetime of the object. Since there is no constructor syntax in Visual Basic, I made use of a friend procedure to set the value. Here's what the GetSeat method of a Forum looks like:

Public Function GetSeat(sAlias As String, sDisplay As String) As CSeat
Dim oSeat  As CSeat
Dim oForum As Object

'Miscellaneous error handling

'Create local seat
Set oSeat = New CSeat
With oSeat
    'Fill in default properties
    Set .Admin = m_oAdmin
    .Id = m_oAdmin.GetNextId
    .Alias = sAlias
    .DisplayName = sDisplay
    .ForumAlias = m_sAlias
End With

'Add it to the private forum
Set oForum = m_oAdmin.GetForumByAlias(m_sAlias)
oForum.SeatAdd oSeat

'Make sure my data is current
Refresh

'Now return the valid seat
Set GetSeat = oSeat
'Miscellaneous clean up
End Function

Using this kind of integrated construction technique, I was able ensure that before an object is handed to a client, it has its unique identifier already assigned and the mirror object in the private interface is already created and working.

An additional note about using constructor methods is that to ensure they are used, you must mark the object as PublicNotCreatable. That way, the only way for a client to get a hold of an object of that type is through your interface methods. Needless to say, this technique can prove itself to be very useful in other ways too.

How to Disconnect the Performance of a Server without Having to Resort to Slimy CreateThread Hacks

I hate long section headers, but this particular one gets right to the point. In a system like the Forum service, you have lots of interoperation between the many Forums and the many Seats. If the server that exposed the Forum interface was monolithic, the size of one Forum or the speed of one Seat would slow down the remaining Forums and Seats every time a call was blocked or took a long time. This little sample takes advantage of a clean technique for disconnecting this performance.

The first step is to separate the interface doing the work in its own out-of-process server. Then mark only the working class for SingleUse. Leave any support classes marked as MultiUse. Set your timers or callbacks or whatever inside the worker class and then just let it run. In this case, we use the SetTimer API to register a timer for the class. In this way, a single instance of the worker class just runs by itself using the timer to keep pace inside its own process. If another instance of a Forum has a lot of clients or a slow client, it won't affect (within reason) the first instance from receiving timer callbacks and performing work.

We make use of this technique by creating Forums and adding them to a collection. Each time we create a Forum (something that really only happens at startup), the Forum is created in its own process space. Because the number of Forums is not large and is known, we can be assured that the number of processes will be acceptable for the machine the ForumService will run on. The performance of the overall system will not be greatly affected by the speed of the clients, and I don't have to worry about creating threads to do it.

This isn't a technique you may use a lot, but in cases where you have the magic keys it can be invaluable. The magic keys are being able to control the number of Forums and being able to control when they are created.

One More Time

So, this sample starts from a very simple premise, but shows three very neat techniques along the way. Using a Forum server as an example, I created packager objects to maintain high-performance communication between machines. At the same time, the packager objects also exposed a robust, user-friendly, and functional interface to the clients.

To hook the public and private interfaces up correctly, I needed to make use of constructor methods to ensure that objects that were given to the client were initialized correctly.

Lastly, I made use an out-of-process server technique to disconnect the performance of the system and provide greater throughput on the server.

I'd love to hear your thoughts on this, or anything really. Feel free to e-mail me at kenbe@microsoft.com.