Advanced Remoting

The first half of this chapter imparted the fundamental knowledge you need to activate managed objects remotely. You learned how to make classes remotable, how to register them for remote activation, and how to activate them. You learned about the difference between server-activated objects and client-activated objects, and you learned about the two activation modes—single-call and singleton—supported by server-activated objects. You also learned about the role that channels play in remote activation and how leases control a remote object’s lifetime.

The second half of this chapter builds on what you’ve learned and enriches your understanding of .NET remoting. In it, you’ll learn:

This part of the chapter concludes with a distributed drawing application that ties together many of the concepts introduced in the following pages.

Using IIS as an Activation Agent

One of the most striking differences between .NET remoting and DCOM is that the former offers no support for automatically launching server processes. Someone has to start the server process running so that it can register classes for remote activation and listen for activation requests. This behavior contrasts starkly with that of DCOM, which starts new server processes on request when remote clients call CoCreateInstanceEx or other activation API functions.

.NET remoting offers two ways for you to avoid having to manually start server processes. Option number 1 is to implement the server application as a service. You can write a service by deriving from System.ServiceProcess.ServiceBase and overriding key virtual methods such as OnStart and OnStop. The benefit to implementing a remoting server as a service is that you can configure a service to start automatically each time the system starts up. A service can also run absent a logged-on user, meaning that after auto-starting, it’s always running and always available—even when no one is logged in at the server console.

Option number 2 is to use IIS as an activation agent. IIS is a service itself and is always up and running on most Web servers. Moreover, IIS is capable of responding to requests from remote clients who want to activate objects on the server using the .NET remoting infrastructure. Using IIS as an activation agent has several advantages:

IIS supports both server-activated objects and client-activated objects. Classes remoted with IIS’s help can be registered programmatically (in Global.asax) or declaratively (in Web.config). The following Web.config file registers the Clock class in Figure 15-2 for remote activation using IIS:

<configuration>
??<system.runtime.remoting>
????<application>
??????<service>
????????<wellknown?mode="SingleCall" type="Clock,?ClockServer"
??????????objectUri="Clock.rem" />
??????</service>
????</application>
??</system.runtime.remoting>
</configuration>

Note Clock’s URI: Clock.rem. URIs registered with IIS must end in .rem or .soap because both extensions are mapped to Aspnet_isapi.dll in the IIS metabase and to the .NET remoting subsystem in Machine.config. You can register additional extensions if you’d like, but these are the only two that work out of the box.

Objects activated with IIS always use HTTP channels to communicate with remote clients. Clients must also register HTTP channels. Here’s how a client would create a Clock instance, assuming Clock resides in a virtual directory named MyClock on the local machine:

HttpClientChannel?channel?=?new?HttpClientChannel?();
ChannelServices.RegisterChannel?(channel);
RemotingConfiguration.RegisterWellKnownClientType
????(typeof?(Clock), "http://localhost/MyClock/Clock.rem");
Clock?clock?=?new?Clock?();

Notice that no port number is specified anywhere—neither on the client nor on the server. IIS picks the port numbers.

Client-activated objects are registered and activated differently. This Web.config file registers Clock as a client-activated object rather than a server-activated one:

<configuration>
??<system.runtime.remoting>
????<application>
??????<service>
????????<activated?type="Clock,?ClockServer" />
??????</service>
????</application>
??</system.runtime.remoting>
</configuration>

And here’s how a remote client would activate it, once more assuming Clock resides in a virtual directory named MyClock on the local machine:

HttpClientChannel?channel?=?new?HttpClientChannel?();
ChannelServices.RegisterChannel?(channel);
RemotingConfiguration.RegisterActivatedClientType
????(typeof?(Clock), "http://localhost/MyClock");
Clock?clock?=?new?Clock?();

Be aware that IIS client activation requires the remotable class to be hosted in a virtual directory other than wwwroot. You can’t use IIS to activate a client-activated object by putting a Web.config file registering the class in wwwroot and the DLL that implements the class in wwwroot\bin. Instead, you must install Web.config in a separate virtual directory (for example, MyClock) and the DLL in the bin subdirectory (MyClock\bin). Only then will IIS activate a client-activated object.

