Remoting Architecture

Establishing communication between two objects from two different domains or contexts, either on the same machine or on two different machines, is a common programming task. Traditionally, this requires in-depth knowledge of transport protocols, communication APIs, security mechanisms, and so on. The .NET Framework, however, makes it easy to develop such distributed applications by providing a number of services, utility classes, and tools. Figure 6.7 illustrates the process of general remoting.

Figure 6.7. Remoting model.


Under .NET remoting, a client simply creates an instance of the server class. The remoting layer creates a proxy object and returns it to the client. When the client makes a method call on the proxy, the remoting layer (in the client domain) intercepts the call, packs the call information into a message, and sends it over a communication channel to the server domain. The remoting layer in the server domain picks up this message, unpacks it, and invokes the appropriate method on the real object.

Channels

Channels transport messages between the remoting boundaries. A channel can either listen to an endpoint for inbound messages, send outbound messages, or both. The architecture allows you to plug in a wide range of protocols.

The .NET Framework provides channel classes to deal with some frequently used transport protocols such as TCP and HTTP. The TcpChannel class (namespace System.Runtime.Remoting.Channels.Tcp) transports the stream to the destination using TCP. By default, it uses a binary formatter (see Chapter 5) to serialize a message into a binary stream. The HttpChannel class (namespace System.Runtime.Remoting.Channels.Http) transports the data stream over HTTP. By default, it serializes messages using the SOAP–XML formatter. All the required SOAP headers are added to the stream before the data is transported.

It is possible to configure the TCP channel to use the SOAP formatter or the HTTP channel to use the binary formatter. The architecture is open enough to support even third-party formatters.

Channel classes such as TcpChannel and HttpChannel implement the channel logic for the client side as well as for the server side. The .NET Framework also provides the classes TcpServerChannel and HttpServerChannel, which implement only the server-side channel logic. Likewise, the classes TcpClientChannel and HttpClientChannel implement only the client-side channel logic.

At this point, it is worth understanding the activation model of the .NET remoting architecture. .NET classifies remote objects as either server-activated objects or client-activated objects, depending on who controls the lifetime of the object. Let's look at the server-activated objects first.

Server-Activated Objects

A server-activated object is an object with a lifetime that is directly controlled by the server. In this mode, the server application publishes the information on a type and assigns it a name. Although not necessary, the name is generally human-readable. Clients can use this name to look up the remote object. For this reason, a server-activated object is also referred to as a well-known object (WKO). The well-known name is referred to as the Uniform Resource Identifier (URI).

Consider the following code excerpt, which defines a class HelloUser that we wish to expose as a server-activated type:

// Project SingleCallObjects/Greeting

public class HelloUser : MarshalByRefObject  {
     public String GetGreeting(String user) {
       String retVal = String.Format(
         "Hello {0} from {1}:{2}",
         user,
         AppDomain.CurrentDomain.FriendlyName,
         this.GetHashCode());
       Console.WriteLine(retVal); // debugging aid
       return retVal;
     }
}

In this code, method GetGreeting takes as input a user name and returns a formatted string containing a greeting, the user name, the friendly name of the domain executing the method, and the hash code of the instance of HelloUser. The hashcode can serve as a unique identifier for the instance.

Note that class HelloUser is inherited from MarshalByRefObject. Without this, our discussion about distributed client–server computing would not make much sense.

Compile this code into a library assembly named Greeting.dll. Let's now expose HelloUser as a server-activated type, as shown in the following code excerpt:

// Project SingleCallObjects/MyHost

class MyApp {
     public static void Main() {
       ChannelServices.RegisterChannel(
         new TcpServerChannel(8085));
       RemotingConfiguration.RegisterWellKnownServiceType(
         Type.GetType("HelloUser, Greeting"),
         "GetHello",
         WellKnownObjectMode.SingleCall);

       // Keep the server alive
       Console.WriteLine("Press any key to quit server");
       Console.Read();
     }
}

To expose a WKO for remoting, a server application has to go through the following general steps:

1.
Register one or more server-side channels for communication.

2.
Register the identity of the remote types.

3.
Provide a mechanism to keep the server application alive.

