|
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.
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:
- 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).
- 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
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:
- TMessage - represents a single message
- 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 >>
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:
- TMessage - represents a single message
- 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 ();
}
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:
- The user that is currently logged in
to the server machine (Interactive User)
- The remote (or local) user that
requested to launch the server (Launching User)
- 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:
- Determine and configure the
authentication level
- Determine and configure the access
control permissions
- Determine and configure the launch
control permissions
- 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!
|