HTTP Channels and Binary Formatters

One drawback to using IIS as an activation agent is that you have no choice but to use HTTP channels to link application domains. HTTP is a higher-level protocol than TCP and is also less efficient on the wire. Furthermore, HTTP channels encode calls as SOAP messages, which increases the verbosity of message traffic.

Fortunately, the .NET remoting infrastructure uses a pluggable channel architecture that lets you choose the channel type as well as the format in which messages are encoded by the chosen channel. IIS supports only HTTP channels, but it doesn’t require the channel to encode calls as SOAP messages. HTTP channels use SOAP by default because they use formatters named SoapClientFormatterSinkProvider and SoapServerFormatterSinkProvider to serialize and deserialize messages. You can replace these formatters with instances of BinaryClientFormatterSinkProvider and BinaryServerFormatterSinkProvider and encode messages in a more compact binary format. Binary messages utilize network bandwidth more efficiently and still allow you to use IIS as the activation agent.

The following Web.config file registers Clock to be activated by IIS as a single-call server-activated object. It also replaces the default SOAP formatter with a binary formatter on the server side. Changes are highlighted in bold:

<configuration>
??<system.runtime.remoting>
????<application>
??????<service>
????????<wellknown?mode="SingleCall" type="Clock,?ClockServer"
??????????objectUri="Clock.rem" />
??????</service>
??????<channels>
????????<channel?ref="http?server">
??????????<serverProviders>
????????????<formatter?ref="binary" />
??????????</serverProviders>
????????</channel>
??????</channels>
????</application>
??</system.runtime.remoting>
</configuration>

A client that wants to activate instances of Clock that are registered in this way must pair a client-side HTTP channel with a binary formatter too. The following example demonstrates how a client can configure the channel programmatically and then activate a remote instance of Clock:

HttpClientChannel?channel?=?new?HttpClientChannel
????("HttpBinary",?new?BinaryClientFormatterSinkProvider?());
ChannelServices.RegisterChannel?(channel);
RemotingConfiguration.RegisterWellKnownClientType
????(typeof?(Clock), "http://localhost/MyClock/Clock.rem");
Clock?clock?=?new?Clock?();

A client that prefers declarative configuration would do it this way instead:

RemotingConfiguration.Configure?("Client.exe.config");
Clock?clock?=?new?Clock?();

Here are the contents of Client.exe.config:

<configuration>
??<system.runtime.remoting>
????<application>
??????<client>
????????<wellknown?type="Clock,?ClockServer"
??????????url="http://localhost/MyClock/Clock.rem" />
??????</client>
??????<channels>
????????<channel?ref="http?client">
??????????<clientProviders>
????????????<formatter?ref="binary" />
??????????</clientProviders>
????????</channel>
??????</channels>
????</application>
??</system.runtime.remoting>
</configuration>

Combining HTTP channels with binary formatters lets you have your cake and eat it too. Using similar tactics, you could combine TCP channels with SOAP formatters to encode message traffic as SOAP messages. You can even build formatters of your own and plug them into existing channels. The modular nature of the .NET Framework’s remoting infrastructure makes all sorts of interesting extensions possible without requiring you to write a ton of code or replace portions of the framework that you have no desire to replace.

Delegates and Events

One of the hallmarks of the .NET Framework’s type system is that it elevates events to first-class type members along with methods, properties, and fields. Better still, the framework’s event infrastructure works with remote objects as well as local objects. A client connects event handlers to events fired by remote objects using the very same syntax that it uses to connect handlers to events fired by local objects. The only catch is that the client must register a server channel as well as a client channel so that it can receive event callbacks. By the same token, the server, which normally registers only a server channel, must register a client channel too so that it can fire events to remote clients.

Suppose you built a Clock class that fires a NewHour event at the top of every hour. Here’s how that class—and a delegate defining the signature of NewHour handlers—might be declared:

public?delegate?void?NewHourHandler?(int?hour);

public?class?Clock?:?MarshalByRefObject
{
????public?event?NewHourHandler?NewHour;
??????...
}

Here’s a Web.config file that registers Clock for remote activation as a server-activated singleton using IIS:

<configuration>
??<system.runtime.remoting>
????<application>
??????<service>
????????<wellknown?mode="Singleton" type="Clock,?ClockServer"
??????????objectUri="Clock.rem" />
??????</service>
??????<channels>
????????<channel?ref="http" />
??????</channels>
????</application>
??</system.runtime.remoting>
</configuration>

Note the ref attribute accompanying the channel element. The value “http” instantiates a two-way HttpChannel object instead of a one-way HttpServerChannel. The two-way channel is necessary if Clock is to receive calls from remote clients and fire events to them as well.

Here’s client code to create a Clock instance and register a handler for NewHour events:

RemotingConfiguration.Configure?("Client.exe.config");
Clock?clock?=?new?Clock?();
clock.NewHour?+=?new?NewHourHandler?(OnNewHour);
??.
??.
??.
public?void?OnNewHour?(int?hour)
{
????//?NewHour?event?received
}

And here’s the CONFIG file referenced in the client’s code, which assumes that Clock is deployed in a virtual directory named MyClock:

<configuration>
??<system.runtime.remoting>
????<application>
??????<client>
????????<wellknown?type="Clock,?ClockServer"
??????????url="http://localhost/MyClock/Clock.rem" />
??????</client>
??????<channels>
????????<channel?ref="http" port="0" />
??????</channels>
????</application>
??</system.runtime.remoting>
</configuration>

The client also registers a two-way HttpChannel, and it specifies a port number of 0. The 0 configures the channel to listen for callbacks but permits the .NET Framework to pick the port number.

A client that receives events from remote objects must have access to metadata describing the objects. In addition, the objects must have metadata describing the client—at least the client components that contain the callback methods. The practical implication is that if you deploy clients on one machine and remote classes on another, you need to put the clients’ binaries in the directory that holds the server components and vice versa.

Asynchronous Method Calls

By default, calls to remote objects are synchronous. A thread that places a synchronous call blocks until the call returns. If the call takes a long time to find its way to the recipient or the recipient takes a long time to process the call once it arrives, the caller waits for a long time as well.

That’s why the .NET Framework supports asynchronous method calls. Asynchronous method calls aren’t limited to remote objects; they work with local objects too. They’re enacted through asynchronous delegates, which make placing asynchronous calls almost as easy as placing synchronous ones. Asynchronous calls return immediately, no matter how long they take to reach their recipients or how long the recipients take to process them.

Suppose a remote object has a CountPrimes method similar to the one in Figure 14-2. Counting primes is a CPU-intensive task that can take a long time to complete. Calling CountPrimes as in the following takes more than 10 seconds on my PC—a 1.4 GHz Pentium with 384 MB of memory:

int?count?=?sieve.CountPrimes?(100000000);

If called through an asynchronous delegate, however, CountPrimes returns immediately. To call CountPrimes asynchronously, you first declare a delegate whose signature matches CountPrimes’ signature, as shown here:

delegate?int?CountPrimesDelegate?(int?max);

You then wrap an instance of the delegate around CountPrimes and call the delegate’s BeginInvoke method:

CountPrimesDelegate?del?=?new?CountPrimesDelegate?(sieve.CountPrimes);
IAsyncResult?ar?=?del.BeginInvoke?(100000000,?null,?null);

To retrieve the value that CountPrimes returns, you later complete the call by calling the delegate’s EndInvoke method and passing in the IAsyncResult returned by BeginInvoke:

int?count?=?del.EndInvoke?(ar);

If CountPrimes hasn’t returned when EndInvoke is called, EndInvoke blocks until it does. Calling BeginInvoke and EndInvoke in rapid succession is morally equivalent to calling CountPrimes synchronously.

Can a client determine whether an asynchronous call has completed before calling EndInvoke? You bet. The IsCompleted property of the IAsyncResult that BeginInvoke returns is true if the call has completed, false if it has not. The following code snippet calls EndInvoke if and only if the call has completed:

CountPrimesDelegate?del?=?new?CountPrimesDelegate?(sieve.CountPrimes);
IAsyncResult?ar?=?del.BeginInvoke?(100000000,?null,?null);
??.
??.
??.
if?(ar.IsCompleted)?{
????int?count?=?del.EndInvoke?(ar);
}
else?{
????//?Try?again?later
}

