techvanguards.com
Last updated on 1/25/2016

Implementing a Multi-User DCOM Application
by Binh Ly

Download the source code for this article

Distributed COM (DCOM) enables COM applications to go out to the real world and enjoy life. DCOM simply extends the goodies of COM across your network so that an application on your computer can chit-chat with  applications running on other computers. If anything, the "coolness" of this capability is easily proven by the huge success of the Internet - if you think of it, the Internet is simply a web of machines and applications across the globe that talk to each other.

One of the nice aspects of DCOM is that it's not that hard to implement applications that work with it. In fact, your ordinary COM server can "probably" be installed and executed from a remote machine without requiring any changes in code at all. I say "probably" because developing applications that work across the network also requires a certain kind of mindset that the ordinary (or newbie) COM developer may not realize at first.

Among the most important things in the "DCOM mindset" are the following:

  • Applications on a network talk to each other through nothing but lowly wires and cables. In a perfect world, we'd ideally want two machines, A and B, to talk to each other using a single cable that connects A to B directly and privately. In a perfect world, another pair of machines, C and D, would also talk to each other using a their own separate private cable connection. In a perfect world, we'd want a hundred pairs of machines to each have their own separate private cable connections. But that's only in the perfect world! 

    In the real world, network cables are really shared by lots of machines. Sharing means that chit-chatting applications should be nice and considerate to other applications, kind of like being nice when sharing a single bathroom in your family. In particular, it is bad for applications spend a lot of time chit-chatting about nonsense, similar to how it is bad manners to spend a lot of time in the bathroom when others are waiting on you. In technical terms, it is simply evil for an application to produce a lot of unnecessary traffic on the network! And that includes your DCOM applications.
     
  • Being able to connect applications together also implies some sort of security. For instance, an email server application should only be accessible to privileged parties. In other words, you don't want some Joe Bum from somewhere on the Internet to read your mailbox and fiddle with private emails between you and mom do you? In a similar sense, DCOM provides a facility to protect applications on the network, and it is important to know how this facility works.

Of course, there are a lot more DCOM issues than the two mentioned above. For now, let's keep it simple and just concentrate on these two.

A Network Message Board

In order to learn the basic DCOM concepts, let's create a simple network message board application. A network message board is similar to an Internet newsgroup: you have a server (message board) where everyone can post messages to and download messages from. 

Figure: Message board clients and server. Arrows indicate that clients post and download messages.

Interface Design

To be as realistic as possible, let's assume that each message has the following attributes:

  • ID - unique message identifier generated by the server. We'll see later where this is used.
  • Date - date/time when message was posted
  • From User - user who posted the message
  • To User - message recipient. Usually this will be "Everyone" but let's also get fancy here and allow a message to be posted to an individual user.
  • Subject - title/subject of the message
  • Message - the message text itself

Let's now look at what the client (a user) should be able to do to the message board:

  • Post a message
  • Retrieve all messages currently on the message board

In simple terms, that translates to:

IMessageBoard = interface
  procedure Post;  // post message
  procedure Get;   // get message(s)
end;

To post a message, the client will pass the following message attributes to the server: ToUser, Subject, and Message. The ID and Date attributes will be calculated by the server. The server will also discover the FromUser identity as part of COM security authentication. Therefore, the client does not have to explicitly pass his identity when posting a message, similar to how you don't have to explicitly type in the From field when you send an email to your friends - it is already determined for you when you were authenticated at login time.

With that, let's define a more specific Post method:

IMessageBoard = interface
  procedure Post (ToUser, Subject, Message);  // post message
  procedure Get;   // get message(s)
end;

Let's now think about the Get method. What should Get return exactly? Should we design an interface for the messages and have Get return that interface? Let's say we design this interface, it'd somehow look like this:

//a single message
IMessage = interface
  property ID;
  property Date;
  property FromUser;
  property ToUser;
  property Subject;
  property Message;
end;

//collection of (all) messages
IMessages = interface
  property Count;
  property Item [Index] : IMessage;
end;

Using these 2 interfaces, we can have Get return IMessages: 

IMessageBoard = interface
  procedure Post (ToUser, Subject, Message);  // post message
  procedure Get (out Messages : IMessages);   // get message(s)
end;

And a client using Get to retrieve all server messages would do something like this:

MessageBoard.Get (Messages);
for i = 1 to Messages.Count do
begin
  AMessage = Messages.Item [i];
  DoSomethingWith (AMessage);
end;

To the normal COM developer, this approach seems reasonable and very object-oriented. What he probably doesn't realize is that in order to retrieve all messages from the server, these need to happen:

  • The MessageBoard.Get call is 1 trip across the network from the client to the server
  • The Messages.Count call is 1 trip across the network
  • The Messages.Item [] call is 1 trip across the network
  • If DoSomethingWith accesses a property of AMessage, that will also require a trip across the network

In fact with the Messages.Item [] call, the for-loop alone will require at least as many trips across the network as there are many messages on the server. If the server has a thousand messages, that equals a thousand trips (at least) between client and server.

Are you starting to get my drift here?! A thousand trips across the network to retrieve a thousand messages would generate a ton of network traffic which, as I've said before, is evil! Your network admin might come pounding on your door if he finds out that you're slowing down his network

Obviously, we need a better approach here. Ideally, what we want is for a single Get call to return as many messages as possible. In fact, we'll actually make Get return *all* messages using only a single call. How do we do that - we'll pack all the messages on the server into a single data packet and return that entire packet to the client. The packet is really nothing but an array of messages that looks something like this:

Message 1 ID 1 Date 1 FromUser 1 ToUser 1 Subject 1
Message 2 ID 2 Date 2 FromUser 2 ToUser 2 Subject 2
...          
Message n ID n Date n FromUser n ToUser n Subject n

I've intentionally excluded the Message text field from each row for a reason. Notice how a newsreader normally doesn't download an entire message (including the message body/text) when you refresh your message list. Instead, your newsreader downloads the message body only when you select/highlight the message in its list, i.e. when you actually want to read the message. There's a reason for that. A message body is normally big and may contain attachments and if the newsreader downloads it all, 1) it will take a lot more time to download all messages and 2) that would easily degrade the performance of the newsgroup server. Although we won't be sending huge message bodies to our message board server, let's just mimic this normal newsreader behavior to be more realistic.

A practical newsreader won't normally ask for a packet that returns *all* messages from the server in one shot. Instead, packets are brought down in groups of, say, 300 messages. Nevertheless, downloading messages in packets is a lot more efficient that downloading them one-by-one. For our purposes, downloading a single packet consisting of all messages should suffice.