First, the server application has to register one or more server-side channels with the remoting layer. These are the channels on which remoting will intercept calls for the WKO. The framework provides a class, ChannelServices (namespace System.Runtime.Remoting.Channels), that can be used to perform this and many other remoting-related operations. Method RegisterChannel on this class can be used to register a channel. In our example, we registered the TCP channel at port 8085.

A channel that is registered is global only within the context of the registering AppDomain. Each AppDomain is required to register the channel exclusively; that is, if the AppDomain intends to supply objects for remoting.

Also note that a port number cannot be shared between two AppDomains on the same machine. If you have a client and a server application that are running on the same machine, make sure that they use different port numbers.

Registering just the channels is not enough. All remote types also have to be registered with the .NET remoting framework before clients can access them. The framework provides a class, RemotingConfiguration (namespace System.Runtime.Remoting), for this purpose. In our code, we called a static method RegisterWellKnownServiceType on this class to register type HelloUser and gave it the URI GetHello. The last parameter to RegisterWellKnowServiceType dictates the mode of the server-activated object and is discussed shortly.

Obtaining Type Information

Recall from Chapter 3 that under .NET, a type can be represented by a display name. The syntax for the display name is:

Namespace.TypeName <,assembly name>

To obtain type information on type HelloUser from assembly Greeting.dll, one can call a static method, System.Type.GetType, passing it the display name HelloUser, Greeting as a parameter. This method returns an instance of class System.Type, a class that encapsulates all the information about a type.

Note that under C#, the type information can also be obtained using the typeof keyword. However, to use this keyword, the assembly containing the type has to be referenced during the compilation.


Given the fact that the type HelloUser has been published as GetHello on TCP port 8085, clients can access an instance of HelloUser using the lookup string tcp://machinename:8085/GetHello, where machinename is the IP name of the machine. This lookup string is referred to as the Uniform Resource Locator (URL) of the remote type. The syntax of the URL is dictated by the channel in use and can be found in the SDK documentation.

The final couple of lines of the preceding code implement a logic to keep the server alive until you press any key on the keyboard.

Compile this code as an assembly, MyHost.exe.

You may be wondering why we broke the server-side logic into two different assemblies, Greeting.dll and MyHost.exe. From a deployment perspective, doesn't it seem logical to create just one assembly with all the necessary code? If you examine the server registration code in MyHost.cs, you will see that the logic is so generic that it can be used to host more than one type. Thus, you can have your types spread over many assemblies but still host them using one generic hosting executable.

There is yet another reason for this separation. It is also possible to publish the types from Greeting.dll in other hosts, such as ASP.NET, eliminating the need for writing a separate host application. Later, we will see how this can be done.

Let's now look at how the client can activate and use the remote object. The following is the relevant code excerpt:

// Project SingleCallObjects/MyClient

class MyApp {
     public static void Main() {
       ChannelServices.RegisterChannel(new TcpClientChannel());

       HelloUser user = (HelloUser) Activator.GetObject(
         typeof(HelloUser),
         "tcp://localhost:8085/GetHello");

       String greeting = user.GetGreeting("Jay");
       Console.WriteLine("Return value: {0}", greeting);

       greeting = user.GetGreeting("Jay");
       Console.WriteLine("Return value: {0}", greeting);
     }
}

Before activating a remote object, a client has to register a client-side channel that it wishes to use. This is done by calling the familiar ChannelServices.RegisterChannel method.

It is not necessary for a client to register a channel explicitly. The default installation of the runtime sets up a machine-wide configuration that makes a TCP channel or an HTTP channel registered as necessary when activating the remote object. For details, look into the configuration/system.run-time.remoting/application/channels section in the global configuration file (Machine.Config).

Once a client channel is registered, the remote object is activated by calling a static method, Activator.GetObject. The first parameter to the method specifies the type of object that the client is interested in and the second parameter specifies the URL of the remote type.

When GetObject is called, a proxy object is created and returned to the client. This proxy object internally holds necessary information for communication, such as the type of channel used, the port number of the channel, the name of the remote machine, and so on. However, the client can treat the proxy object as though it is a direct reference to the remote object.

It is interesting to note that even though the runtime creates a proxy object on the call to GetObject, it does not create the corresponding remote object on the server, at least not yet. The remote object is created only when the first method call is made on the proxy object.

Compile the client code as MyClient.exe and execute it. Be sure to run MyHost.exe first. For our sample, both programs need to run on the same machine (machine name localhost is a standard DNS name representing the local machine).

