Miscellaneous .NET Remoting Features

 
Chapter 21 - Distributed Applications with .NET Remoting
bySimon Robinsonet al.
Wrox Press 2002
  

In the final section of this chapter we shall explore the following .NET Remoting features such as

  • How application configuration files can be used to define remoting channels

  • Hosting .NET Remoting Servers in a IIS Server by using the ASP.NET runtime

  • Different ways to get the type information of the server for building the client with the utility SOAPSuds

  • How tracking services can help with debugging

  • Calling .NET Remoting methods asynchronously

  • Implementing events to callback methods in the client

  • Using call contexts to pass some data to the server behind the scenes automatically

Configuration Files

Instead of writing the channel and object configuration in the sourcecode, configuration files can be used. This way the channel can be reconfigured, additional channels can be added, and so on, without changing the sourcecode. Like all the other configuration files on the .NET platform, XML is used. The same application and configuration files that you read about in .config . For the server HelloServer.exe the configuration file is HelloServer.exe.config .

In the code download, you'll find the following example configuration files in the root directory of the client and server examples, under the names clientactivated.config and wellknown.config . With the client example you will also find the file wellknownhttp.config that specifies an HTTP channel to a well-known remote object. To use these configurations, the files must be renamed as above and placed in the directory containing the executable file.

Here is just one example of how such a configuration file could look. We will walk through all the different configuration options:

   <configuration>     <system.runtime.remoting>     <application name="Hello">     <service>     <wellknown mode="SingleCall"     type="Wrox.ProCSharp.Remoting.Hello, RemoteHello"     objectUri="Hi" />     </service>     <channels>     <channel ref="tcp" port="6791" />     <channel ref="http" port="6792" />     </channels>     </application>     </system.runtime.remoting>     </configuration>   

< configuration > is the XML root element for all .NET configuration files. All the remoting configurations can be found in the sub-element < system.runtime.remoting >. < application > is a subelement of < system.runtime.remoting >.

Let's look at the main elements and attributes of the parts within < system.runtime.remoting >:

  • With the < application > element we can specify the name of the application using the attribute name . On the server side, this is the name of the server, and on the client side it's the name of the client application. As an example for a server configuration, < application name="Hello" > defines the remote application name Hello , which is used as part of the URL by the client to access the remote object.

  • On the server, the element < service > is used to specify a collection of remote objects. It can have < wellknown > and < activated > subelements to specify the type of the remote object as well-known or client-activated .

  • The client part of the < service > element is < client >. Like the < service > element, it can have < wellknown > and < activated > subelements to specify the type of the remote object. Unlike the < service > counterpart , < client > has a url attribute to specify the URL to the remote object.

  • < wellknown > is a element that's used on the server and the client to specify well-known remote objects. The server part could look like this:

     <wellknown mode="SingleCall"            type="Wrox.ProCSharp.Remoting.Hello, RemoteHello"                  objectURI="Hi" /> 

While the mode attribute SingleCall or Singleton can be specified, the type is the type of the remote class including the namespace Wrox.ProCSharp.Remoting.Hello , followed by the assembly name RemoteHello . objectURI is the name of the remote object that's registered in the channel.

On the client, the type attribute is the same as for the server version. mode and objectURI are not needed, but instead the url attribute is used to define the path to the remote object: protocol, hostname, port-number, application-name, and the object URI:

 <wellknown type="Wrox.ProCSharp.Remoting.Hello, RemoteHello"                            url="tcp://localhost:6791/Hello/Hi" /> 
  • The < activated > element is used for client-activated objects. With the type attribute the type and assembly must be defined both for the client and the server application:

     <activated type="Wrox.ProCSharp.Remoting.Hello, RemoteHello" /> 
  • To specify the channel, the < channel > element is used. It's a subelement of < channels > so that a collection of channels can be configured for a single application. Its use is similar for clients and servers. With the XML attribute ref we reference a channel name that is configured in the configuration file machine.config . We will look into this file next . For the server channel we have to set the port number with the XML attribute port . The XML attribute displayName is used to specify a name for the channel that is used from the .NET Framework Configuration tool, as we will see later.

     <channels>             <channel ref="tcp" port="6791" displayName="TCP Channel" />            <channel ref="http" port="6792" displayName="HTTP Channel" />         </channels> 

Predefined Channels in machine.config

In the machine.config configuration file that you can find in the directory < windir > \Microsoft.NET\Framework\ < version > \CONFIG , predefined channels can be found. These predefined channels can be used in your application, or you can specify your own channel class.