With the packet-technique, here's how IMessageBoard would now look like:

IMessageBoard = interface
  procedure Post (ToUser, Subject, Message);  // post message
  function Get (out Messages : Array) : integer;   // get message(s)
end;

Note that I'm also making Get return the count of messages in the packet array. Our client above would now use the new Get method like this:

Count = MessageBoard.Get (Messages);
for i = 1 to Count do
begin
  AMessage = Messages [i];
  DoSomethingWith (AMessage);
end;

Note that the only trip across the network here is the Get call. After that, all operations are locally done to the Messages packet array on the client. Pretty cool huh?!

One last thing before our design is complete: since the packet returned from Get doesn't include the message text/body, we'll want to create another method to allow the client to retrieve a message's text given its ID.

IMessageBoard = interface
  procedure Post (ToUser, Subject, Message);  // post message
  function Get (out Messages) : integer;   // get message(s)
  function GetMessage (ID; out Message) : boolean;  // get message body given message ID
end;

Since Get returns the message IDs as part of the packet, we can use the ID when calling GetMessage to retrieve the message body that corresponds to a given message. Note that I'm making GetMessage return a boolean to indicate whether or not a message with the given ID really exists. This might be useful later on when we want to be able to delete messages - if a client wants to retrieve the message body of a message that has been yanked under its nose, GetMessage simply returns False. 

Whew! That's a lot of design for an interface with only 3 simple methods isn't it? Thinking about a practical design up front always pays off in the long run. Notice what we've done with the IMessageBoard.Get method: by eliminating the more object-oriented (using IMessages and IMessage) but extremely inefficient way of doing things, we are saving ourselves a lot of headache in the future. In fact, if our server was simply an inproc (DLL) server that runs locally with the client, there is absolutely nothing wrong with using IMessages and IMessage. But, our server is going to be remotely deployed where each unnecessary call across the network counts as a demerit. Now this is what I call the "DCOM mindset"!

Before we move on to the good stuff (i.e. the code), let's first lay down a few constraints for our implementation:

  1. We'll create an out-of-process (EXE) server. This allows us to easily deploy the server remotely and to easily see the important aspects of DCOM security. Also, we'll create server objects that support automation (IDispatch).
     
  2. For simplicity, we won't be concerned with multithreading. For now, assume that everything is single-threaded and that our code need not be concerned with multithreading synchronization issues.

Let's now rock and roll...

Delphi Implementation
C++ Builder Implementation


Delphi Implementation

In Delphi, we create an out-of-process (EXE) COM server by creating a normal Win32 application using the File | New Application menu item. Let's name our project "MessageBoardServer" - Delphi will produce a file named MessageBoardServer.exe as our server.

The next step is to create the MessageBoard automation object. Let's use Delphi's wizard to do this: select File | New and pick Automation Object on the ActiveX tab in the dialog, name the object "MessageBoard", and then save the new unit as MessageBoard.pas. Then we want to implement the IMessageBoard interface we've previously designed:

IMessageBoard = interface
  procedure Post (ToUser, Subject, Message);
  function Get (out Messages) : integer; 
  function GetMessage (ID; out Message) : boolean;
end;

To do that, we go into the Type Library Editor (TLE) and add the IMessageBoard methods correspondingly:

Figure: Defining IMessageBoard

Note that we use WideString for strings, WordBool for boolean, and OleVariant for the Messages parameter in Get. That's just the automation way of doing things.

Before we implement these methods, let's think about something for a bit. Note that each client will create an instance of the MessageBoard object we just created. So if we have 2 connected clients, these 2 clients will create 2 MessageBoard instances. So far, that's not a problem. However, we want all clients to have the "illusion" that there is only 1 global message board that holds all messages. In other words, each MessageBoard instance associated with each client has to post and retrieve messages from a single central place in the server. Let's encapsulate this single central place into a GlobalMessageBoard object. Thus:

procedure TMessageBoard.Post(const ToUser, Subject, Message: WideString);
begin
  GlobalMessageBoard.Post (ToUser, Subject, Message);
end;

function TMessageBoard.Get(out Messages: OleVariant): Integer;
begin
  Result := GlobalMessageBoard.Get (Messages);
end;

function TMessageBoard.GetMessage(ID: Integer;
  out Message: WideString): WordBool;
begin
  Result := GlobalMessageBoard.GetMessage (Id, Message);
end;

Next step would be to implement GlobalMessageBoard. But before we do that, let's first encapsulate and implement messages into 2 classes: 

  1. TMessage - represents a single message
  2. TMessages - represents a collection of TMessage's

Let's do the trivial TMessage first:

type
  TMessage = class
  protected
    FID : integer;
    FDate : TDateTime;
    FFromUser : string;
    FToUser : string;
    FSubject : string;
    FMessage : string;
  public
    constructor Create (Id : integer; Date : TDateTime; const FromUser, ToUser,
      Subject, Message : string);
  end;

constructor TMessage.Create(Id: integer; Date: TDateTime; const FromUser,
  ToUser, Subject, Message: string);
begin
  FId := Id;
  FDate := Date;
  FFromUser := FromUser;
  FToUser := ToUser;
  FSubject := Subject;
  FMessage := Message;
end;

There's nothing really interesting going on here. Let's look at TMessages instead.

Recall that IMessageBoard exposes the GetMessage method to allow a client to get a message's body given it's ID. This means that we need to provide some sort of lookup facility in the TMessages class that maps message IDs to each TMessage. For practical purposes, we'd like to avoid scanning the entire TMessages list and compare a given ID with each message's ID when performing the search. Imagine if a newsgroup server actually scans its entire message list whenever a client wants to open a particular message - that would be considered sloppy programming! 

One practical way to perform lookups like this is to use a Map container. I've implemented a simple reusable TMap container (based on string keys) in Delphi and we'll simply make use of it.

A Map is a container that associates a key (usually a string) to each element in the container. This association is normally implemented in such a way that if you want to search for an element given it's key, the map will perform the search in a very efficient manner. 

In Delphi, a map (based on string keys) can easily be simulated using a sorted TStringList class. However, I chose to implement it using a separate class because I wanted to preserve the ordering of the items (messages) as they are added to the list - a sorted TStringList would mangle the order based on the keys. Although this ordering rule is not a requirement of a map, it is rather useful for our application. After all, you'd want to retrieve and view messages from a newsgroup in order of the date they are posted - not by some useless message ID.

FYI, there are some issues that my simple TMap class doesn't robustly address. For instance, I have not taken into consideration how to handle duplicate keys. You can always feel free to explore and improve the class depending on your needs.