Here is the output from our client program:

Return value: Hello Jay from MyHost.exe:58
Return value: Hello Jay from MyHost.exe:61

The client application calls GetGreeting twice on the proxy object and displays the string returned from each call. The domain name returned is MyHost.exe, confirming that the code was actually executed in the server application.

Also note that the hash code of the remote object returned is different for each method call. This implies that two different instances of the remote type were created even though only one proxy object was activated. How is this possible?

Single-Call and Singleton Objects

Under .NET, a server-activated object can be published in two possible modes, single-call and singleton. Each mode indirectly lets the server application control the lifetime of the remote object. A single-call object mode implies that the remote object is created on each method call and torn down after the call returns. A singleton object mode implies that the remote object is created just once on the first method call and is reused on subsequent method calls.

The mode of the object is defined at the time of registering the object by calling RegisterWellKnownServiceType. The last parameter to this method can be either WellKnownObjectMode.SingleCall for single-call mode or WellKnownObjectMode.Singleton for singleton mode.

Our earlier program sample registered the type in the single-call mode, so you saw two different instances of the remote type. If you edit MyHost.cs and change the object mode to singleton, as follows, then you will see that the hash code returned is the same for both the method calls made by the client:

// Project SingletonObjects/MyHost

   ...
   RemotingConfiguration.RegisterWellKnownServiceType(
     Type.GetType("HelloUser, Greeting"),
     "GetHello",
     WellKnownObjectMode.Singleton);

At this point, it is worth understanding the usage implications of each of the object modes. For a single-call mode, as a new object is created on each method call, the client cannot use it to store instance-specific data between two method calls. Even for a singleton object, it is not a good idea to store instance-specific data between method calls. As the same object is shared among all the clients, there is no guarantee that the data set by one client will not be overridden by another client. Moreover, as we will see later, there is also a lifetime issue to deal with. Server-activated objects, single-call or singleton, are generally designed to perform atomic operations; the client sends as method parameters any data that is needed by the server and gets back the result in a single method call.

If instance-specific data should be shared between method calls, then the server must publish client-activated types instead.

Client-Activated Objects

Client-activated objects (CAOs) are objects with a lifetime that is controlled by the client, just as they would be if the object were local to the client.

A server publishes a client-activated type using a static method, RemotingConfiguration.RegisterActivatedServiceType, as shown in the following code excerpt:

// Project ClientActivated/MyHost

class MyApp {
     public static void Main() {
       ChannelServices.RegisterChannel(
         new TcpServerChannel(8085));
       RemotingConfiguration.RegisterActivatedServiceType(
         Type.GetType("HelloUser, Greeting"));

       // Keep the server alive
       Console.WriteLine("Press any key to quit server");
       Console.Read();
     }
}

This code publishes our earlier defined class HelloUser (in assembly Greeting.dll) as a client-activated type. This class is available at TCP port 8085, which can be represented in URL form as tcp://<machinename>:8085.

A client instantiates a client-activated type using Activator.CreateInstance, a method we saw earlier. However, the URL information for the type has to be made available to CreateInstance. This is done by way of a context attribute called UrlAttribute. Here is our client code:

// Project ClientActivated/MyClient

class MyApp {
     public static void Main() {
       ChannelServices.RegisterChannel(new TcpClientChannel());

       HelloUser user = (HelloUser) Activator.CreateInstance(
         typeof(HelloUser),
         null,
         new object[] {
           new  UrlAttribute("tcp://localhost:8085")
         });

       String greeting = user.GetGreeting("Jay");
       Console.WriteLine("Return value: {0}", greeting);

       greeting = user.GetGreeting("Jay");
       Console.WriteLine("Return value: {0}", greeting);
     }
}

When the client tries to create an instance of a client-activated type, a remote object is created on the server and a proxy is returned to the client. Contrast this to a server-activated object, where the remote object is not created until a method call is made on the proxy. The returned proxy represents a specific remote object. If two new remote objects are created, two different proxy instances are returned to the client. The client can use the returned proxy as if it were a local object. The client can even store instance-specific data between two method calls.

It is important to note that the remote object may get destroyed even before the client releases the proxy object. This seems contrary to the intuition that as long as the client keeps a reference to the proxy object alive, the corresponding remote object should stay alive. This is because of the way .NET remoting manages the remote object's lifetime. We cover this later when we discuss lifetime leases.