A client can also use IAsyncResult’s AsyncWaitHandle property to retrieve a synchronization handle. A thread that calls WaitOne on that handle blocks until the call completes.

A client can also ask to be notified when an asynchronous call completes. Completion notifications enable a client to learn when a call completes without polling IsCompleted. The basic strategy is to wrap a callback method in an instance of System.AsyncCallback and pass the resulting delegate to BeginInvoke. When the call completes, the callback method is called. Here’s an example:

CountPrimesDelegate?del?=?new?CountPrimesDelegate?(sieve.CountPrimes);
AsyncCallback?ab?=?new?AsyncCallback?(PrimesCounted);
IAsyncResult?ar?=?del.BeginInvoke?(100000000,?ab,?null);
??.
??.
??.
void?PrimesCounted?(IAsyncResult?ar)
{
????//?CountPrimes?completed
}

After the callback method is called, you still need to complete the call by calling EndInvoke. Only by calling EndInvoke can you get the results of the call and let the system clean up after itself following a successful asynchronous call. You can call EndInvoke from inside the callback method if you’d like.

One-Way Methods

The .NET remoting infrastructure supports a slightly different form of asynchronous method calls that rely on entities known as one-way methods. A one-way method has input parameters only—no out or ref parameters are allowed—or no parameters at all, and it returns void. You designate a method as a one-way method by tagging it with a OneWay attribute:

[OneWay]
public?void?LogError?(string?message)
{
??...
}

OneWay is shorthand for OneWayAttribute, which is an attribute class defined in the System.Runtime.Remoting.Messaging namespace.

Calls to one-way methods execute asynchronously. You don’t get any results back, and you aren’t notified if the method throws an exception. You don’t even know for sure that a one-way method call reached the recipient. One-way methods let you place calls using “fire-and-forget” semantics, which are appropriate when you want to fire off a method call, you don’t want any results back, and the method’s success or failure isn’t critical to the integrity of the application as a whole. Sound intriguing? The application in the next section uses one-way methods to fire notifications to remote objects without affecting the liveliness of its user interface. As you work with .NET remoting, you’ll probably find additional uses for one-way methods.

Putting It All Together: The NetDraw Application

Let’s close with an application that assembles many of the concepts outlined in this chapter into one tidy package. The application is shown in Figure 15-5. It’s a distributed drawing program that links clients together so that sketches drawn in one window appear in the other windows, too. Before you try it, you need to deploy it. Here’s how:

  1. Build the application’s binaries. Here are the commands:

    csc?/t:library?paperserver.cs
    csc?/t:winexe?/r:paperserver.dll?netdraw.cs

    These commands produce binaries named PaperServer.dll and NetDraw.exe. PaperServer.dll implements a remotable class named Paper and a utility class named Stroke. It also declares a delegate that clients can use to wrap handlers for the NewStroke events that Paper fires. NetDraw.exe is a Windows Forms application that serves as a remote client to Paper objects.

  2. Create a virtual directory named NetDraw on your Web server. Copy Web.config to the NetDraw directory. Create a bin subdirectory in the NetDraw directory and copy both NetDraw.exe and PaperServer.dll to bin.

  3. Create another directory somewhere on your Web server (it needn’t be a virtual directory) and copy NetDraw.exe, NetDraw.exe.config, and PaperServer.dll to it.

Now start two instances of NetDraw.exe and scribble in one of them by moving the mouse with the left button held down. Each time you release the button, the stroke that you just drew should appear in the other NetDraw window. If you’d like to try it over a network, simply move NetDraw.exe, NetDraw.exe.config, and PaperServer.dll to another machine and modify the URL in NetDraw.exe.config to point to the remote server.

Figure 15-5
The NetDraw application.

NetDraw, whose source code appears in Figure 15-6, demonstrates several important principles of .NET remoting. Let’s tackle the big picture first. At startup, the client—NetDraw.exe—instantiates a Paper object and registers a handler for the object’s NewStroke events:

VirtualPaper?=?new?Paper?();
NewStrokeHandler?=?new?StrokeHandler?(OnNewStroke);
VirtualPaper.NewStroke?+=?NewStrokeHandler;

