|
|
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)
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); } } } }
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.
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.
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)
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; } }
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
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(); } } }
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
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; } } }
When implementing this so-called intuitive solution, you'll be presented with the error message shown in Figure 6-19.
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.
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.
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
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; } } }
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
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); } } }
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.
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()
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(); } } }
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.
Figure 6-22: Remote events now work successfully!
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
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(); } } }
When using events in this way, you ensure the best possible performance, no matter how long your server application keeps running.
|
|