Creating Proxy Classes

In Chapter 4, we learned that the common language runtime requires access to the metadata for the managed code. This holds true even if a client application is accessing a remote object; the metadata for the remote object must be available locally and should be accessible by the common language runtime. For example, MyClient.exe does not execute properly if it cannot access Greeting.dll, the assembly that stores the type information for the HelloUser class.

There are cases in which it is not acceptable to install the server assemblies on a client's machine. Perhaps the server does not wish to expose the implementation details, or perhaps it is not practical to keep a copy of the server assemblies on each client.

No matter what the reason is, there is no real need for a client to be able to access the implementation of remote classes. All that the client really needs is local access to the metadata of the referenced classes. This is where the Soapsuds tool (soapsuds.exe) comes into play.

The Soapsuds tool comes with the .NET Framework SDK. This tool can extract metadata information from an assembly and can save the information in various output formats such as XML schema, C# classes, and .NET assembly. Check the SDK documentation for more details. The following command line, for example, reads an assembly Greeting.dll as input and generates a C# source file Greeting.cs:

soapsuds -ia:Greeting -gc

The source code used in creating Greeting.dll can be found under Project Soapsuds. It essentially defines a class HelloUser as follows:

// Project Soapsuds/Greeting

public class HelloUser : MarshalByRefObject {
     public HelloUser() {...}

     public String GetGreeting(String user) {
       ...
     }
}

Here is the relevant portion of the code generated by the Soapsuds tool:

public class HelloUser :
     System.Runtime.Remoting.Services.RemotingClientProxy
{

     public Object RemotingReference {
          get{return(_tp);}
     }

     public String GetGreeting(String user) {
          return ((HelloUser) _tp).GetGreeting(user);
     }
}

The generated proxy class maintains all the public methods (with matching signatures) found in the original class. A client can use this proxy class to satisfy the metadata requirement of the runtime.

Note that the proxy class inherits from a class RemotingClientProxy. This class provides some frequently used properties when dealing with Soapsuds-generated proxies. For example, you can specify user authentication information such as the user name, password, and so on. If the client is behind a firewall, you can also specify the proxy server to use.

If inheriting from RemotingClientProxy is not desired, you can specify the -nowp command-line switch to Soapsuds.exe. In this case, the generated proxy class inherits from MarshalByRefObject.

Instead of generating the source code as the output, it is also possible to specify the Soapsuds tool to generate an assembly containing the proxy class. This is illustrated in the following command line:

soapsuds -ia:Greeting –oa:ClientGreeting.dll

Either way, the output of the Soapsuds tool can be consumed by the client code to generate the client-side executable, as illustrated here:

csc -t:exe -r:ClientGreeting.dll MyClient.cs

There is no need to reference the original assembly anymore when building the client code. As a result, the original assembly need not be installed on the client machine.

Using URLs

The Soapsuds tool can also take its input from an HTTP URL. This feature is useful when the server assembly is not available to the client.

Project Soapsuds hosts the server class using an HTTP channel. Here is the relevant code excerpt for the host:

// Project Soapsuds/MyHost

class MyApp {
public static void Main() {
     ChannelServices.RegisterChannel(
        new HttpServerChannel(8085));
     RemotingConfiguration.RegisterWellKnownServiceType(
       Type.GetType("HelloUser, Greeting"),
       "Greeting/GetHello.soap",
       WellKnownObjectMode.SingleCall);
       ...
     }
}

Note that the URI has the extension .soap. Although not necessary, the convention is to have an extension of either .soap or .rem. The global configuration file Machine.config defines HTTP remoting handlers to handle a request that ends with either of these two extensions. This is the default behavior. You can also add your own extensions, if desired. Look at <httpHandlers> tag for configuration details.

Given this URI and HTTP port number, a client can access the WKO using the URL http://localhost:8085/Greeting/GetHello.soap.

If a remote application is set up to use the HTTP channel, then the Soapsuds tool can be run against an HTTP URL as illustrated in the following command line:

soapsuds -url:http://localhost:8085/Greeting/GetHello.soap?wsdl
     -oa:MyGreeting.dll

This command reads the input from the specified URL and generates an assembly containing the proxy class.

