Remoting Events

As you might have already guessed from the previous topics, you're already reaching an area of .NET Remoting where the intuitive way of solving the problem might not be the correct one. The remoting of events is another such topic.

First and foremost, as you've seen before, asynchronous calls don't go very well with SoapSuds-generated metadata. Because of this, I'll go back to using interfaces defined in a shared DLL. The reason why I hadn't been following this approach is that you'd miss the opportunity to use the new operator to transparently create references to remote objects. Instead, you would have to use Activator.GetObject() for SAOs and a factory design pattern for CAOs.

To avoid hard coding the URLs in the client application and to provide consistency with the standard .NET Remoting configuration file, you can use the helper class shown in Listing 6-17, which allows you to create a remote object even if it's been designed using an interface or an abstract base.

Listing 6-17: The RemotingHelper Called with typeof(ISomeInterface)

start example
 using System; using System.Collections; using System.Runtime.Remoting; namespace RemotingTools {    public class RemotingHelper {       private static bool _isInit;       private static IDictionary _wellKnownTypes;      public static Object GetObject(Type type) {          if (! _isInit) InitTypeCache();          WellKnownClientTypeEntry entr =              (WellKnownClientTypeEntry) _wellKnownTypes[type];          if (entr == null) {             throw new RemotingException("Type not found!");          }          return Activator.GetObject(entr.ObjectType,entr.ObjectUrl);       }       public static void InitTypeCache() {          _wellKnownTypes= new Hashtable();          foreach (WellKnownClientTypeEntry entr in             RemotingConfiguration.GetRegisteredWellKnownClientTypes()) {             _wellKnownTypes.Add (entr.ObjectType,entr);          }       }    } } 
end example

When you want to instantiate a remote object that implements the interface IMyInterface (which is defined in a shared DLL called General.dll), you can use the following configuration file to register the interface as a remote object:

 <configuration>   <system.runtime.remoting>     <application>       <channels>          <channel ref="http"/>       </channels>       <client>         <wellknown type="General.IMyInterface, General"                     url="http://localhost:5555/MySAO.soap" />       </client>     </application>   </system.runtime.remoting> </configuration> 