The most important methods of TMap that you'll be using are:

type
  TMap = class
    function Add (const Name : string; Item : TObject) : integer;
    property Count : integer;
    property Item [Index : variant] : TObject;
  end;

Add allows you to add a key-item pair to the map. Count returns the count of items in the map. Item [] returns an item from the map. Note that the Index parameter to Item [] is a variant so that you can index 2 ways. If you use an integer value for Index, Item [] will return the Index'th element in the map. If you use a string value for Index, Item [] will return the element whose key is Index. In other words, if you have a map like this:

Key Item
"Key1" Element 1
"Key2" Element 2

Assuming 0-based indexing: 

Map.Item [0] is Element 1
Map.Item [1] is Element 2
Map.Item ["Key2"] is Element 2
Map.Item ["Key1"] is Element 1.

With TMap in place, let's now implement TMessages:

type
  TMessages = class
  protected
    FMessages : TMap;
  public
    function GetMessage (Id : integer) : TMessage;
    property Count : integer;
    property Item [Index : integer] : TMessage;
  end;

//returns message count
function TMessages.GetCount: integer;
begin
  Result := FMessages.Count;
end;

//returns message at Index'th position
function TMessages.GetItem(Index: integer): TMessage;
begin
  Result := TMessage (FMessages.Item [Index]);
end;

//return message whose ID is Id
function TMessages.GetMessage(Id: integer): TMessage;
begin
  //IntToStr conversion is because TMap is based on string keys
  Result := TMessage (FMessages.Item [IntToStr (Id)]);
end;

TMessages would also enable us to add a message to its list:

procedure TMessages.Add(FDate: TDateTime; const FromUser, ToUser, Subject,
  Message: string);
var
  Msg : TMessage;
  Id : integer;
begin
  inc (FLastId);  //internal ID that gets incremented for every new message
  Id := FLastId;
  Msg := TMessage.Create (Id, FDate, FromUser, ToUser, Subject, Message);
  //add <ID, Msg> mapping to list
  FMessages.Add (IntToStr (Id), Msg);
end;

With TMessages in place, it's finally time to go back and implement GlobalMessageBoard:

type
  TGlobalMessageBoard = class
  protected
    FMessages : TMessages;
  public
    function Get(out Messages: OleVariant): Integer;
    function GetMessage(ID: Integer; out Message: WideString): WordBool;
    procedure Post(const ToUser, Subject, Message: WideString);
  end;

Let's first look at Get. Recall that Get is supposed to return a packet array of messages.

function TGlobalMessageBoard.Get(out Messages: OleVariant): Integer;
var
  i : integer;
begin
  //create array only if there are messages
  if (FMessages.Count > 0) then
  begin
    //create array whose row count is the message count
    Messages := VarArrayCreate ([0, FMessages.Count - 1], varVariant);
    //fill each array row with each message
    for i := 0 to FMessages.Count - 1 do
      Messages [i] := FMessages.Item [i].AsPacket;
  end;

  //return message count to client
  Result := FMessages.Count;
end;

Notice how we use the VarArrayCreate function to create our packet as an array. VarArrayCreate simply wraps something called a COM variant array. A variant array is nothing but a COM-compatible array data type that can be passed between the client and server. The [0, FMessages.Count - 1] part means that Messages will contain FMessages.Count elements indexed from Messages [0] through Messages [FMessages.Count - 1]. After creating the array, we then loop through the messages, and for each message, we pack its fields into a smaller packet (using the AsPacket property) and then we put that packet into the Messages array. TMessage.AsPacket is implemented as follows:

function TMessage.GetAsPacket: variant;
begin
  //Packet will look like this: | ID | Date | FromUser | ToUser | Subject |
  //exclude Message field
  Result := VarArrayCreate ([0, 4], varVariant);
  Result [0] := FId;
  Result [1] := FDate;
  Result [2] := FFromUser;
  Result [3] := FToUser;
  Result [4] := FSubject;
end;

By using VarArrayCreate again here, a single message packet is also a variant array. We simply fill this array with fields from the message. So in the end, TGlobalMessageBoard.Get will return an array of messages wherein each row in the array is a smaller array consisting of each message's fields. Got it?!

VarArrayCreate can also be used to create n-dimensional arrays. If you're adventurous, you can try implementing a 2-dimensional array for TGlobalMessageBoard.Get. That would probably be more efficient than my implementation of a 1-dimensional array of elements that are each 1-dimensional arrays.

Note, though, that if you change the format or dimensional indexing of your array, you'll also have to modify the client code to interpret the array using the new format/dimension.

So much for these arrays, let's now look at GetMessage:

function TGlobalMessageBoard.GetMessage(ID: Integer;
  out Message: WideString): WordBool;
var
  Msg : TMessage;
begin
  Result := False;
  //get message by ID
  Msg := FMessages.GetMessage (Id);
  if (Msg <> NIL) then
  begin
    //found message
    Message := Msg.FMessage;
    Result := True;
  end;
end;

This is easy. We look for the message given its ID. If found, we return the message text/body into Message and return True to indicate success. Otherwise we return False to indicate that no message with the given ID exists.

And last but not least, Post:

procedure TGlobalMessageBoard.Post(const ToUser, Subject,
  Message: WideString);
var
  PostDate : TDateTime;
  PostUser : string;
begin
  PostDate := Now;
  PostUser := CallerId;
  //add new message to list
  FMessages.Add (PostDate, PostUser, ToUser, Subject, Message);
end;

function CallerId : string;
var
  PrincipalName : PWideChar;
begin
  //use COM security to determine caller Principal
  OleCheck (CoQueryClientBlanket (NIL, NIL, NIL, 0, 0, @PrincipalName, 0));
  Result := PrincipalName;
end;

What we're doing here is we're simply trying to post a new message. Remember how we said before that the server takes care of a message's post Date and FromUser attributes? The Date part is easy - we simply call  Delphi's built-in Now function. The FromUser part requires a few more keystrokes, but is nonetheless also easy. COM provides the CoQueryClientBlanket API that can be used by a server to determine client security settings that are used when making a method call into the server. Of particular interest here is the 6th parameter, which under NT 4 security returns the NT account name of the user/principal that made the Post call. 

Note that obtaining the client's name this way only works if the client specifies authentication (Authentication Level is at least Connect) and agrees to at least be identified for impersonation (Impersonation Level is at least Identify) at the time when the call is made. 