Note that the URL has to be suffixed with ?wsdl. The remoting layer is set up to handle this suffix and generate the necessary WSDL information that Soapsuds can consume.

Remoting Configuration

Although servers can register an object by hard-coding the publishing information in the source, as we did in our earlier examples, it is also possible to put the type information in external XML-based files. This provides the flexibility of configuring registration parameters without the need to recompile the source code.

The syntax for the configuration file can be found in the SDK documentation under the topic “Remoting Settings Schema.”

Host Settings

Using the configuration file, one or more types to be hosted and one or more channels to be used can be specified. The following server-side configuration settings publish three types in three different remote object modes (Project RemotingConfig/MyHost):

<configuration>
  <system.runtime.remoting>
    <application name="MyRemotingHost">
      <service>
        <wellknown type="Foo,Greeting" mode="SingleCall"
        objectUri="GetFoo" />
        <wellknown type="Bar,Greeting" mode="Singleton"
        objectUri="GetBar" />
        <activated type="Baz,Greeting" />
      </service>
      <channels>
        <channel port="8085" ref="http" />
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>

This configuration implies:

  • Class Foo is exposed as a single-call object on the HTTP channel at port number 8085.

  • Class Bar is exposed as a singleton object on the HTTP channel at port number 8085.

  • Class Baz is exposed as a client-activated object on the HTTP channel at port number 8085.

The configuration setting file can be loaded using a static method RemotingConfiguration.Configure. The name of the configuration file must be passed as a parameter to this method, as shown in the following code excerpt:

// Project RemotingConfig/MyHost

class MyApp {
     public static void Main() {
       RemotingConfiguration.Configure("MyHost.cfg");

       // Keep the server alive
       Console.WriteLine("Press any key to quit server");
       Console.Read();
     }
}

It is easy to guess that the Configure method internally calls Channel-Services.RegisterChannel to register the channels and RegisterWell-KnownServiceType or RegisterActivatedServiceType to register the types to be remoted. However, placing the connection information in an external file makes it easy to administer the parameters, and this is the preferred way.

Client Settings

Remoting configuration settings are not just limited to the server side. Similar settings can be specified on the client side as well, although the syntax is slightly different. The following configuration settings make our just published server-side objects available to the client:

<configuration>
  <system.runtime.remoting>
    <application name="ThisIsMyClient">
      <client>
        <wellknown type="Foo,Greeting"
        url="http://localhost:8085/GetFoo" />
        <wellknown type="Bar,Greeting"
        url="http://localhost:8085/GetBar" />
      </client>
      <client url="http://localhost:8085/">
         <activated type="Baz,Greeting" />
      </client>
    </application>
  </system.runtime.remoting>
</configuration>

The configuration file needs to be loaded in the client's AppDomain using the RemotingConfiguration.Configure method. Once the configuration is loaded, the client can instantiate the remote objects using the new operator as illustrated in the following code:

// Project RemotingConfig/MyClientNew

class MyApp {
     public static void Main() {
       RemotingConfiguration.Configure("MyClientNew.cfg");

       // FOO
       Foo foo = new Foo();
       String greeting = foo.GetGreeting("Foo");
       Console.WriteLine("Return value: {0}", greeting);

       // BAR
       Bar bar = new Bar();
       greeting = bar.GetGreeting("Bar");
       Console.WriteLine("Return value: {0}", greeting);

       // BAZ
       Baz baz = new Baz();
       greeting = baz.GetGreeting("Baz");
       Console.WriteLine("Return value: {0}", greeting);
     }
}

Note that clients can also use Activator.CreateInstance or Activator.GetObject to override the configuration file settings for specified types.

Internally, RemotingConfiguration.Configure calls the method RegisterWellKnownClientType to associate a type with the URL of the remote server-activated object and method RegisterActivatedClientType, to associate a type with the URL of the remote client-activated type. You can use these methods directly if you do not wish to use a client-side configuration file. This is illustrated in the following code:

// Project RemotingConfig/MyClientUsingNew

public static void Main() {

     // Configuring object information programmatically

     // Foo - Single-call object
     RemotingConfiguration.RegisterWellKnownClientType(
       typeof(Foo),
       "http://localhost:8085/GetFoo");

     // Bar - Singleton object
     RemotingConfiguration.RegisterWellKnownClientType(
       typeof(Bar),
       "http://localhost:8085/GetBar");

     // Baz - Client-activated object
     RemotingConfiguration.RegisterActivatedClientType(
       typeof(Baz),
       "http://localhost:8085/");

       // Now instantiating classes using "new" operator
       Foo foo = new Foo(); // Foo
       ...
     }