In the XML file below you can see an extract of the machine.config file showing the predefined channels. The < channel > element is used as a subelement of < channels > to define channels. Here the attribute id specifies a name of a channel that can be referenced with the ref attribute. With the type attribute the class of the channel is specified followed by the assembly; for example, the channel class System.Runtime.Remoting.Channels.Http.HttpChannel can be found in the assembly System.Runtime.Remoting . Because the System.Runtime.Remoting assembly is shared, the strong name of the assembly must be specified with Version , Culture , and PublicKeyToken .

   <system.runtime.remoting>     <!-- ... -->     <channels>     <channel id="http" type="System.Runtime.Remoting.Channels.Http.HttpChannel,     System.Runtime.Remoting, Version=1.0.3300.0, Culture=neutral,     PublicKeyToken=b77a5c561934e089"/>     <channel id="http client"     type="System.Runtime.Remoting.Channels.Http.HttpClientChannel,     System.Runtime.Remoting, Version=1.0.3300.0, Culture=neutral,     PublicKeyToken=b77a5c561934e089"/>     <channel id="http server"     type="System.Runtime.Remoting.Channels.Http.HttpServerChannel,     System.Runtime.Remoting, Version=1.0.3300.0, Culture=neutral,     PublicKeyToken=b77a5c561934e089"/>     <channel id="tcp" type="System.Runtime.Remoting.Channels.Tcp.TcpChannel,     System.Runtime.Remoting, Version=1.0.3300.0, Culture=neutral,     PublicKeyToken=b77a5c561934e089"/>     <channel id="tcp client"     type="System.Runtime.Remoting.Channels.Tcp.TcpClientChannel,     System.Runtime.Remoting, Version=1.0.3300.0, Culture=neutral,     PublicKeyToken=b77a5c561934e089"/>     <channel id="tcp server"     type="System.Runtime.Remoting.Channels.Tcp.TcpServerChannel,     System.Runtime.Remoting, Version=1.0.3300.0, Culture=neutral,     PublicKeyToken=b77a5c561934e089"/>     </channels>     <!-- ... -->     <system.runtime.remoting>   

Server Configuration for Well-Known Objects

This example file, wellknown.config , has the value Hello for the name property. We are using the TCP channel to listen on port 6791, and the HTTP channel to listen on port 6792. The remote object class is Wrox.ProCSharp.Remoting.Hello in the assembly RemoteHello , the object is called Hi in the channel, and we are using the mode SingleCall :

   <configuration>     <system.runtime.remoting>     <application name="Hello">     <service>     <wellknown mode="SingleCall"     type="Wrox.ProCSharp.Remoting.Hello, RemoteHello"     objectUri="Hi" />     </service>     <channels>     <channel ref="tcp" port="6791"     displayName="TCP Channel (HelloServer)" />     <channel ref="http" port="6792"     displayName="HTTP Channel (HelloServer)" />     </channels>     </application>     </system.runtime.remoting>     </configuration>   

Client Configuration for Well-Known Objects

For well-known objects, we have to specify the assembly and the channel in the client configuration file wellknown.config . The types for the remote object can be found in the RemoteHello assembly, Hi is the name of the object in the channel, and the URI for the remote type Wrox.ProCSharp.Remoting.Hello is tcp://localhost:6791/Hi . In the client we are also using a TCP channel, but in the client no port is specified, so a free port is selected:

   <configuration>     <system.runtime.remoting>     <application name="Client">     <client url="tcp:/localhost:6791/Hello"     displayName="Hello client for well-known objects">     <wellknown type = "Wrox.ProCSharp.Remoting.Hello, RemoteHello"     url="tcp://localhost:6791/Hello/Hi" />     </client>     <channels>     <channel ref="tcp" displayName="TCP Channel (HelloClient)" />     </channels>     </application>     </system.runtime.remoting>     </configuration>   

A small change in the configuration file, and we're using the HTTP channel (as can be seen in wellknownhttp.config) :

   <client url="http:/localhost:6792/Hello">     <wellknown type="Wrox.ProCSharp.Remoting.Hello, RemoteHello"     url="http://localhost:6792/Hello/Hi" />     </client>     <channels>     <channel ref="http" displayName="HTTP Channel (HelloClient)" />     </channels>   

Server Configuration for Client-Activated Objects

By changing only the configuration file (which can be found in clientactivated.config ), we can change the server configuration from server-activated to client-activated objects. Here the < activated > subelement of the < service > element is specified. With the < activated > element for the server configuration, just the type attribute must be specified. The name attribute of the application element defines the URI:

 <configuration>    <system.runtime.remoting>   <application name="HelloServer">     <service>     <activated type="Wrox.ProCSharp.Remoting.Hello, RemoteHello" />     </service>   <channels>             <channel ref="http" port="6788"                       displayName="HTTP Channel (HelloServer)" />             <channel ref="tcp" port="6789"                 displayName="TCP Channel (HelloServer)" />          </channels>       </application>    </system.runtime.remoting> </configuration> 