Authentication is a rather simple, but essential, topic in COM security. Authentication is the process of verifying the client's identity on the network, i.e. does he have an account on one of your servers? Impersonation is the process by which a server will assume (impersonate) the identity of the client in order to perform some operation that might require the client's credentials. For a more detailed discussion, try Keith Brown's excellent Security Briefs column in the November 1998 issue of the Microsoft Systems Journal or Don Box's Essential COM book.

That's pretty much it for our TMessageBoard class. For completeness, let's see what GlobalMessageBoard really is:

var
  GlobalMessageBoardVar : TGlobalMessageBoard = NIL;

function GlobalMessageBoard : TGlobalMessageBoard;
begin
  if (GlobalMessageBoardVar = NIL) then
    GlobalMessageBoardVar := TGlobalMessageBoard.Create;
  Result := GlobalMessageBoardVar;
end;

What we're seeing is a global TGlobalMessageBoard instance stored in GlobalMessageBoardVar. The first time we ask for it, we create it. After that, we just return that instance, effectively resulting in a single global message board for our entire server!

Implementing the Client

Before we dissect the client code, let's first see what the client application looks like:

Figure: Client main screen

As you can see, the client's main screen displays the list of messages in the upper listview (named lvwMessages). The message fields are shown in the columns: Subject, Date, From, and To - the first column actually is the ID field, you just don't see it because I gave the ID column a Width=0. The lower part is a memo field (named memoMessage) that displays the message body/text for the currently selected message in the listview. Up on top, we have the 2 buttons: Refresh and Post. Refresh retrieves all messages from the server and refreshes the message list. Post pops up another form that allows the client to post a new message to the server. The post screen looks like this:

Figure: Client's create new message screen

Let's first look at how the listview is populated with messages:

procedure TfrmMain.RefreshMessages;
var
  Messages : olevariant;
  Message : variant;
  i, Count : integer;
  ListItem : TListItem;
begin
  //BeginUpdate/EndUpdate avoids UI updates while loading
  lvwMessages.Items.BeginUpdate;
  //reset list
  lvwMessages.Items.Clear;
  memoMessage.Clear;
  //retrieve
  //assume FMessageBoard is an instance of the server MessageBoard object
  Count := FMessageBoard.Get (Messages);
  //load list
  for i := 0 to Count - 1 do
  begin
    //load 1 message
    Message := Messages [i];
    //message format is [0] ID | [1] Date | [2] From | [3] To | [4] Subject
    ListItem := lvwMessages.Items.Add;
    // ID | Subject | Date | From | To
    ListItem.Caption := IntToStr (Message [0]); // ID
    ListItem.SubItems.Add (Message [4]); // Subject
    ListItem.SubItems.Add (DateTimeToStr (Message [1])); // Date
    ListItem.SubItems.Add (Message [2]); // From
    ListItem.SubItems.Add (Message [3]); // To
  end;
  lvwMessages.Items.EndUpdate;
end;

There's nothing complicated going on here. We first clear the messages listview and the message body memo. Then we retrieve all messages from the server by calling FMessageBoard.Get. Then we look at how many messages we got, we iterate through them, and for each message, we "unpack" the packet back into the message fields and load each field into the listview columns.

And here's the part that loads the message body into the memo:

procedure TfrmMain.lvwMessagesSelectItem(Sender: TObject; Item: TListItem;
  Selected: Boolean);
begin
  //load message text for current message
  if (Selected) then
    LoadMessageText (StrToInt (Item.Caption));  //Item.Caption is the hidden ID column
end;

procedure TfrmMain.LoadMessageText(Id: integer);
var
  Message : widestring;
begin
  if (FMessageBoard.GetMessage (Id, Message)) then
    memoMessage.Text := Message
  else
    memoMessage.Clear;
end;

Again, nothing complicated going on here. LoadMessageText simply calls FMessageBoard.GetMessage to retrieve the body of the currently selected message. If it gets the body, it loads the text into the memo, otherwise it clears the memo.

Here's the trivial code that posts a new message from the post form:

procedure TfrmMessage.btnPostClick(Sender: TObject);
begin
  //post
  //assume FMessageBoard is an instance of the server MessageBoard object
  FMessageBoard.Post (edtToUser.Text, edtSubject.Text, memoMessage.Text);
  //bye bye
  Close;
end;

There's one important thing I'd like to point out in the client application. The FMessageBoard variable that we're using is actually declared like this:

uses MessageBoardServer_TLB;
...
FMessageBoard : IMessageBoard;

This means that we are using early (vtable) binding when making calls to the server. What's important here is that you should never use late binding (i.e. declaring FMessageBoard as a variant) when calling into a remote server unless you have a very good reason to. Recall from our discussion on IDispatch that each late bound method call actually involves 2 IDispatch calls: GetIDsOfNames and Invoke. That translates to 2 network trips per method call, which again is evil. By using early binding, we are confident that we are as efficient as possible when making calls across the network. 

In addition to the 2 trips per late bound method call, IDispatch.Invoke might also require some extra overhead when parsing the parameters that are passed between client and server. This extra overhead adds extra time to the overall processing of a single late bound method call. With this overhead, a late-bound remote method call can be as much as 5 to 10 times slower than making the equivalent early bound call.

Proceed to deployment section >> 


C++ Builder Implementation

In CBuilder, we create an out-of-process (EXE) COM server by creating a normal Win32 application using the File | New Application menu item. Let's name our project "MessageBoardServer" - CBuilder will produce a file named MessageBoardServer.exe as our server.

The next step is to create the MessageBoard automation object. Let's use CBuilder's wizard to do this: select File | New and pick Automation Object on the ActiveX tab in the dialog, name the object "MessageBoard", and then save the new module as MessageBoard.cpp. Then we want to implement the IMessageBoard interface we've previously designed:

IMessageBoard = interface
  procedure Post (ToUser, Subject, Message);
  function Get (out Messages) : integer; 
  function GetMessage (ID; out Message) : boolean;
end;

To do that, we go into the Type Library Editor (TLE) and add the IMessageBoard methods correspondingly:

Figure: Defining IMessageBoard

Note that we use BSTR for strings, VARIANT_BOOL for boolean, and VARIANT for the Messages parameter in Get. That's just the automation way of doing things.

Before we implement these methods, let's think about something for a bit. Note that each client will create an instance of the MessageBoard object we just created. So if we have 2 connected clients, these 2 clients will create 2 MessageBoard instances. So far, that's not a problem. However, we want all clients to have the "illusion" that there is only 1 global message board that holds all messages. In other words, each MessageBoard instance associated with each client has to post and retrieve messages from a single central place in the server. Let's encapsulate this single central place into a GlobalMessageBoard object. Thus:

STDMETHODIMP TMessageBoardImpl::Post(BSTR ToUser, BSTR Subject,
  BSTR Message)
{
  GlobalMessageBoard ().Post (ToUser, Subject, Message);
  return S_OK;
}

STDMETHODIMP TMessageBoardImpl::Get(TVariant* Messages, long* Value)
{
  *Value = GlobalMessageBoard ().Get (Messages);
  return S_OK;
}

STDMETHODIMP TMessageBoardImpl::GetMessage(long ID, BSTR* Message,
  TOLEBOOL* Value)
{
  *Value = GlobalMessageBoard ().GetMessage (ID, Message);
  return S_OK;
}

Next step would be to implement GlobalMessageBoard. But before we do that, let's first encapsulate and implement messages into 2 classes: 

  1. TMessage - represents a single message
  2. TMessages - represents a collection of TMessage's

Let's do the trivial TMessage first:

class TMessage
{
public:
  //initializer ctor
  TMessage (long ID, TDateTime Date, String& FromUser, String& ToUser,
    String& Subject, String &Message) :
    mID (ID), mDate (Date), mFromUser (FromUser), mToUser (ToUser),
    mSubject (Subject), mMessage (Message)
  {}

  long mID;
  TDateTime mDate;
  String mFromUser;
  String mToUser;
  String mSubject;
  String mMessage;
};

There's nothing really interesting going on here. Let's look at TMessages instead.

Recall that IMessageBoard exposes the GetMessage method to allow a client to get a message's body given it's ID. This means that we need to provide some sort of lookup facility in the TMessages class that maps message IDs to each TMessage. For practical purposes, we'd like to avoid scanning the entire TMessages list and compare a given ID with each message's ID when performing the search. Imagine if a newsgroup server actually scans its entire message list whenever a client wants to open a particular message - that would be considered sloppy programming! 

One practical way to perform lookups like this is to use the STL Map container. 

A Map is a container that associates a key (usually a string) to each element in the container. This association is normally implemented in such a way that if you want to search for an element given it's key, the map will perform the search in a very efficient manner. 

The Standard Template Library (STL) provides a template-based map class that implements an associative mapping of almost anything that you can think of. If you're not familiar with STL, try reading STL Tutorial and Reference Guide by David Musser and Atul Saini.

Let's look at how we can implement TMessages with a map class:

#include <map.h>

class TMessages
{
public:
  TMessage* GetMessage (long ID);
  TMessage* GetMessageAt (long Index);
  long GetCount ();
protected:
  map <long, TMessage> mMessages;
  vector <long> mMessagesOrder;
};

//return message whose ID is given
TMessage* TMessages::GetMessage (long ID)
{
  //do a find here to avoid auto-insertion into a map
  map <long, TMessage>::iterator msg = mMessages.find (ID);
  if (msg == mMessages.end ())
    return NULL;
  else
    return &(*msg).second;
}

//returns message at Index'th position
TMessage* TMessages::GetMessageAt (long Index)
{
  long id = mMessagesOrder [Index];
  return GetMessage (id);
}

//returns message count
long TMessages::GetCount ()
{
  return mMessages.size ();
}

Note that we're implementing a map that associates an ID (long) to a message (TMessage). This way, given a message's ID, we can ask the map to find a message in the most efficient manner. We're also returning TMessage* from GetMessage because we want to be able to return a NULL value if the given ID doesn't exist.

Also notice how we have 2 ways to retrieve a message: GetMessage given an ID, and GetMessageAt given an index. I've added GetMessageAt to retrieve a message based on index (i.e. the position at which it was inserted/posted) because when we later return all the messages in our list to the client, we want the list to be in sequence of the order in which the messages were posted. Unlike the STL vector container, a map may not necessarily preserve the order of insertion of its elements because it is optimized based on the keys. Because of this, we're going to create a vector container that will hold a sequential list of the message IDs in the order of when each message is posted. 

Using our vector, TMessages would enable us to add a message this way:

#include <vector.h>

class TMessages
{
  ...
protected:
  map <long, TMessage> mMessages;
  vector <long> mMessagesOrder;
};

void TMessages::Add (TDateTime Date, String& FromUser, String& ToUser, String& Subject,
  String& Message)
{
  //internal ID that gets incremented for every new message
  mLastID++;
  TMessage msg (mLastID, Date, FromUser, ToUser, Subject, Message);
  //add <ID, Msg> mapping to list
  mMessages [mLastID] = msg;
  //remember its position
  mMessagesOrder.push_back (mLastID);
}

With TMessages in place, it's finally time to go back and implement GlobalMessageBoard:

class TGlobalMessageBoard
{
public:
  void Post (BSTR ToUser, BSTR Subject, BSTR Message);
  long Get (TVariant* Messages);
  bool GetMessage (long ID, BSTR* Message);
protected:
  TMessages mMessages;
};

Let's first look at Get. Recall that Get is supposed to return a packet array of messages.

long TGlobalMessageBoard::Get (TVariant* Messages)
{
  //create array only if there are messages
  if (mMessages.GetCount () > 0)
  {
    //create and populate messages packet array
    long MessageCount = mMessages.GetCount ();
    //create array whose row count is the message count
    Variant MessagesArray = VarArrayCreate (OPENARRAY (int, (0, MessageCount - 1)),
      varVariant);
    //fill each array row with each message
    for (int i = 0; i < MessageCount; i++)
      MessagesArray.PutElement (mMessages.GetMessageAt (i)->AsPacket (), i);
    //return array client
    *Messages = MessagesArray;
  }
  //return message count to client
  return mMessages.GetCount ();
}

Notice how we use the VarArrayCreate function to create our packet as an array. VarArrayCreate simply wraps something called a COM variant array. A variant array is nothing but a COM-compatible array data type that can be passed between the client and server. The OPENARRAY (int, (0, FMessages.Count - 1)) part means that Messages will contain mMessages.Count elements indexed from Messages [0] through Messages [mMessages.Count - 1]. After creating the array, we then loop through the messages, and for each message, we pack its fields into a smaller packet (using the AsPacket method) and then we put that packet into the Messages array. TMessage.AsPacket is implemented as follows:

Variant TMessage::AsPacket ()
{
  //Packet will look like this: | ID | Date | FromUser | ToUser | Subject |
  //exclude Message field
  Variant Result = VarArrayCreate (OPENARRAY (int, (0, 4)), varVariant);
  Result.PutElement (mID, 0);
  Result.PutElement (mDate, 1);
  Result.PutElement (mFromUser, 2);
  Result.PutElement (mToUser, 3);
  Result.PutElement (mSubject, 4);
  return Result;
}