As may be obvious, if a type is not associated with a remote URL, the new operator ends up creating a local object.

Note that RegisterWellKnownClientType and RegisterActivatedClientType register well-known types only for the AppDomain they are invoked from. Each AppDomain that wishes to use the new operator for remote objects should call these methods (or load the configuration file).

Hosting under ASP.NET

Earlier, I mentioned that a reason to separate the main business logic (Greeting.dll in our case) from the hosting logic (MyHost.exe) is that it is possible to publish classes under ASP.NET, thus eliminating the need for a hosting executable. Let's see how this can be done.

Under ASP.NET, classes are published using a configuration file. The file is named web.config and the configuration format is similar to one we saw earlier for the server-side settings. There are just a few differences:

  1. Under ASP.NET, classes are always published using the HTTP channel. Therefore, channel information should not be present in web.config.

  2. ASP.NET recognizes only two extensions for the URI, .rem or .soap. These two extensions are defined in the global configuration file (Machine.config). If need be, you can add your own extension by editing this file.

  3. The <application> element should not have any name attribute. When ASP.NET gets loaded for a specific Web application, it automatically sets the name of the application to the alias name of the IIS virtual directory in which it is being hosted.

Here is the modified version of our server-side configuration file that can be used under ASP.NET (Project WebApplication):

<configuration>
  <system.runtime.remoting>
     <application>
       <service>
         <wellknown type="MyCompany.Foo, Greeting"
         mode="SingleCall" objectUri="GetFoo.rem" />
         <wellknown type="MyCompany.Bar, Greeting"
         mode="Singleton" objectUri="GetBar.rem" />
         <activated type="MyCompany.Baz,Greeting" />
        </service>
     </application>
  </system.runtime.remoting>
</configuration>

To avoid any naming conflicts, I have defined the remote types under the namespace MyCompany. Using the company name as a namespace is a good coding guideline.

To host ASP.NET, you need to have IIS Web server running on your machine.

Here are the steps needed to host our assembly under ASP.NET:

1.
Create a new virtual directory under IIS. Give a suitable name for the alias and point the virtual directory (also called the virtual root or vroot) to the directory where web.config resides. This is an important step. ASP.NET expects web.config to be present in the vroot (although it is possible to customize the behavior for a subdirectory under vroot by defining another web.config in the subdirectory). For our experiment, let the alias name be Greeting and the directory be C:DotNetProgrammingCh06-DistributedComputingWebApplication.

2.
Create a subdirectory called bin under this directory and copy the required assemblies to this directory. In our case, Greeting.dll has already been built in C:DotNetProgrammingCh06-DistributedComputingWebApplicationin directory. The default PrivatePath configuration setting for ASP.NET is defined as bin. If you wish, you can customize this setting for your needs.

We are ready to go. A client can now access the published types at the URL http://<machinename>/<alias>/<objectUri>. In our experiment, for example, this translates to http://localhost/Greeting/GetFoo.rem for accessing class MyCompany.Foo, as shown in the following code excerpt:

// Project WebApplication/MyClient

class MyApp {
     public static void Main() {
       ChannelServices.RegisterChannel(new HttpChannel());

       String url = "http://localhost/Greeting/";
       // FOO
       Foo foo = (Foo) Activator.GetObject(
         typeof(Foo),
         url + "GetFoo.rem");
       String greeting = foo.GetGreeting("Foo");
       Console.WriteLine("Return value: {0}", greeting);
       ...
     }
}

Recall that our implementation of Foo.GetGreeting returns the greeting along with the name of the assembly and the hash code of the instance of Foo. Here is the partial output when the client program is executed:

Return value: Hello Foo from
     /LM/W3SVC/1/Root/Greeting-1-126572344064463520:181