Client Configuration for Client-Activated Objects

The clientactivated.config file defines the client-activated remote object using the url attribute of the < client > element and the type attribute of the < activated > element:

 <configuration>    <system.runtime.remoting>       <application>   <client url="http://localhost:6788/HelloServer"     displayName="Hello client for client-activated objects">     <activated type="Wrox.ProCSharp.Remoting.Hello, RemoteHello" />     </client>   <channels>             <channel ref="http" displayName="HTTP Channel (HelloClient)" />             <channel ref="tcp" displayName="TCP Channel (HelloClient)" />          </channels>       </application>    </system.runtime.remoting> </configuration> 

Server Code Using Configuration Files

In the server code we have to configure remoting using the static method Configure() from the RemotingConfiguration class. Here all the channels that are defined are built up and instantiated . Maybe we also want to know about the channel configurations from the server application - that's why I've created the static methods ShowActivatedServiceTypes() and ShowWellKnownServiceTypes(); they are called after loading and starting the remoting configuration:

 public static void Main(string[] args) {   RemotingConfiguration.Configure("HelloServer.exe.config");   Console.WriteLine("Application: " + RemotingConfiguration.ApplicationName);    ShowActivatedServiceTypes();    ShowWellKnownServiceTypes();    System.Console.WriteLine("hit to exit");    System.Console.ReadLine();    return; } 

These two functions show configuration information of well-known and client-activated types:

   public static void ShowWellKnownServiceTypes()     {     WellKnownServiceTypeEntry[] entries =     RemotingConfiguration.GetRegisteredWellKnownServiceTypes();     foreach (WellKnownServiceTypeEntry entry in entries)     {     Console.WriteLine("Assembly: " + entry.AssemblyName);     Console.WriteLine("Mode: " + entry.Mode);     Console.WriteLine("URI: " + entry.ObjectUri);     Console.WriteLine("Type: " + entry.TypeName);     }     }     public static void ShowActivatedServiceTypes()     {     ActivatedServiceTypeEntry[] entries =     RemotingConfiguration.GetRegisteredActivatedServiceTypes();     foreach (ActivatedServiceTypeEntry entry in entries)     {     Console.WriteLine("Assembly: " + entry.AssemblyName);     Console.WriteLine("Type: " + entry.TypeName);     }     }   

Client Code Using Configuration Files

In the client code, we only have to configure the remoting services using the configuration file client.exe.config . After that, we can use the new operator to create new instances of the Remote class, no matter whether we work with server-activated or client-activated remote objects. Be aware, however - there's a small difference! With client-activated objects it's now possible to use non-default constructors with the new operator. This isn't possible for server-activated objects, and it doesn't make sense there: SingleCall objects can have no state because they are destroyed with every call; Singleton objects are created just once. Calling non-default constructors is only useful for client-activated objects because it is only for this kind of objects that the new operator really calls the constructor in the remote object.

In the Main() method of the file HelloClient.cs we can now change the remoting code to use the configuration file with RemotingConfiguration.Configure() , and we create the remote object with the new operator:

   RemotingConfiguration.Configure("HelloClient.exe.config");     Hello obj = new Hello();   if (obj == null) {    Console.WriteLine("could not locate server");    return 0; } for (int i=0; i < 5; i++) {    Console.WriteLine(obj.Greeting("Christian")); } 

Delayed Loading of Client Channels

With the configuration file machine.config , two channels are configured that can be used automatically if the client doesn't configure a channel.

   <system.runtime.remoting>     <application>     <channels>     <channel ref="http client" displayName="http client (delay loaded)"     delayLoadAsClientChannel="true"/>     <channel ref="tcp client" displayName="tcp client (delay loaded)"     delayLoadAsClientChannel="true"/>     </channels>     </application>     </system.runtime.remoting>   

The XML attribute delayLoadAsClientChannel with a value true defines that the channel should be used from a client that doesn't configure a channel. The runtime tries to connect to the server using the delay loaded channels. So it is not necessary to configure a channel in the client configuration file, and a client configuration file for the well-known object we have used earlier can look as simple as this:

   <configuration>     <system.runtime.remoting>     <application name="Client">     <client url="tcp:/localhost:6791/Hello">     <wellknown type = "Wrox.ProCSharp.Remoting.Hello, RemoteHello"     url="tcp://localhost:6791/Hello/Hi" />     </client>     </application>     </system.runtime.remoting>     </configuration>   