Upon completion of each new stroke drawn by the user (that is, when the mouse button comes up), NetDraw calls the Paper object’s DrawStroke method and passes in a Stroke object containing a series of x-y coordinate pairs describing the stroke:

VirtualPaper.DrawStroke?(CurrentStroke);

DrawStroke, in turn, fires a NewStroke event to all clients that registered handlers for NewStroke events. It passes the Stroke provided by the client in the event’s parameter list:

public?void?DrawStroke?(Stroke?stroke)
{
????if?(NewStroke?!=?null)
????????NewStroke?(stroke);
}

The event activates each client’s OnNewStroke method, which adds the Stroke to a collection of Stroke objects maintained by each individual client and draws the stroke on the screen. Because Paper is registered as a singleton, all clients that call new on it receive a reference to the same Paper object. Consequently, a stroke drawn in one client is reported immediately to the others.

That’s the view from 10,000 feet. The meat, however, is in the details. Here are some highlights to look for as you peruse the source code:

If NetDraw.cs looks familiar to you, that’s because it’s almost identical to Chapter 4’s NetDraw.cs. With a little help from .NET remoting, it didn’t take much to turn a stand-alone application into a distributed application. You provide client and server components; the framework provides the plumbing that connects them. That’s what .NET remoting is all about.

PaperServer.cs
using?System;
using?System.Drawing;
using?System.Drawing.Drawing2D;
using?System.Runtime.Remoting.Messaging;
using?System.Collections;

public?delegate?void?StrokeHandler?(Stroke?stroke);

public?class?Paper?:?MarshalByRefObject
{
????public?event?StrokeHandler?NewStroke;

????public?override?object?InitializeLifetimeService?()
????{
????????return?null;
????}

????[OneWay]
????public?void?DrawStroke?(Stroke?stroke)
????{
????????if?(NewStroke?!=?null)
????????????NewStroke?(stroke);
????}
}

[Serializable]
public?class?Stroke
{
????ArrayList?Points?=?new?ArrayList?();

????public?int?Count
????{
????????get?{?return?Points.Count;?}
????}

????public?Stroke?(int?x,?int?y)
????{
????????Points.Add?(new?Point?(x,?y));
????}
????public?void?Add?(int?x,?int?y)
????{
????????Points.Add?(new?Point?(x,?y));
????}

????public?void?Draw?(Graphics?g)
????{
????????Pen?pen?=?new?Pen?(Color.Black,?8);
????????pen.EndCap?=?LineCap.Round;
????????for?(int?i=0;?i<Points.Count?-?1;?i++)
????????????g.DrawLine?(pen,?(Point)?Points[i],?(Point)?Points[i?+?1]);
????????pen.Dispose?();
????}

????public?void?DrawLastSegment?(Graphics?g)
????{
????????Point?p1?=?(Point)?Points[Points.Count?-?2];
????????Point?p2?=?(Point)?Points[Points.Count?-?1];
????????Pen?pen?=?new?Pen?(Color.Black,?8);
????????pen.EndCap?=?LineCap.Round;
????????g.DrawLine?(pen,?p1,?p2);
????????pen.Dispose?();
????}
}
Figure 15-6
Source code for a distributed drawing application.
Web.config
<configuration>
??<system.runtime.remoting>
????<application>
??????<service>
????????<wellknown?mode="Singleton" type="Paper,?PaperServer"
??????????objectUri="Paper.rem" />
??????</service>
??????<channels>
????????<channel?ref="http">
??????????<clientProviders>
????????????<formatter?ref="binary"/>
??????????</clientProviders>
??????????<serverProviders>
????????????<formatter?ref="binary"/>
??????????</serverProviders>
????????</channel>
??????</channels>
????</application>
??</system.runtime.remoting>
</configuration>
NetDraw.cs
using?System;
using?System.Collections;
using?System.Windows.Forms;
using?System.Drawing;
using?System.Drawing.Drawing2D;
using?System.Runtime.Remoting;
using?System.ComponentModel;