After doing this and reading the configuration file with RemotingConfiguration.Configure(), you'll be able to create a reference to the remote SAO using the following statement:

 IMyInterface sao = (IMyInterface) RemotingHelper.GetObject(typeof(IMyInterface); 

without hard coding the URL. Remember, though, that this approach only works when you want to register exactly one server-side object for each interface.

The Problem with Events

Let's say you have to implement a type of broadcast application in which a number of clients register themselves at the servers as listeners and other clients can send messages that will be broadcast to all listening clients.

You need to take into account two key facts when using this kind of application. The first one is by design: when the event occurs, client and server will change roles. This means that the client in reality becomes the server (for the callback method), and the server will act as a client and try to contact the "real" client. This is shown in Figure 6-18.

click to expand
Figure 6-18: The clients will be contacted by the server.

Caution 

This implies that clients located behind firewalls are not able to receive events using any of the included channels!

The second issue can be seen when you are "intuitively" developing this application. In this case, you'd probably start with the interface definition shown in Listing 6-18, which would be compiled to General.dll and shared between the clients and the server.

Listing 6-18: The IBroadcaster Interface (Nonworking Sample)

start example
 using System; using System.Runtime.Remoting.Messaging; namespace General {    public delegate void MessageArrivedHandler(String msg);    public interface IBroadcaster {       void BroadcastMessage(String msg);       event MessageArrivedHandler MessageArrived;    } } 
end example

This interface allows clients to register themselves to receive a notification by using the MessageArrived event. When another client calls BroadcastMessage(), this event will be invoked and the listening clients called back. The server-side implementation of this interface is shown in Listing 6-19.

Listing 6-19: The Server-Side Implementation of IBroadcaster

start example
 using System; using System.Runtime.Remoting; using System.Threading; using General; namespace Server {    public class Broadcaster: MarshalByRefObject, IBroadcaster    {       public event General.MessageArrivedHandler MessageArrived;       public void BroadcastMessage(string msg) {          // call the delegate to notify all listeners          Console.WriteLine("Will broadcast message: {0}", msg);          MessageArrived(msg);       }       public override object InitializeLifetimeService() {          // this object has to live "forever"          return null;       }    }    class ServerStartup    {       static void Main(string[] args)       {          String filename = "server.exe.config";          RemotingConfiguration.Configure(filename);          Console.WriteLine ("Server started, press <return> to exit.");          Console.ReadLine();       }    } } 
end example

The listening client's implementation would be quite straightforward in this case. The only thing you'd have to take care of is that the object that is going to be called back to handle the event has to be a MarshalByRefObject as well. This is shown in Listing 6-20.

Listing 6-20: The First Client's Implementation, That Won't Work

start example
 using System; using System.Runtime.Remoting; using General; using RemotingTools; // RemotingHelper namespace EventListener {    class EventListener    {       static void Main(string[] args)       {          String filename = "eventlistener.exe.config";          RemotingConfiguration.Configure(filename);          IBroadcaster bcaster =             (IBroadcaster) RemotingHelper.GetObject(typeof(IBroadcaster));          Console.WriteLine("Registering event at server");          // callbacks can only work on MarshalByRefObjects, so          // I created a different class for this as well          EventHandler eh = new EventHandler();          bcaster.MessageArrived +=             new MessageArrivedHandler(eh.HandleMessage);          Console.WriteLine("Event registered. Waiting for messages.");          Console.ReadLine();       }    }    public class EventHandler: MarshalByRefObject {       public void HandleMessage(String msg) {          Console.WriteLine("Received: {0}",msg);       }       public override object InitializeLifetimeService() {          // this object has to live "forever"          return null;       }    } } 
end example

When implementing this so-called intuitive solution, you'll be presented with the error message shown in Figure 6-19.

click to expand
Figure 6-19: An exception occurs when combining the delegate with the remote event.

This exception occurs while the request is deserialized at the server. At this point, the delegate is restored from the serialized message and it tries to validate the target method's signature. For this validation, the delegate attempts to load the assembly containing the destination method. In the case presented previously, this will be the client-side assembly EventListener.exe, which is not available at the server.

You're probably thinking, "Great, but how can I use events nevertheless?" I show you how in the next section.

Refactoring the Event Handling

As always, there's the relatively easy solution of shipping the delegate's destination assembly to the caller. This would nevertheless mean that the client-side application has to be referenced at the server—doesn't sound that nice, does it?

Instead, you can introduce an intermediate MarshalByRefObject (including the implementation, not only the interface) that will be located in General.dll and will therefore be accessible by both client and server:

 public class BroadcastEventWrapper: MarshalByRefObject {    public event MessageArrivedHandler MessageArrivedLocally;    [OneWay]    public void LocallyHandleMessageArrived (String msg) {       // forward the message to the client       MessageArrivedLocally(msg);    }    public override object InitializeLifetimeService() {       // this object has to live "forever"       return null;    } } 
Note 

This is still not the final solution, as there are some problems with using [OneWay] events in real-world applications as well. I cover this shortly after the current example!

This wrapper is created in the client's context and provides an event that can be used to call back the "real" client. The server in turn receives a delegate to the BroadcastEventWrapper's LocallyHandleMessageArrived() method. This method activates the BroadcastEventWrapper's MessageArrivedLocally event, which will be handled by the client. You can see the sequence in Figure 6-20.

click to expand
Figure 6-20: Event handling with an intermediate wrapper

The server's event will therefore be handled by a MarshalByRefObject that is known to it (as it's contained in general.dll) so that the delegate can resolve the method's signature. As the BroadcastEventWrapper runs in the client context, its own delegate has access to the real client-side event handler's signature.

The complete source code to General.dll is shown in Listing 6-21.

Listing 6-21: The Shared Assembly Now Contains the BroadcastEventWrapper

start example
 using System; using System.Runtime.Remoting.Messaging; namespace General {    public delegate void MessageArrivedHandler(String msg);    public interface IBroadcaster {       void BroadcastMessage(String msg);       event MessageArrivedHandler MessageArrived;    }    public class BroadcastEventWrapper: MarshalByRefObject {       public event MessageArrivedHandler MessageArrivedLocally;       [OneWay]       public void LocallyHandleMessageArrived (String msg) {          // forward the message to the client          MessageArrivedLocally(msg);       }       public override object InitializeLifetimeService() {          // this object has to live "forever"          return null;       }    } } 
end example

The listening client's source code has to be changed accordingly. Instead of passing the server a delegate to its own HandleMessage() method, it has to create aBroadcastEventWrapper and pass the server a delegate to this object's LocallyHandleMessageArrived() method. The client also has to pass a delegate to its own HandleMessage() method (the "real" one) to the event wrapper's MessageArrivedLocally event.

The changed listening client's source code is shown in Listing 6-22.

Listing 6-22: The New Listening Client's Source Code

start example
 using System; using System.Runtime.Remoting; using General; using RemotingTools; // RemotingHelper namespace EventListener {    class EventListener    {       static void Main(string[] args)       {          String filename = "eventlistener.exe.config";          RemotingConfiguration.Configure(filename);          IBroadcaster bcaster =             (IBroadcaster) RemotingHelper.GetObject(typeof(IBroadcaster));          // this one will be created in the client's context and a          // reference will be passed to the server          BroadcastEventWrapper eventWrapper =             new BroadcastEventWrapper();          // register the local handler with the "remote" handler          eventWrapper.MessageArrivedLocally +=             new MessageArrivedHandler(HandleMessage);          Console.WriteLine("Registering event at server");          bcaster.MessageArrived +=             new MessageArrivedHandler(eventWrapper.LocallyHandleMessageArrived);          Console.WriteLine("Event registered. Waiting for messages.");          Console.ReadLine();       }       public static void HandleMessage(String msg) {          Console.WriteLine("Received: {0}",msg);       }    } } 
end example

When this client is started, you will see the output in Figure 6-21, which shows you that the client is currently waiting for remote events. You can, of course, start an arbitrary number of clients, because the server-side event is implicitly based on a MulticastDelegate.

click to expand
Figure 6-21: The client is waiting for messages.

To start broadcasting messages to all listening clients, you'll have to implement another client. I'm going to call this one EventInitiator in the following examples. The EventInitiator will simply connect to the server-side SAO and invoke its BroadcastMessage() method. You can see the complete source code for EventInitiator in Listing 6-23.

Listing 6-23: EventInitiator Simply Calls BroadcastMessage()

start example
 using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Activation; using General; using RemotingTools; // RemotingHelper namespace Client {    class Client    {       static void Main(string[] args)       {          String filename = "EventInitiator.exe.config";          RemotingConfiguration.Configure(filename);          IBroadcaster bcast =             (IBroadcaster) RemotingHelper.GetObject(typeof(IBroadcaster));          bcast.BroadcastMessage("Hello World! Events work fine now ... ");          Console.WriteLine("Message sent");          Console.ReadLine();       }    } } 
end example

When EventInitiator is started, the output shown in Figure 6-22 will be displayed at each listening client, indicating that the remote events now work as expected.

click to expand
Figure 6-22: Remote events now work successfully!

Why [OneWay] Events Are a Bad Idea

You might have read in some documents and articles that remoting event handlers should be defined as [OneWay] methods. The reason is that without defining remote event handlers this way, an exception will occur whenever a client is unreachable or has been disconnected without first unregistering the event handler.

When just forwarding the call to your event's delegate, as shown in the previous server-side example, two things will happen: the event will not reach all listeners, and the client that initiated the event in the first place will receive an exception. This is certainly not what you want to happen.

When using [OneWay] event handlers instead, the server will try to contact each listener but won't throw an exception if it's unreachable. This seems to be a good thing at first glance. Imagine, however, that your application will run for several months without restarting. As a result, a lot of "unreachable" event handlers will end up registered, and the server will try to contact each of them every time. Not only will this take up network bandwidth, but your performance will suffer as well, as each "nonworking" call might take up some seconds, adding up to minutes of processing time for each event. This, again, is something you wouldn't want in your broadcast application.

Instead of using the default event invocation mechanism (which is fine for local applications), you will have to develop a server-side wrapper that calls all event handlers in a try/catch block and removes all nonworking handlers afterwards. This implies that you define the event handlers without the [OneWay] attribute! To make this work, you first have to remove this attribute from the shared assembly:

 public class BroadcastEventWrapper: MarshalByRefObject {    public event MessageArrivedHandler MessageArrivedLocally;    // don’t use OneWay here!    public void LocallyHandleMessageArrived (String msg) {       // forward the message to the client       MessageArrivedLocally(msg);    }    public override object InitializeLifetimeService() {       // this object has to live "forever"       return null;    } } 

In the server-side code, you remove the call to MessageArrived() and instead implement the logic shown in Listing 6-23, which iterates over the list of registered delegates and calls each one. When an exception is thrown by the framework because the destination object is unreachable, the delegate will be removed from the event.

Listing 6-23: Invoking Each Delegate on Your Own

start example
 using System; using System.Runtime.Remoting; using System.Threading; using General; namespace Server {    public class Broadcaster: MarshalByRefObject, IBroadcaster    {       public event General.MessageArrivedHandler MessageArrived;       public void BroadcastMessage(string msg) {          Console.WriteLine("Will broadcast message: {0}", msg);          SafeInvokeEvent(msg);       }       private void SafeInvokeEvent(String msg) {          // call the delegates manually to remove them if they aren't          // active anymore.          if (MessageArrived == null) {             Console.WriteLine("No listeners");          } else {             Console.WriteLine("Number of Listeners: {0}",                                   MessageArrived.GetInvocationList().Length);             MessageArrivedHandler mah=null;             foreach (Delegate del in MessageArrived.GetInvocationList()) {                try {                   mah = (MessageArrivedHandler) del;                   mah(msg);                } catch (Exception e) {                   Console.WriteLine("Exception occured, will remove Delegate");                   MessageArrived -= mah;                }             }          }       }       public override object InitializeLifetimeService() {          // this object has to live "forever"          return null;       }    }    class ServerStartup    {       static void Main(string[] args)       {          String filename = "server.exe.config";          RemotingConfiguration.Configure(filename);          Console.WriteLine ("Server started, press <return> to exit.");          Console.ReadLine();       }    } } 
end example

When using events in this way, you ensure the best possible performance, no matter how long your server application keeps running.




Advanced  .NET Remoting C# Edition
Advanced .NET Remoting (C# Edition)
ISBN: 1590590252
EAN: 2147483647
Year: 2002
Pages: 91
Authors: Ingo Rammer

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net