Lifetime Services in Configuration Files

Leasing configuration for remote servers can also be done with the application configuration files. The < lifetime > element has the attributes leaseTime , sponsorshipTimeOut , renewOnCallTime , and pollTime as you see here:

   <configuration>     <system.runtime.remoting>     <application>     <lifetime leaseTime = "15M" sponsorshipTimeOut = "4M"     renewOnCallTime = "3M" pollTime = "30s"/>     </application>     </system.runtime.remoting>     </configuration>   

Using configuration files, it is possible to change the remoting configuration by editing files instead of working with sourcecode. We can easily change the channel to use HTTP instead of TCP, change a port, the name of the channel, and so on. With the addition of a single line the server can listen to two channels instead of one.

.NET Framework Configuration Tool

The System Administrator can use the .NET Framework Configuration Tool to reconfigure existing configuration files. You can find this tool with the Administrative Tools in the Control Panel.

Adding the application HelloClient.exe where we used the client configuration file to the configured applications in this tool, we can configure the URL of the remote object by selecting the hyperlink View Remoting Services Properties :

click to expand

For the client application we can see the value of the displayName attribute in the combo box to select the remote application, and we can change the URL of the remote object:

click to expand

Adding the server application to this tool we can change the configuration of the remote object and the channels as the two pictures below demonstrate :

click to expand

Hosting Applications

Up to this point all our sample servers were running in self-hosted .NET servers. A self-hosted server must be launched manually. A .NET remoting server can also be started in a lot of other application types. In a Windows Service the server can be automatically started at boot-time, and in addition the process can run with the credentials of the system account. We'll see more about Windows Services in Chapter 22.

Hosting Remote Servers in ASP.NET

There's special support for .NET Remoting servers for ASP.NET. ASP.NET can be used for the automatic startup of remote servers. Contrary to EXE-hosted applications, ASP.NET Remoting uses a different file for configuration.

To use the infrastructure from the Internet Information Server and ASP.NET, we just have to create a class that derives from System.MarshalByRefObject and has a default constructor. The code used earlier for our server to create and register the channel is no longer necessary; that's done by the ASP.NET runtime. We just have to create a virtual directory on the web server that maps a directory to where we put the configuration file web.config . The assembly of the remote class must reside in the bin subdirectory.

To configure a virtual directory on the web server we can use the Internet Information Services MMC. Selecting the Default Web Site and opening the Action menu creates a new Virtual Directory.

The configuration file web.config on the web server must be put in the home directory of the virtual web site. With the default IIS configuration, the channel that will be used listens to port 80:

   <configuration>     <system.runtime.remoting>     <application>     <service>     <wellknown mode="SingleCall"     type="Wrox.ProCSharp.Remoting.Hello, RemoteHello"     objectUri="HelloService.soap" />     </service>     </application>     </system.runtime.remoting>     </configuration>   

The client can now connect to the remote object using the following configuration file. The URL that must be specified for the remote object here is the web server localhost , followed by the web application name RemoteHello (specified when creating the virtual web site), and the URI of the remote object HelloService.soap that we defined in the file web.config . It's not necessary to specify the port number 80, because that's the default port for the HTTP protocol. Not specifying a < channels > section means that we use the delay loaded HTTP channel from the configuration file machine.config :

   <configuration>     <system.runtime.remoting>     <application>     <client url="http:/localhost/RemoteHello">     <wellknown type="Wrox.ProCSharp.Remoting.Hello, RemoteHello"     url="http://localhost/RemoteHello/HelloService.soap" />     </client>     </application>     </system.runtime.remoting>     </configuration>   
Important 

Hosting remote objects in ASP.NET only supports well-known objects.

Classes, Interfaces, and SoapSuds

In the client-server examples we've done up until now, we have always copied the assembly of the remote object not only to the server, but also to the client application. This way we have the MSIL code of the remote object in both the client and the server applications, although in the client application only the metadata is needed. However, copying the remoting object assembly means that it's not possible for the client and server to be programmed independently. A much better way to use just the metadata is to use interfaces or the SoapSuds.exe utility instead.

Interfaces

We have a cleaner separation of the client and server code using interfaces. An interface simply defines the methods without implementation. We separate the contract between the client and the server from the implementation. Here are the necessary steps to use an interface:

  1. Define an interface that will be placed in a separate assembly.

  2. Implement the interface in the remote object class. To do this, the assembly of the interface must be referenced.

  3. On the server side no more changes are required. The server can be programmed and configured in the usual ways.

  4. On the client side, reference the assembly of the interface instead of the assembly of the remote class.

  5. The client can now use the interface of the remote object rather than the remote object class. The object can be created using the Activator class as we've done earlier. You can't use the new operator in this way, because the interface itself cannot be instantiated.