By using VarArrayCreate again here, a single message packet is also a variant array. We simply fill this array with fields from the message. So in the end, TGlobalMessageBoard.Get will return an array of messages wherein each row in the array is a smaller array consisting of each message's fields. Got it?!

VarArrayCreate can also be used to create n-dimensional arrays. If you're adventurous, you can try implementing a 2-dimensional array for TGlobalMessageBoard.Get. That would probably be more efficient than my implementation of a 1-dimensional array of elements that are each 1-dimensional arrays.

Note, though, that if you change the format or dimensional indexing of your array, you'll also have to modify the client code to interpret the array using the new format/dimension.

So much for these arrays, let's now look at GetMessage:

bool TGlobalMessageBoard::GetMessage (long ID, BSTR* Message)
{
  TMessage* msg = mMessages.GetMessage (ID);
  //return false if can't find message
  if (!msg) return false;
  //found message
  *Message = WideString (msg->mMessage).Detach ();
  //success is ours!
  return true;
}

This is easy. We look for the message given its ID. If found, we return the message text/body into Message and return True to indicate success. Otherwise we return False to indicate that no message with the given ID exists.

And last but not least, Post:

void TGlobalMessageBoard::Post (BSTR ToUser, BSTR Subject, BSTR Message)
{
  //determine Date and FromUser
  TDateTime PostDate = Now ();
  String PostUser = CallerID ();
  //add to messages list
  mMessages.Add (PostDate, PostUser, String (ToUser), String (Subject), String (Message));
}

String CallerID ()
{
  OLECHAR* PrincipalName = 0;
  //use COM security to determine caller Principal
  HRESULT hr = CoQueryClientBlanket (0, 0, 0, 0, 0, (void**)&PrincipalName, 0);
  if SUCCEEDED (hr)
    return String (PrincipalName);
  else
    return "Unknown principal";
}

What we're doing here is we're simply trying to post a new message. Remember how we said before that the server takes care of a message's post Date and FromUser attributes? The Date part is easy - we simply call  CBuilder's built-in Now function. The FromUser part requires a few more keystrokes, but is nonetheless also easy. COM provides the CoQueryClientBlanket API that can be used by a server to determine client security settings that are used when making a method call into the server. Of particular interest here is the 6th parameter, which under NT 4 security returns the NT account name of the user/principal that made the Post call. 

Note that obtaining the client's name this way only works if the client specifies authentication (Authentication Level is at least Connect) and agrees to at least be identified for impersonation (Impersonation Level is at least Identify) at the time when the call is made. 

Authentication is a rather simple, but essential, topic in COM security. Authentication is the process of verifying the client's identity on the network, i.e. does he have an account on one of your servers? Impersonation is the process by which a server will assume (impersonate) the identity of the client in order to perform some operation that might require the client's credentials. For a more detailed discussion, try Keith Brown's excellent Security Briefs column in the November 1998 issue of the Microsoft Systems Journal or Don Box's Essential COM book.

That's pretty much it for our TMessageBoard class. For completeness, let's see what GlobalMessageBoard really is:

//returns single-instance TGlobalMessageBoard
TGlobalMessageBoard& GlobalMessageBoard ()
{
  static TGlobalMessageBoard GlobalMessageBoardVar;
  return GlobalMessageBoardVar;
}

What we're seeing is a global static TGlobalMessageBoard instance stored in GlobalMessageBoardVar. This effectively ensures that there is only one single global message board for our entire server!

Implementing the Client

Before we dissect the client code, let's first see what the client application looks like:

Figure: Client main screen

As you can see, the client's main screen displays the list of messages in the upper listview (named lvwMessages). The message fields are shown in the columns: Subject, Date, From, and To - the first column actually is the ID field, you just don't see it because I gave the ID column a Width=0. The lower part is a memo field (named memoMessage) that displays the message body/text for the currently selected message in the listview. Up on top, we have the 2 buttons: Refresh and Post. Refresh retrieves all messages from the server and refreshes the message list. Post pops up another form that allows the client to post a new message to the server. The post screen looks like this:

Figure: Client's create new message screen

Let's first look at how the listview is populated with messages:

void TfrmMain::RefreshMessages ()
{
  //BeginUpdate/EndUpdate avoids UI updates while loading
  lvwMessages->Items->BeginUpdate ();
  //reset list
  lvwMessages->Items->Clear ();
  memoMessage->Clear ();
  //retrieve
  TVariant vMessages;
  long Count = mMessageBoard->Get (&vMessages);
  Variant Messages = vMessages;
  //load list
  for (int i = 0; i < Count; i++)
  {
    Variant Message = Messages.GetElement (i);
    //load 1 message
    //message format is [0] ID | [1] Date | [2] From | [3] To | [4] Subject
    TListItem* ListItem = lvwMessages->Items->Add ();
    // ID | Subject | Date | From | To
    ListItem->Caption = Message.GetElement (0); // ID
    ListItem->SubItems->Add (Message.GetElement (4)); // Subject
    ListItem->SubItems->Add (DateTimeToStr (Message.GetElement (1))); // Date
    ListItem->SubItems->Add (Message.GetElement (2)); // From
    ListItem->SubItems->Add (Message.GetElement (3)); // To
  }

  lvwMessages->Items->EndUpdate ();
}

There's nothing complicated going on here. We first clear the messages listview and the message body memo. Then we retrieve all messages from the server by calling mMessageBoard->Get. Then we look at how many messages we got, we iterate through them, and for each message, we "unpack" the packet back into the message fields and load each field into the listview columns.

And here's the part that loads the message body into the memo:

void __fastcall TfrmMain::lvwMessagesSelectItem(TObject *Sender,
TListItem *Item, bool Selected)
{
  //load message text for current message
  if (Selected)
    LoadMessageText (StrToInt (Item->Caption));
}

void TfrmMain::LoadMessageText (long ID)
{
  WideString Message;
  if (mMessageBoard->GetMessage (ID, &Message))
    memoMessage->Text = Message;
  else
    memoMessage->Clear ();
}

Again, nothing complicated going on here. LoadMessageText simply calls mMessageBoard->GetMessage to retrieve the body of the currently selected message. If it gets the body, it loads the text into the memo, otherwise it clears the memo.

Here's the trivial code that posts a new message from the post form:

void __fastcall TfrmMessage::btnPostClick(TObject *Sender)
{
  //post
  mMessageBoard->Post (WideString (edtToUser->Text),
    WideString (edtSubject->Text), WideString (memoMessage->Text));
  //bye bye
  Close ();
}