Notice the strange assembly name returned for Greeting.dll. This is because ASP.NET uses a mechanism called shadow copy. Using this mechanism, ASP.NET makes a “shadow” copy of an assembly to be used, which is then locked and loaded. As the original assembly is not locked, it gives a chance for the developers or administrators to replace the assembly with a newer version even while the Web server is running. Furthermore, any changes to the original assembly can automatically be detected by ASP.NET, and, if needed, the assembly is shadow-copied once again. The newer incoming requests are automatically redirected to the newer copy of the assembly.

A final word on hosting .NET remoting applications either under ASP.NET or using an HTTP channel directly: Although the communication is based on HTTP and SOAP, the way the SOAP messages are formatted, not many non-.NET platforms are capable of interpreting the message. There have been some success stories with PocketSOAP (www.pocketsoap.com/weblog/soapInterop/base.html) and Apache (www.apache.org/~rubys/ApacheClientInterop.html). Later in the chapter, we look at Web services, a different mechanism for exposing types for remoting. The SOAP format used by Web services can be consumed by a wide range of platforms.

Lifetime Leases

For a CAO, it seems intuitive that as long as the client keeps a reference to the proxy the remote object stays alive and that when the client releases the last reference to the proxy, the remote object gets garbage collected. However, don't count on this.

In a distributed system, if a reference counting mechanism is used on an object, there needs to be a mechanism in place to adjust the object's reference count in case a client is not reachable anymore. Perhaps the client has terminated unexpectedly. Traditionally, this check is achieved by requiring the server to ping the clients periodically. Those who have worked with DCOM are familiar with this concept. Although this technique of periodically pinging the clients works well for a small number of clients per service, it doesn't scale well when there are a huge number of clients per service.

.NET uses a leased-based technique to manage the lifetime of MBR objects. An MBR object, either client-activated or singleton, can be associated with a lifetime lease. When the lease expires, the object is destroyed (garbage collected). This is the default behavior. As we will see shortly, there are various ways to extend the lease.

A lease implements the ILease interface (all lifetime-related interfaces and classes are defined in the namespace System.Runtime.Remoting .Lifetime). Here is its definition:

interface ILease {
     LeaseState CurrentState {get; } // Initial, Active, etc.
     TimeSpan InitialLeaseTime {get; set;}
     TimeSpan RenewOnCallTime {get; set; }
     TimeSpan CurrentLeaseTime {get; } // remaining lease time

     // Renew a lease
     TimeSpan Renew(TimeSpan renewalTime);

     // Sponsorship related
     void Register(ISponsor)
     void Register(ISponsor, TimeSpan)
     void Unregister(ISponsor)
     TimeSpan SponsorshipTimeout (get; set;}
}

Property ILease.InitialLeaseTime defines the time the object will be kept alive once it is remoted, irrespective of whether a client makes a method call or not. If a call comes in during this time, the remaining lease time (ILease.CurrentLeaseTime) is automatically adjusted to the greater of ILease.CurrentLeaseTime and ILease.RenewOnCallTime. The default value for the initial lease time is five minutes and for the renew-on-call time is two minutes. This means that once a client obtains a remote object, if the client doesn't make a call within five minutes, the remote object is destroyed, leaving the client with a dangling proxy (and we all know how much that hurts). If the unsuspecting client makes a call on such a proxy, the runtime throws an exception of type RemotingException.

Let's say the client makes a call on the remote object after four minutes. The runtime adjusts the remaining lease time (which should be one minute at this point) to two minutes (the default value for ILease.RenewOnCallTime). The client must now continue to make calls on the object at least once every two minutes, failing which the remote object is garbage collected.

The default lease parameters for an MBR object can be adjusted before the object leaves the context. A type can control its own lifetime policy by overriding the MarshalByRefObject.InitializeLifetimeService method, as shown in the following code excerpt:

// Project LifetimeLeases/Greeting

public class HelloUser : MarshalByRefObject {
     public override Object InitializeLifetimeService() {
       ILease lease =
         (ILease) base.InitializeLifetimeService();
       if (lease.CurrentState == LeaseState.Initial) {
         lease.InitialLeaseTime = TimeSpan.FromSeconds(30);
         lease.RenewOnCallTime = TimeSpan.FromSeconds(10);
       }
       return lease;
     }
     ...
}

The runtime calls InitializeLifetimeService when the object is being remoted. A typical override implementation obtains the lease by calling the base class InitializeLifetimeService, modifies the lease parameters, and returns it to the runtime. A null value can be returned, indicating that no lease can be created for the associated object, giving the object an infinite timeout.