The interface defines the contract between the client and server. The two applications can now be developed independently of each other. If you also stick to the old COM rules about interfaces (that interfaces should never be changed) you will not have any versioning problems.

Soapsuds

We can also use the Soapsuds utility to get the metadata from an assembly if an HTTP channel and the SOAP formatter are used. Soapsuds can convert assemblies to XML Schemas, XML Schemas to wrapper classes, and also works in the other directions.

The following command converts the type Hello from the assembly RemoteHello to the assembly HelloWrapper where a transparent proxy is generated that calls the remote object:

  soapsuds -types:Wrox.ProCSharp.Remoting.Hello,RemoteHello  -oa:HelloWrapper.dll 

With Soapsuds we can also get the type information directly from a running server if the HTTP channel and the SOAP formatter are used:

  soapsuds -url:http://localhost:6792/hello/hi?wsdl -oa:HelloWrapper.dll  

In the client we can now reference the Soapsuds -generated assembly instead of the original one. Some of the options are listed in this table:

Option

Description

-url

Retrieve schema from the specified URL

-proxyurl

If a proxy server is required to access the server, specify the proxy with this option

-types

Specify a type and assembly to read the schema information from it

-is

Input schema file

-ia

Input assembly file

-os

Output schema file

-oa

Output assembly file

Generating a WSDL Document with .NET Remoting

In the WSDL protocol used by ASP.NET Web Services. WSDL is also supported by .NET Remoting when an HTTP channel and the SOAP formatter are used. This can be tested easily by using a browser to access a remote object. Adding ?wsdl to the URI of the remote object returns a WSDL document, as you can see in the screenshot below, which shows the output from accessing our remote server we created earlier.

.NET Remoting uses the RPC style of WSDL documents, unlike ASP.NET Web Services, which uses the Document style by default.

click to expand

We can access the WSDL document across the network only if we use a well-known remote object type. For client-activated objects the URI is created dynamically. With client activated objects we can use the assembly with the -ia option of soapsuds to get the metadata.

Tracking Services

For debugging and troubleshooting applications using .NET, remoting tracking services can be used. The System.Runtime.Remoting.Services.TrackingService class provides a tracking service to get information about when marshaling and unmarshaling occur, when remote objects are called and disconnected, and so on:

  • With the TrackingServices utility class we can register and unregister a handler that implements ITrackingHandler .

  • The ITrackingHandler interface is called when an event happens on a remote object, or a proxy. We can implement three methods in the handler: MarshaledObject() , UnmarshaledObject() , and DisconnectedObject() .

To see tracking services in action in both the client and the server, we create a new class library, TrackingHandler . The TrackingHandler class implements the ITrackingHandler interface. In the methods we receive two arguments: the object itself and an ObjRef . With the ObjRef we can get information about the URI, the channel, and the envoy sinks. We can also attach new sinks to add a contributor to all the called methods. In our example we're writing the URI and information about the channel to the console:

   using System;     using System.Runtime.Remoting;     using System.Runtime.Remoting.Services;     namespace Wrox.ProCSharp.Remoting     {     public class TrackingHandler : ITrackingHandler     {     public TrackingHandler()     {     }     public void MarshaledObject(object obj, ObjRef or)     {     Console.WriteLine("--- Marshaled Object " +     obj.GetType() + " ---");     Console.WriteLine("Object URI: " + or.URI);     object[] channelData = or.ChannelInfo.ChannelData;     foreach (object data in channelData)     {     ChannelDataStore dataStore = data as ChannelDataStore;     if (dataStore != null)     {     foreach (string uri in dataStore.ChannelUris)     {     Console.WriteLine("Channel URI: " + uri);     }     }     }     Console.WriteLine("---------");     Console.WriteLine();     }     public void UnmarshaledObject(object obj, ObjRef or)     {     Console.WriteLine("Unmarshal");     }     public void DisconnectedObject(object obj)     {     Console.WriteLine("Disconnect");     }     }     }   

The server program is changed to register the TrackingHandler . Just two lines need to be added to register the handler:

   using System.Runtime.Remoting.Services;   //...         public static void Main(string[] args)         {   TrackingServices.RegisterTrackingHandler(new TrackingHandler());   TcpChannel channel = new TcpChannel(8086);            //... 

When starting the server, a first instance is created during registration of the well-known type and we get the following output. MarshaledObject() gets called and displays the type of the object to marshal - Wrox.ProCSharp.Remoting.Hello . With the object URI we see a GUID that's used internally in the remoting runtime to distinguish different instances and the URI we specified. With the channel URI the configuration of the channel can be verified . In this case the hostname is CNagel :