Deploying the MessageBoard Server

For our purposes, let's assume that we have a simple network of 3 machines:

  • an NT 4 server named "Server". This server is a primary domain controller (PDC). It contains 3 accounts: "Server\Administrator", "Server\Jack" and "Server\Jill".
  • an NT 4 workstation named "Jack" who logs in to the domain as "Server\Jack"
  • a 95 workstation named "Jill" who logs in to the domain as "Server\Jill"

Also assume that all machines are running TCP/IP for DCOM connectivity. The NT 4 machines have SP3 (or above) installed and the 95 machine has DCOM95 1.2 (or above) installed.

DCOM95 has to be downloaded and manually installed on your 95 machine. I believe a Win98 installation already has DCOM support in it but there may be periodic downloadable updates on the Microsoft site. It is critical that you always have the latest (COM) service pack installed on your system. Un-updated systems sometimes exhibit inconsistent and weird behavior due to earlier bugs/limitations in the COM runtime.

Once you have DCOM properly set up, you'll find a utility named DCOMCNFG.exe (located in WINNT\System32 under NT and WINDOWS\System under 95). This is used to easily configure DCOM security settings for both client and server machines. 

The first thing that we want to do is to register MessageBoardServer on Server. To do this, we copy MessageBoardServer.exe onto a local folder in Server and run it as follows:

MessageBoardServer /regserver

Then we decide if we want to configure authentication from the server's point of view, i.e. do we want the server to identify the client? Recall that we need to identify the client in order to determine the FromUser field whenever a message is posted. For our purposes, we will *only* need to be able to identify the client and nothing more. Therefore our authentication level can simply be Connect, i.e. authenticate only the first time the client connects to the server. 

An authentication level specifies the minimum level of authentication that a client call must come in as. For instance, if the server specifies an authentication level of Connect but the client made an unauthenticated (authentication level=None) call into it, COM will automatically reject the call with an Access Denied error because the client cannot be identified. For a comprehensive discussion on the various levels of authentication, consult Don Box's Essential COM.

Authentication can involve contacting remote domain controllers in order to determine a client's identity. This can take a lot of time on a fairly complex network - which explains why you might experience a long delay when your COM server is being launched remotely. Because of this, you should consider authenticating only if you need to or, better yet, authenticate only at the level that your business requires!

COM provides the CoInitializeSecurity API that you can use to programmatically specify the authentication level, among others. If you don't call CoInitializeSecurity, COM will silently call it for you (once) the moment an interface pointer needs to be produced or acquired in your application. When COM does this, it will use settings in the registry to determine the authentication (and impersonation) level, among others. 

For our purposes, we won't be calling CoInitializeSecurity in the server. We'll instead use DCOMCNFG to configure the registry settings that COM uses when implicitly calling CoInitializeSecurity. However, for non-trivial applications that require a finer level of security control, you should use CoInitializeSecurity together with the other security facilities that COM provides. 

With the upcoming COM+ release in Windows 2000, security will be made a lot easier than this. However, knowledge of the basic concepts can help a lot in understanding any higher level insulation methodologies.

To configure authentication, let's run DCOMCNFG on Server. When you run DCOMCNFG, you'll see something like this: [Images look blurry because I scaled them to conserve space. DCOMCNFG doesn't understand the word "resize"!]

Figure: DCOMCNFG main screen

Each entry in the Applications list corresponds to a COM server on your machine. This means that even if your server has 10 objects, there is only one entry in the DCOMCNFG main screen that you can use to configure security settings for your entire server - not for each object in your server. If you're curious, the entries in this list are stored under HKCR\AppID in the registry. 

From the Applications list, we select MessageBoardServer.MessageBoard and then view its properties dialog (click on Properties button). In the General tab, we go to the Authentication Level dropdown list and select Connect. We then confirm all changes by clicking Ok. 

Figure: Configuring authentication level

That should do it for authentication.

On my NT 4 installations with SP4 (or above), I can only seem to get authentication level to work correctly from the Default Properties tab on the main DCOMCNFG screen - not from the application properties dialog as described above. This sounds to me like a bug or a change in behavior from SP3. However, I cannot verify this because I don't have an SP3 installation anymore - if any of you can confirm this, let me know.

Authentication only specifies if the client should be identified or not. After we identify (authenticate) the client, we then want to decide whether or not he can access our MessageBoard server. This is useful because your domain might have many more users besides Jack and Jill, but we only want Jack and Jill to access MessageBoard. This is easily done by granting access individually to Jack and Jill. However, for maintenance purposes, we'd normally want to create a user group, include Jack and Jill as members of the group, and then grant access to this user group. In fact, your administrator would prefer that you do it this way because it can make things a lot easier for him and for you.

Using the NT User Manager application, we'll create a user group named "MessageBoard Users". Then we add Jack and Jill as members (so far) to this group. If you don't know how to do this, you can ask your administrator to do this for you. Then we go back to DCOMCNFG, open up the MessageBoardServer.MessageBoard properties dialog, go to the Security tab, and select Use custom access permissions

Figure: Configuring custom access permissions

Then we open the access user list dialog (click on the Edit button) and we add our MessageBoard Users group into the list:

Figure: Configuring access for MessageBoard Users

It is very important that the System account is included in the access permissions list for your server. This is because the COM runtime runs under this account when doing low-level interaction with your server. If you forget to do this, you'll most likely get the dreaded "Not enough storage to complete this operation" error.

We then confirm all changes by clicking Ok all the way back to the DCOMCNFG main window. That should do it for access permissions.

Again, on my NT 4 installations with SP4 (or above), I can only seem to get access permissions to work correctly from the Default Security tab on the main DCOMCNFG screen - not from the application properties dialog as described above. This sounds to me like a bug or a change in behavior from SP3. However, I cannot verify this because I don't have an SP3 installation anymore - if any of you can confirm this, let me know.

Aside from authentication and access control, we also need to decide on who can launch MessageBoardServer, if it is not already running. Again, this is another important decision. At first, you might think that everyone who is allowed access should also be given launch permissions. That's not necessarily true. An administrator might want to run a server only during a certain business period of the day. During that period, clients who have access can connect to the application while it is running. However, when the administrator later shuts down the application, he will not want any client to be able to launch the application "outside of the business period".

For our purposes, let's allow any member of MessageBoard Users to be able to launch the server. To do this we go back to DCOMCNFG, open up the MessageBoardServer.MessageBoard properties dialog, go to the Security tab, and select Use custom launch permissions

Figure: Configuring custom launch permissions

Then we open the launch user list dialog (click on the Edit button) and we add our MessageBoard Users group into the list:

Figure: Configuring launch access for MessageBoard Users

We then confirm all changes by clicking Ok all the way back to the DCOMCNFG main window. That should do it for launch permissions. 

There is one final thing that we need configure: when MessageBoardServer runs, whose identity should it assume as? Again, this is an important decision because the identity under which it runs as dictates what kinds of things the server can do. For instance, if the our server assumes the identity of a user that has no rights to open a particular document, then any attempt by MessageBoardServer to open that document will fail miserably. 

Identity can be any of 3 choices:

  1. The user that is currently logged in to the server machine (Interactive User)
  2. The remote (or local) user that requested to launch the server (Launching User)
  3. A specific domain user (This User)

Interactive User might not be useful because we can't predict nor assume who is currently logged in to the server machine at any given time. In fact if nobody is logged in, the server won't run at all. Launching User will force COM to launch a separate instance of the server application per connected user - obviously, a single application can't run under the guise of multiple users all at the same time! Furthermore, COM restricts all remote/network access (from the server machine) for the Launching User (bummer!). This User is the preferred identity for a server to run as. Using this option, you can create a dedicated user account specifically for purposes of your server application. If you ever need your server to access a secured resource, you simply grant this dedicated user the proper rights. This way, your administrator will love you for doing the right thing!

Note that when you specify This User, make sure you type in the correct user name and password of the account. If you don't do this, your client will appear to be hung for a while before getting a "Server execution failed" error. Also for Delphi 3/4 and C++ Builder 4 servers, there's some code in the VCL that tries to register (write entries to the registry) your server when it loads, even though the "/regserver" parameter isn't specified. Unfortunately, this forces you to add the This User account into the local Administrators group to enable registry access on your server machine. Again if you don't do this, you'll also get the dreaded "Server execution failed" error. You have been warned!

Interactive User has interesting powers in that it's the only option where you can get to see any UI from your server. In other words, if your server has a main form, or does some sort of UI notification such as message boxes, you'll be able see it on your server's desktop (assuming of course that somebody's logged in). Because of this, the Interactive User is very useful for debugging purposes where you might want to occasionally pop-up a fancy message box here and there. More importantly, if you select Launching User or This User, your server should never pop up any message box because nobody will ever see it and thus nobody will be able to click Ok to dismiss the message box, resulting in your server appearing to be hung and unresponsive to the client!