Note that the runtime may call InitializeLifetimeService multiple times but the lease parameters can be adjusted only when the lease is in its initial state (LeaseState.Initial).

Each AppDomain contains a lease manager that is responsible for administering leases in its domain. The lease manager periodically checks all leases for expired leases. If the lease expires, the corresponding object is garbage collected. Note that a lease comes into play only when the object is remoted out of its AppDomain, not just out of its context.

There are two ways for the clients to extend the lease. The first way is to call ILease.Renew whenever appropriate and specify a new expiration time period. This is illustrated in the following code excerpt:

// Project LifetimeLeases/MyClient

public static void Main() {

     HelloUser user =
        (HelloUser) Activator.CreateInstance(...);

     ILease lease = (ILease) user.GetLifetimeService();
     lease.Renew(TimeSpan.FromMinutes(3));
     ...
}

The lease for an object can be obtained by calling MarshalByRefObject .GetLifetimeService. The preceding code obtains the lease and sets the new expiration time to three minutes.

The second technique for extending the lease is based on a sponsorship mechanism. A client can register an object, referred to as the sponsor object, with the lease manager. When the lease for an object expires, the lease manger first walks through all the sponsors for the object to see if any sponsor wishes to renew the lease. If no sponsor renews, only then is the object destroyed.

A sponsor object implements an interface, ISponsor. The interface definition is shown here:

interface ISponsor {
     TimeSpan Renewal(ILease lease)
}

When the lease expires for an object, the lease manager calls Renewal on each of the sponsors of the object. Each sponsor can return the new expiration time period, and the lease manager uses the maximum of the returned values as the new lease period.

A sponsor might not respond immediately, perhaps because of network problems such as latency, delays, and so on. To account for this, the lease manager waits for the ILease.SponsorshipTimeout time period for a response. If the response doesn't come within this period, the sponsor is removed from the list and the lease manager moves on to the next sponsor. The default value for SponsorshipTimeout is two minutes, but it can be set to a different value when initializing lifetime services.

A type can refuse to accept any sponsor by setting SponsorshipTimeout to a value TimeSpan.Zero.

A client can register a sponsor by calling ILease.Register, as shown in the following code excerpt:

// Project LifetimeLeases/MyClient

public class MySponsor : MarshalByRefObject, ISponsor {
     public override Object InitializeLifetimeService() {
       return null;
     }

     public TimeSpan Renewal(ILease lease) {
       Console.WriteLine("Renewal at {0}", DateTime.Now);
       return TimeSpan.FromSeconds(20);
     }
}
class MyApp {
     public static void Main() {
       ChannelServices.RegisterChannel(new TcpChannel(8086));

       HelloUser user =
          (HelloUser) Activator.CreateInstance(...);

       ILease lease = (ILease) user.GetLifetimeService();
       lease.Register(new MySponsor());
       ...
     }
}

Once a sponsor object is registered, a client can unregister it anytime by calling ILease.Unregister.

Note that the sponsor object has to be marked as remotable (either serializable or MBR). In addition, if the sponsor object is of the MBR type, then it also has to take care of its own lifetime.

In the preceding code, the client is using a TcpChannel instead of TcpClientChannel. Recall that TcpChannel implements client-side as well as server-side logic for the TCP channel. As the client-local object MySponsor is implemented as an MBR object and is being sent to a remote AppDomain, in a way, the client is now acting as a server. This explains the need for a server-side channel.

Technically, if TcpChannel is initialized using the default constructor, the implementation of TcpChannel sets up only the client channel and does not listen on any ports. Using a constructor that takes the port number as a parameter indicates that the server channel also has to be set up. Obviously, the port number to be used should not conflict with any other application.

Automatic Port Number Selection

If you are not sure of the port number to use, pass in the value 0 to the constructor of TcpChannel or HttpChannel. The remoting system automatically chooses an available port for you.

Note that this mechanism is meaningful for a client that may act as a server. When a client passes a local MBR object to a remote AppDomain, the object carries with it all the necessary information, including the port number, to connect back to the client. Therefore, the remote AppDomain need not explicitly know the port number of the client. Contrast this to a client trying to obtain an object by calling Activator.GetObject or Activator.CreateInstance. In this case, the client and the server have to agree on the port number to use.


..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.76.0