click to expand

Asynchronous Remoting

If server methods take a while to complete and the client needs to do some different work at the same time, it isn't necessary to start a separate thread to do the remote call. By doing an asynchronous call, the method starts but returns immediately to the client. Asynchronous calls can be made on a remote object as they are made on a local object with the help of a delegate.

To make an asynchronous method, we create a delegate, GreetingDelegate , with the same argument and return value as the Greeting() method of the remote object. With the delegate keyword a new class GreetingDelegate that derives from MulticastDelegate is created. You can verify this by using ildasm and checking the assembly. The argument of the constructor of this delegate class is a reference to the Greeting() method. We start the Greeting() call using the BeginInvoke() method of the delegate class. The second argument of BeginInvoke() is an AsyncCallback instance that defines the method HelloClient.Callback() , which is called when the remote method is finished. In the Callback() method the remote call is finished using EndInvoke() :

 using System; using System.Runtime.Remoting; namespace Wrox.ProCSharp.Remoting   {     public class HelloClient     {     private delegate String GreetingDelegate(String name);     private static string greeting;   [STAThread]       public static void Main(string[] args)   {   RemotingConfiguration.Configure("HelloClient.exe.config");          Hello obj = new Hello();          if (obj == null)          {             Console.WriteLine("could not locate server");             return 0;          }   // synchronous version     // string greeting = obj.Greeting("Christian");     // asynchronous version     GreetingDelegate d = new GreetingDelegate(obj.Greeting);     IAsyncResult ar = d.BeginInvoke("Christian", null, null);     // do some work and then wait     ar.AsyncWaitHandle.WaitOne();     if (ar.IsCompleted)     {     greeting = d.EndInvoke(ar);     }         Console.WriteLine(greeting);     }     }     }   

You can read more about delegates and events in Chapter 4.

OneWay Attribute

A method that has a void return and only input parameters can be marked with the OneWay attribute. The OneWay attribute makes a method automatically asynchronous, not matter how the client calls it. Adding the method TakeAWhile() to our remote object class RemoteHello creates a fire-and-forget method. If the client calls it by the proxy, the proxy immediately returns to the client. On the server, the method finishes some time later:

   [OneWay]     public void TakeAWhile(int ms)     {     Console.WriteLine("TakeAWhile started");     System.Threading.Thread.Sleep(ms);     Console.WriteLine("TakeAWhile finished");     }   

Remoting and Events

With .NET Remoting not only can the client call methods on the remote object across the network, but the server can also call methods in the client. For this, a mechanism that we already know from the basic language features is used: delegates and events .

In principle, the architecture is simple. The server has a remotable object that the client can call, and the client has a remotable object that the server can call:

  • The remote object in the server must declare an external function (a delegate) with the signature of the method that the client will implement in a handler

  • The arguments that are passed with the handler function to the client must be marshalable, so all the data sent to the client must be serializable

  • The remote object must also declare an instance of the delegate function modified with the event keyword; the client will use this to register a handler

  • The client must create a sink object with a handler method that has the same signature as the delegate defined, and it has to register the sink object with the event in the remote object

To help explain this, let's take a look at an example. To see all the parts of event handling with .NET Remoting we will create five classes; Server , Client , RemoteObject , EventSink , and StatusEventArgs .

The Server class is a remoting server such as the one we already know. The Server class will create a channel based on information from a configuration file and register the remote object that's implemented in the RemoteObject class in the remoting runtime. The remote object declares the arguments of a delegate and fires events in the registered handler functions. The argument that's passed to the handler function is of type StatusEventArgs . The class StatusEventArgs must be serializable so it can be marshaled to the client.

The Client class represents the client application. This class creates an instance of the EventSink class and registers the StatusHandler() method of this class as a handler for the delegate in the remote object. EventSink must be remotable like the RemoteObject class, because this class will also be called across the network:

click to expand

Remote Object

The remote object class is implemented in the file RemoteObject.cs . The remote object class must be derived from MarshalByRefObject , as we already know from our previous examples. To make it possible that the client can register an event handler that can be called from within the remote object, we have to declare an external function with the delegate keyword. We declare the delegate StatusEvent() with two arguments: the sender (so the client knows about the object that fired the event) and a variable of type StatusEventArgs . Into the argument class we can put all the additional information we want to send to the client.

The method that will be implemented in the client has some strict requirements. It may have only input parameters; return types, ref , and out parameters are not allowed; and the argument types must be either [Serializable] , or remotable (derived from MarshalByRefObject ). These requirements are fulfilled by the parameters we define with our StatusEvent delegate:

   public delegate void StatusEvent(object sender, StatusEventArgs e);     public class RemoteObject : MarshalByRefObject     {   

Within the RemoteObject class we declare an instance of the delegate function, Status , modified with the event keyword. The client must add an event handler to the Status event to receive status information from the remote object:

 public class RemoteObject : MarshalByRefObject {   public RemoteObject()     {     Console.WriteLine("RemoteObject constructor called");     }     public event StatusEvent Status;   

In the LongWorking() method we're checking if a event handler is registered before the event is fired with Status(this, e) . To verify that the event is fired asynchronously, we fire an event at the start of the method before doing the Thread.Sleep() , and after the sleep:

   public void LongWorking(int ms)     {     Console.WriteLine("RemoteObject: LongWorking() Started");     StatusEventArgs e = new StatusEventArgs(     "Message for Client: LongWorking() Started");     // fire event     if (Status != null)     {     Console.WriteLine("RemoteObject: Firing Starting Event");     Status(this, e);     }     System.Threading.Thread.Sleep(ms);     e.Message = "Message for Client: LongWorking() Ending";     // fire ending event     if (Status != null)     {     Console.WriteLine("RemoteObject: Firing Ending Event");     Status(this, e);     }     Console.WriteLine("RemoteObject: LongWorking() Ending");     }   

Event Arguments

As you've seen in the RemoteObject class, the class StatusEventArgs is used as argument for the delegate. With the [Serializable] attribute an instance of this class can be transferred from the server to the client. We are using a simple property of type string to send a message to the client:

   [Serializable]     public class StatusEventArgs     {     public StatusEventArgs(string m)     {     message = m;     }     public string Message     {     get     {     return message;     }     set     {     message = value;     }     }     private string message;     }   

Server

The server is implemented within a console application. We are only waiting for a user to end the server after reading the configuration file, and setting up the channel and the remote object:

 using System; using System.Runtime.Remoting; namespace Wrox.ProCSharp.Remoting {    class Server    {       [STAThread]       static void Main(string[] args)       {          RemotingConfiguration.Configure("Server.exe.config");          Console.WriteLine("Hit a key to exit");          Console.ReadLine();       }    } } 
Server Configuration File

The server configuration file, Server.exe.config , is also created as we've already discussed. There is just one important point: because the client first registers the event handler and then calls the remote method, the remote object must keep state for the client. It isn't possible to use SingleCall objects with events, so the RemoteObject class is configured as a client-activated type:

 <configuration>    <system.runtime.remoting>       <application name="CallbackSample">   <service>     <activated type="Wrox.ProCSharp.Remoting.RemoteObject,     RemoteObject" />     </service>     <channels>     <channel ref="http" port="6791" />     </channels>   </application>    </system.runtime.remoting> </configuration> 

Event Sink

The event sink implements the handler StatusHandler() that's defined with the delegate. As previously noted when we declared the delegate, the method can have only input parameters, and only a void return. These are exactly the requirements for [OneWay] methods as we've seen earlier with Asynchronous Remoting. StatusHandler() will be called asynchronously. The EventSink class must also inherit from the class MarshalByRefObject to make it remotable because it will be called remotely, from the server:

   using System;     using System.Runtime.Remoting.Messaging;     namespace Wrox.ProCSharp.Remoting     {     public class EventSink : MarshalByRefObject     {     public EventSink()     {     }     [OneWay]     public void StatusHandler(object sender, StatusEventArgs e)     {     Console.WriteLine("EventSink: Event occurred: " + e.Message);     }     }     }   

Client

The client reads the client configuration file with the RemotingConfiguration class: this is no different from the clients we've seen so far. The client creates an instance of the remotable sink class EventSink locally. The method that should be called from the remote object on the server is passed to the remote object:

 using System; using System.Runtime.Remoting; namespace Wrox.ProCSharp.Remoting {    class Client    {       static void Main(string[] args)       {          RemotingConfiguration.Configure("Client.exe.config"); 

The differences start here. We have to create an instance of the remotable sink class EventSink locally. Since this class will not be configured with the < client > element, it's instantiated locally. Next, the remote object class RemoteObject is instantiated. This class is configured in the < client > element, so it's instantiated on the remote server:

   EventSink sink = new EventSink();     RemoteObject obj = new RemoteObject();   

Now can we register the handler method of the EventSink object in the remote object. StatusEvent is the name of the delegate that was defined in the server. The StatusHandler() method has the same arguments as are defined in the StatusEvent .

By calling the LongWorking() method, the server will call back into the method StatusHandler() at the beginning and end of the method:

   // register client sink in server - subscribe to event     obj.Status += new StatusEvent(sink.StatusHandler);     obj.LongWorking(5000);   

Now we are no longer interested in receiving events from the server and unsubscribe from the event. The next time we call LongWorking() no events will be received:

   // unsubscribe from event     obj.Status -= new StatusEvent(sink.StatusHandler);     obj.LongWorking(5000);   Console.WriteLine("Hit to exit");          Console.ReadLine();       }    } } 
Client Configuration File

The configuration file for the client, client.exe.config , is nearly the same configuration file for client-activated objects that we've already seen. The difference can be found in defining a port number for the channel. Since the server must reach the client with a known port, we have to define the port number for the channel as an attribute of the < channel > element. It isn't necessary to define a < service > section for our EventSink class, because this class will be instantiated from the client with the new operator locally. The server does not access this object by its name; instead it will receive a marshaled reference to the instance:

 <configuration>    <system.runtime.remoting>       <application name="Client">   <client url="http://localhost:6791/CallbackSample">     <activated type="Wrox.ProCSharp.Remoting.RemoteObject,     RemoteObject" />     </client>     <channels>     <channel ref="http" port="777" />     </channels>   </application>    </system.runtime.remoting> </configuration> 

Running Programs

We see the resulting output on the server. The constructor of the remote object is called once because we have a client-activated object. Next, we see the call to LongWorking() has started and we are firing the events to the client. The next start of the LongWorking() method doesn't fire events, because the client has already unregistered its interest in the event:

click to expand

In the client output we can see that the events made it across the network:

click to expand

Call Contexts

Client-activated objects can hold state for a specific client. With client-activated objects, we need resources on the server. With server-activated SingleCall objects, a new instance is created for every instance call, and no resources are held on the server; these objects can't hold state for a client. For state management we can keep state on the client side; details of the state of that object are sent with every method call to the server. We don't have to change all method signatures to include an additional parameter that passes the state to the server, because we can use call contexts .

A call context flows with a logical thread and is passed with every method call. A logical thread is started from the calling thread and flows through all method calls that are started from the calling thread, passing through different contexts, different application domains, and different processes.

We can assign data to the call context using CallContext.SetData() . The class of the object that's used as data for the SetData() method must implement the interface ILogicalThreadAffinative . We can get this data again in the same logical thread (but possibly a different physical thread) using CallContext.GetData() .

For the data of the call context I'm creating a new C# Class Library with the newly created class CallContextData . This class will be used to pass some data from the client to the server with every method call. The class that's passed with the call context must implement the System.Runtime.Remoting.Messaging.ILogicalThreadAffinative interface. This interface doesn't have a method; it's just a markup for the runtime to define that instances of this class should flow with a logical thread. The CallContextData class must also be marked with the Serializable attribute so it can be transferred through the channel:

   using System;     using System.Runtime.Remoting.Messaging;     namespace Wrox.ProCSharp.Remoting     {     [Serializable]     public class CallContextData : ILogicalThreadAffinative     {     public CallContextData()     {     }     public string Data     {     get     {     return data;     }     set     {     data = value;     }     }     protected string data;     }     }   

In our Hello class, the Greeting() method is changed so that we access the call context. For the use of the CallContextData class we have to reference the previously created assembly CallContextData in the file CallContextData.dll . To work with the CallContext class, the namespace System.Runtime.Remoting.Messaging must be opened. Because the context works similar to a browser-based cookie, where the client automatically sends data to the web server, I'm giving the name cookie to the variable that holds the data that is sent from the client to the server:

 public string Greeting(string name)       {          Console.WriteLine("Greeting started");   CallContextData cookie =     (CallContextData)CallContext.GetData("mycookie");     if (cookie != null)     {     Console.WriteLine("Cookie: " + cookie.Data);     }   Console.WriteLine("Greeting finished");          return "Hello, " + name;       } 

In the client code we pass the call context information by creating an instance of CallContextData , and declare that the values referenced by the cookie variable should be sent to the server by calling CallContext.SetData() . Now every time we call the Greeting() method in the for loop, the context data is automatically passed to the server.

   CallContextData cookie = new CallContextData();     cookie.Data = "information for the server";     CallContext.SetData("mycookie", cookie);   for (int i=0; i < 5; i++)          {             Console.WriteLine(obj.Greeting("Christian"));          } 

Such a call context can be used to send information about the user, the name of the client system, or simply a unique identifier that's used on the server side to get some state information from a database.

  


Professional C#. 2nd Edition
Performance Consulting: A Practical Guide for HR and Learning Professionals
ISBN: 1576754359
EAN: 2147483647
Year: 2002
Pages: 244

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