For our purposes, we'll select Interactive User so that we can easily monitor the server's activations. To do this, we go back to DCOMCNFG, open up the MessageBoardServer.MessageBoard properties dialog, go to the Identity tab, and select The interactive user:

Figure: Configuring server identity

We then confirm all changes by clicking Ok all the way back to the DCOMCNFG main window.

To summarize, everytime you need to configure a DCOM server, you'll need to do the following:

  1. Determine and configure the authentication level
  2. Determine and configure the access control permissions
  3. Determine and configure the launch control permissions
  4. Determine and configure the server's identity

Delpoying the MessageBoard Client

If the client logs in using an account from an NT domain controller, it's really not that hard to configure it for DCOM access. For instance, for both Jack and Jill, we just need to do a remote activation call to create the MessageBoard object:

In Delphi

uses MessageBoardServer_TLB;
...
//create MessageBoard object
FMessageBoard := CoMessageBoard.CreateRemote ('Server');

In C++ Builder

#include "MessageBoardServer_TLB.h"
...
//create MessageBoard object
mMessageBoard = CoMessageBoard::CreateRemote (WideString ("Server"));

This is simply saying that we want to create a MessageBoard object that resides on a remote machine named "Server". In addition to calling CreateRemote, we also need to deploy and register the server's type library file (MessageBoardServer.tlb) on the client machine. This is because COM will need the type library file to be able to handle making calls between the client and server. 

To register a type library file, use the tregsvr.exe utility that comes with Delphi or C++ Builder (look in the bin\ folder). Although this might seem a hassle to the client, you can simply incorporate this registering process into your client installation. Note that unlike DLL servers, you cannot use the regsvr32.exe application to register a type library file.

Also note that registering the type library is required only if you use early binding. Late binding (i.e. variants) doesn't require the presence of the type library file because it relies on the "known and registered" COM IDispatch interface. However, this convenience comes with an "unacceptable" performance penalty that we've learned above. 

If you forget to register the type library on the client machine when early binding, you'll most likely get the dreaded "Interface not supported" error.

Before we forget, remember that MessageBoardServer needs to identify the client to determine the FromUser field of a posted message. We've already handled the authentication part of this requirement in the server. However, to complete this requirement, the client also needs to trust the server to at least be able to identify itself (the client). This requires that the client specify an impersonation level of at least Identify. Fortunately, with NT 4-equivalent security, the client's impersonation level is never set below Identify, i.e. even if you set impersonation level=Anonymous, NT will automatically promote it to Identify. If, in the future, this behavior no longer holds, you can run DCOMCNFG on the client machine to specify the desired impersonation level.

Again, it is important to note that the client can also call CoInitializeSecurity to set its desired impersonation level, among others.

On a different note, a client can also specify its desired authentication level, which may or may not be the same as the server's. If they aren't the same, COM will automatically use the higher of the 2 authentication levels as the negotiated level between the client and server. For more information on the impact of this behavior, consult Don Box's (and et. al.) Essential COM and Effective COM.

At this point, I'd like to say that it's this simple to configure DCOM security. However, "simple" is a relative term. In this example, I've assumed that there's a domain controller that holds all client accounts. In the real world, you will have networks that are far more complicated than this. For instance, a simple peer-to-peer network might require you to duplicate user accounts (using the exact names and passwords) from machine to machine for authentication to work. There will also be cases where domain trust relationships or firewalls will drive you insane when configuring DCOM security. I cannot possibly give you examples (nor am I an expert) of all possible scenarios, but I can refer you to a couple of interesting sites on the subject:

Conclusion

I have just shown you how to develop a simple multi-user DCOM application. Along the way, we've learned a lot of interesting and important concepts when working with DCOM. I hope that you can use this knowledge to successfully implement your own DCOM applications.

A Word of Caution

All source code presented here is not necessarily production quality code. In particular, tasks like error handling, multithreading, optimizations, etc. were not taken into consideration because they are not relevant to the tutorial topic. Therefore, I do not make any guarantees that the code will work for you, and you cannot hold me liable for any damages resulting from the use (or misuse) of this material. In other words, don't blame me if you get ****ed!

Copyright (c) 1999-2011 Binh Ly. All Rights Reserved.