class?MyForm?:?Form
{
????Paper?VirtualPaper;
????Stroke?CurrentStroke?=?null;
????ArrayList?Strokes?=?new?ArrayList?();
????StrokeHandler?NewStrokeHandler;

????MyForm?()
????{
????????Text?= "NetDraw";

????????try?{
????????????//?Configure?the?remoting?infrastructure
????????????RemotingConfiguration.Configure?("NetDraw.exe.config");

????????????//?Create?a?remote?Paper?object
????????????VirtualPaper?=?new?Paper?();

????????????//?Connect?a?handler?to?the?object's?NewStroke?events
????????????NewStrokeHandler?=?new?StrokeHandler?(OnNewStroke);
????????????VirtualPaper.NewStroke?+=?NewStrokeHandler;
????????}
????????catch?(Exception?ex)?{
????????????MessageBox.Show?(ex.Message);
????????????Close?();
????????}
????}

????protected?override?void?OnPaint?(PaintEventArgs?e)
????{
????????lock?(Strokes.SyncRoot)?{
????????????//?Draw?all?currently?recorded?strokes
????????????foreach?(Stroke?stroke?in?Strokes)
????????????????stroke.Draw?(e.Graphics);
????????}
????}

????protected?override?void?OnMouseDown?(MouseEventArgs?e)
????{
????????if?(e.Button?==?MouseButtons.Left)?{
????????????//?Create?a?new?Stroke?and?assign?it?to?CurrentStroke
????????????CurrentStroke?=?new?Stroke?(e.X,?e.Y);
????????}
????}

????protected?override?void?OnMouseMove?(MouseEventArgs?e)
????{
????????if?((e.Button?&?MouseButtons.Left)?!=?0?&&
????????????CurrentStroke?!=?null)?{
????????????//?Add?a?new?segment?to?the?current?stroke
????????????CurrentStroke.Add?(e.X,?e.Y);
????????????Graphics?g?=?Graphics.FromHwnd?(Handle);
????????????CurrentStroke.DrawLastSegment?(g);
????????????g.Dispose?();
????????}
????}

????protected?override?void?OnMouseUp?(MouseEventArgs?e)
????{
????????if?(e.Button?==?MouseButtons.Left?&&?CurrentStroke?!=?null)?{
????????????//?Complete?the?current?stroke
????????????if?(CurrentStroke.Count?>?1)?{
????????????????//?Let?other?clients?know?about?it,?too
????????????????VirtualPaper.DrawStroke?(CurrentStroke);
????????????}
????????????CurrentStroke?=?null;
????????}
????}

????protected?override?void?OnKeyDown?(KeyEventArgs?e)
????{
????????if?(e.KeyCode?==?Keys.Delete)?{
????????????//?Delete?all?strokes?and?repaint
????????????lock?(Strokes.SyncRoot)?{
????????????????Strokes.Clear?();
????????????}????
????????????Invalidate?();
????????}
????}

????protected?override?void?OnClosing?(CancelEventArgs?e)
????{
????????//?Disconnect?event?handler?before?closing
????????base.OnClosing?(e);
????????VirtualPaper.NewStroke?-=?NewStrokeHandler;
????}

????public?void?OnNewStroke?(Stroke?stroke)
????{
????????//?Record?and?display?a?stroke?drawn?in?a?remote?client
????????lock?(Strokes.SyncRoot)?{
????????????Strokes.Add?(stroke);
????????}
????????Graphics?g?=?Graphics.FromHwnd?(Handle);
????????stroke.Draw?(g);
????????g.Dispose?();
????}

????static?void?Main?()
????{
????????Application.Run?(new?MyForm?());
????}
}
NetDraw.exe.config
<configuration>
??<system.runtime.remoting>
????<application>
??????<client>
????????<wellknown?type="Paper,?PaperServer"
??????????url="http://localhost/NetDraw/Paper.rem" />
??????</client>
??????<channels>
????????<channel?ref="http" port="0">
??????????<clientProviders>
????????????<formatter?ref="binary" />
??????????</clientProviders>
??????????<serverProviders>
????????????<formatter?ref="binary" />
??????????</serverProviders>
????????</channel>
??????</channels>
????</application>
??</system.runtime.remoting>
</configuration>