Click here to Skip to main content
15,881,861 members
Articles / Programming Languages / C#
Article

A Simple But Useful Example of .NET Remoting Part 2

Rate me:
Please Sign up or sign in to vote.
4.84/5 (15 votes)
16 Nov 2004CPOL23 min read 99.1K   1.4K   83   11
Introduces .NET remoting via a simple but potentially useful example.

Introduction

This article is a follow-up to my previous article entitled: "Simple but potentially useful example of .NET Remoting". In my earlier article, I expounded on basic codes necessary to startup a .NET remoting project. The example code of that article used Server Activated Remote Objects. In this article, we shift our attention to Client Activated Remote Objects which are different from and are often much more useful than server activated ones.

In this article, I will be discussing the following concepts:

  • Differences between Server Activated and Client Activated remote objects.
  • The need for serialization of objects passed to clients.
  • Object Marshalling, Parameterizing and Returning.
  • Remote object lifetimes.

Example codes are again provided, and the functionality remains the same as the first article, which is to demonstrate the launching of a process on a remote machine. I provided such an example because such programs are practical and are often used in real systems where remote control is required. I do hope that the reader will use my example codes to experiment further and emerge a truly professional and viable solution.

Differences Between Server Activated And Client Activated Remote Objects

The key difference between a Server Activated and a Client Activated Remote Object is that a Server Activated Object cannot hold state for a client. The exception is for "Singleton" Server Activated Objects. Let's explore this one step at a time.

A Server Activated Object comes in two varieties: "Single Call" and "Singleton" modes. Look up the WellKnownObjectMode enumeration in the MSDN documentation for more details.

Basically, using the SingleCall mode stipulates that a new instance of the Remote Object is created for every method call on the object. This will be so even if there are many methods exposed by the object. After each method call, the remote object is destroyed.

This being the case, it is not hard to imagine that such objects cannot hold any state for the client.

Using the Singleton mode stipulates that every incoming method call is serviced by the same object instance on the server side. This object is first instantiated on invocation of the first method call from a client. The implication is that all clients will share the one single object for all services.

Such objects can, in a strained sense, hold state for a client. Some sophisticated mechanism must be employed to ensure that each client holds some private piece of data which must be maintained by the Singleton Remote Object.

This is certainly possible but can be tedious to maintain. The other disadvantage is that this arrangement does not take into account the fact that there is no lifetime management system in place to ensure that should a client terminate suddenly, the server will be able to clean up resources set aside for the client. The .NET Remoting System provides remote object lifetime management support specially for Client Activated Remote Objects, which makes it much more attractive for usage.

In comes Client Activated Objects. Note that these objects are genuine Remote Objects. They are instantiated on the server and not on the client side. Once created, they are private discrete objects which survive normally until no further reference to it is required by the client after which the object is destroyed as per normal by the Garbage Collector (we will examine remote object lifetimes later in this article). Each created object is not shared among clients.

This makes Client Activated Objects truly hold state for clients. They can be used normally albeit they are created and maintained on a remote machine.

This presents some useful advantages to our example program (ProcessActivator). With a Client Activated approach, a ProcessActivator object can be used to run one single process on the remote server side. This same object can be re-used to perform actions on the same remote process (e.g., get all its loaded DLLs, get its memory usage status, etc.).

We will examine these when we study our source codes in the next few sections below.

The New Example Code

The New Interfaces

The basic functionality of our current example code remains the same as that in my first article: it starts up processes on a remote machine together with command line arguments. For the new code, I have created a new interface named "IProcessActivator_ClientActivated" in order to avoid possible confusion with the "IProcessActivator" interface of the first example code.

In the "IProcessActivator_ClientActivated" interface, I defined the Run() method which has exactly the same signature and specification as in the first example code, i.e., it starts up a process on the server machine. The name of the process is supplied in the first parameter to Run(), and the command line arguments are supplied in the second parameter.

As an enhancement to the example code, and as a way to show statefulness of a Client Activated Remote Object, I defined a new method named GetModules() which is specified to return an array of objects each of which contains information on a loaded module (DLL, exe, etc.) within the process started up by Run().

To accomplish this, the GetModules() method is specified to return an ArrayList containing objects which implement the "IModuleEntry" interface. Let's have a look at our "IProcessActivator_ClientActivated" and "IModuleEntry" interfaces. You can find these in the IProcessActivator_ClientActivated.cs source file of the IProcessActivator_ClientActivated.sln project in the IProcessActivator_ClientActivated folder.

C#
public interface IModuleEntry : ISerializable
{
  IntPtr BaseAddress
  {
    get;
  }

  IntPtr EntryPointAddress
  {
    get;
  }

  string FileName
  {
    get;
  }

  int ModuleMemorySize
  {
    get;
  }

  string ModuleName
  {
    get;
  }
}

public interface IProcessActivator_ClientActivated
{
  bool Run(string strProgramName, string strArgumentString);
  ArrayList GetModules();
}

As mentioned, the IProcessActivator_ClientActivated.GetModules() method returns an ArrayList which contains objects that implement the IModuleEntry interface. Each such object is meant to store information on a loaded module (DLL, exe) within the process started by the Run() method.

The IModuleEntry interface defines five read-only properties which are:

  • BaseAddress - this is the memory address where the module was loaded.
  • EntryPointAddress - this is the memory address for the function that runs when the system loads and runs the module (e.g.: DllMain()).
  • FileName - this is the full path to the module.
  • ModuleMemorySize - this is the amount of memory that is required to load the module.
  • ModuleName - this is the name of the process module.

These are just some of the example properties which can be defined by IModuleEntry. There can be many more depending on individual needs. Look up the MSDN documentation on the ProcessModule class for more details and ideas.

The Server Code

The Server Code has been enhanced to contain new implementation code as well as code required to host Client Activated Remote Objects. Let's start examining the code by first studying the ProcessActivator_ClientActivated class.

The ProcessActivator_ClientActivated class is derived from MarshalByRefObject as usual. It also implements the IProcessActivator_ClientAct interface.

C#
class ProcessActivator_ClientActivated : MarshalByRefObject, 
  IProcessActivator_ClientActivated.IProcessActivator_ClientActivated
{
  private Process m_process = new Process();

  public ProcessActivator_ClientActivated()
  {
    Console.WriteLine("ProcessActivator_ClientActivated constructor.");
  }
  ...
  ...
  ...
}

The ProcessActivator_ClientActivated class defines a private member data named m_process (type Process). The m_process member data is used to hold a new Process class instance.

To help in solidifying the fact that a separate object is created for every client, I have added console output strings in the constructor code to show when the constructor is invoked. This is purely for testing and demonstration purposes. The reader can delete it with no consequence.

The New Run() Method.

C#
public bool Run(string strProgramName, string strArgumentString)
{
  bool bRet = false;

  m_process.StartInfo = new ProcessStartInfo(strProgramName, 
                                         strArgumentString);

  bRet = m_process.Start();

  if (bRet)
  {
    Console.WriteLine("Program {0} started.", strProgramName);

    m_process.WaitForInputIdle();
    Console.WriteLine("Program {0} now in idle mode.", 
                                      strProgramName);
  }
  return bRet;
}

The Run() method has been enhanced from the previous version to use a non-static implementation of the Process.Start() method. The m_process member data will thereafter hold state on the newly created process. Now, one extra bit of code that I added in is the call to Process.WaitForInputIdle() method (highlighted in bold above).

Note the documentation for the WaitForInputIdle() method:

"Use WaitForInputIdle to force the processing of your application to wait until the message loop has returned to the idle state. When a process with a user interface is executing, its message loop executes every time a Windows message is sent to the process by the operating system. The process then returns to the message loop. A process is said to be in an idle state when it is waiting for messages inside of a message loop. This state is useful, for example, when your application needs to wait for a starting process to finish creating its main window before the application communicates with that window.

If a process does not have a message loop, WaitForInputIdle immediately returns false."

The call to WaitForInputIdle() is important to our ProcessActivator code because it forces Run() to wait until the starting process is stabilized and is no longer loading anymore modules (DLLs, OCXs etc.).

This makes any later calls to GetModules() succeed normally. If we had not called WaitForInputIdle(), there is a chance that an immediate call to GetModules() (after a call to Run()) will result in an exception (an example is IExplore.exe). Please take note of this small but significant fact. The reader is encouraged to temporarily comment out the WaitForInputIdle() call and to learn the negative effects.

The New GetModules() Method

C#
public ArrayList GetModules()
{
  ProcessModuleCollection pmc = m_process.Modules;
  ArrayList ar_ret = new ArrayList();
  int i = 0;

  Console.WriteLine("GetModules() started."); /* For testing purposes. */

  for (i = 0; i < pmc.Count; i++)
  {
    ar_ret.Add(
      (object)(new ModuleEntry(
                   pmc[i].BaseAddress, 
                   pmc[i].EntryPointAddress, 
                   pmc[i].FileName, 
                   pmc[i].ModuleMemorySize, 
                   pmc[i].ModuleName
              )
      ));
  }

  Console.WriteLine("GetModules() ended.");
  /* For testing purposes. */

  return ar_ret;
}

The GetModules() method's implementation uses the Modules property of m_process to return to us a ProcessModuleCollection collection object. This collection object contains ProcessModule objects. Each ProcessModule represents a .dll or .exe file that is loaded into a particular process. We use each ProcessModule object to return to us information about each module and to build up our array of IModuleEntry objects.

As can be seen from the GetModules() method, the information contained in each ProcessModule object is extracted and stored in a separate ModuleEntry object. Each ModuleEntry object is an implementation of the IModuleEntry interface.

Each of these ModuleEntry object is inserted into an ArrayList object which is then returned to the caller.

Now, you may be wondering: why take the trouble of defining the IModuleEntry interface, then developing a concrete implementation of this interface (the ModuleEntry class), and then creating an ArrayList of ModuleEntry objects to be returned to the GetModules() caller?

Why not simply make the GetModules() method return a ProcessModuleCollection collection object? This would certainly make life much simpler for GetModules(). The reason is that the ProcessModuleCollection collection class is not marked as serializable. More on this as we study the ModuleEntry class next.

The ModuleEntry Class

C#
[Serializable]
class ModuleEntry : IModuleEntry
{
  private IntPtr m_ipBaseAddress;
  private IntPtr m_ipEntryPointAddress;
  private string m_strFileName = "";
  private int m_iModuleMemorySize = 0;
  private string m_strModuleName = "";

  public ModuleEntry
    (
      IntPtr ipBaseAddress, 
      IntPtr ipEntryPointAddress, 
      string strFileName, 
      int iModuleMemorySize, 
      string strModuleName
    )
  {
    m_ipBaseAddress = ipBaseAddress;
    m_ipEntryPointAddress = ipEntryPointAddress;
    m_strFileName = strFileName;
    m_iModuleMemorySize = iModuleMemorySize;
    m_strModuleName = strModuleName;
  }

  private ModuleEntry
   (
     SerializationInfo info,
     StreamingContext context
   )
  {
    m_ipBaseAddress = (IntPtr)info.GetValue("m_ipBaseAddress", 
                                              typeof(IntPtr));
    m_ipEntryPointAddress = (IntPtr)info.GetValue("m_ipEntryPointAddress", 
                                              typeof(IntPtr));
    m_strFileName = (string)info.GetValue("m_strFileName", typeof(string));
    m_iModuleMemorySize = (int)info.GetValue("m_iModuleMemorySize", 
                                                              typeof(int));
    m_strModuleName = (string)info.GetValue("m_strModuleName", typeof(string));
  }

  ...
  ...
  ...
}

Recall the original specifications of the IModuleEntry interface:

C#
public interface IModuleEntry : ISerializable

This means than any implementation of IModuleEntry must also implement the ISerializableinterface. This is crucial because any object used as part of a remote method parameter or as a return value must be serializable.

Now, because the ModuleEntry class implements the ISerializable interface, the GetObjectData() method and the special private constructor (that takes a SerializationInfo and StreamingContext parameters) are defined.

I have tried to keep the ISerializable interface implementations as simple as possible to avoid overwhelming the beginner level readers with unnecessary concerns. Indeed, the IModuleEntry interface may even be defined without implementing the ISerializable interface. The ModuleEntry class must remain serializable, however, so simply applying the [Serializable] attribute to the ModuleEntry class will also do.

Much of the remainder of the ModuleEntry class is simple (even trivial) implementation of the "get" accessors of the IModuleEntry interface. The actual properties are implemented by the private member data of the class:

C#
private IntPtr m_ipBaseAddress;
private IntPtr m_ipEntryPointAddress;
private string m_strFileName = "";
private int m_iModuleMemorySize = 0;
private string m_strModuleName = "";

I have also created a convenient constructor for ModuleEntry that takes values for all five member data. This constructor will be used in the GetModules() method. It is a simple class meant to hold module data. No frills about it.

The Main() Function And The Remote Object Hosting Code

The Main() function of the server is described below after the code snippet:

C#
static void Main(string[] args)
{
  TcpServerChannel channel = new TcpServerChannel(9000);
  HttpServerChannel http_channel = new HttpServerChannel(8000);

  ChannelServices.RegisterChannel(channel);
  ChannelServices.RegisterChannel(http_channel);

  ActivatedServiceTypeEntry remObj = 
    new ActivatedServiceTypeEntry(typeof(ProcessActivator_ClientActivated));

  RemotingConfiguration.ApplicationName = "ProcessActivator_ClientActivated";
  RemotingConfiguration.RegisterActivatedServiceType(remObj);

  Console.WriteLine("Press [ENTER] to exit.");

  Console.ReadLine();
}

As was used in my first example code, I have defined two channels to be used to transfer any request for a ProcessActivator_ClientActivated object.

Note the way that a Client Activated Remote Object Server performs hosting of the object. Previously, in our Server Activated Remote Object hosting code, we created a WellKnownServiceTypeEntry object and then registered it via the RemotingConfiguration.RegisterWellKnownServiceType() method call.

In our new Client Activated Remote Object hosting code, we create a ActivatedServiceTypeEntry object, specifying in the constructor the type of the remote object, and then register it via the RemotingConfiguration.RegisterActivatedServiceType() method call. We also specify the URI of the Remote Object via the RemotingConfiguration.ApplicationName property. This URI is set to "ProcessActivator_ClientActivated" in our server.

Note also that the URI is not tied in with the type of the Remote Object in our construction of the ActivatedServiceTypeEntry object. This is unlike the Server Activated example code where the URI is specified along with the WellKnownServiceTypeEntry object:

C#
WellKnownServiceTypeEntry remObj = new WellKnownServiceTypeEntry
   (
   typeof(ProcessActivator),
   "ProcessActivator",
   WellKnownObjectMode.SingleCall
   );

Later, in the client code of the Server Activated Remote Object, when we perform the following:

C#
IProcessActivator.IProcessActivator process_activator = 
      (IProcessActivator.IProcessActivator)Activator.GetObject
   (
   typeof(IProcessActivator.IProcessActivator),
   "tcp://localhost:9000/ProcessActivator"
   );

the URI part of the URL parameter (i.e., ProcessActivator) binds the return value (as stored in process_activator) to the specific Remote Object whose URI is ProcessActivator.

We will examine the way clients connect to Client-Activated Remote Objects later in the section on Client Code.

The remainder of the server code remains the same as in the first example code. That is, we simply wait out until the user presses the ENTER key, after which the entire server application ends.

We shall examine the client code next.

The Client Code

The client code is presented below:

C#
class ProcessActivator_ClientActivated_Client
{
  static RealProxy m_real_proxy = null;
  static object m_transparent_proxy = null;
  static ILease m_lease = null;
  ...
  ...
  ...
  /* The main entry point for the application. */
  [STAThread]
  static void Main(string[] args)
  {
    try
    {
      UrlAttribute[] attr = { new 
        UrlAttribute(@"http://localhost:8000/ProcessActivator_ClientActivated") };
      ObjectHandle object_handle = null;
      IProcessActivator_ClientActivated.IProcessActivator_ClientActivated 
        process_activator = null;
      ArrayList process_module_collection = null;
      ...
      ...
      ...
      /* Create a client channel from which to receive the Remote Object. */
      TcpClientChannel tcp_channel = new TcpClientChannel();
      /* Register the channel. */
      ChannelServices.RegisterChannel(tcp_channel);
      /* During the following call to Activator.CreateInstance(), the constructor 
      of the class that implements the IProcessActivator_ClientActivated 
      interface will be invoked. */
      object_handle = Activator.CreateInstance
        (
          "ProcessActivator_ClientActivated",
          "ProcessActivator_ClientActivated.ProcessActivator_ClientActivated",
          attr
        );

      /* Unwrap the delivered object and cast it to the 
      IProcessActivator_ClientActivated interface.*/
      process_activator 
        = (IProcessActivator_ClientActivated.IProcessActivator_ClientActivated)
          (object_handle.Unwrap());
      /* We can now get hold of the ILease object within the newly created 
      Remote Object. */
      m_real_proxy = RemotingServices.GetRealProxy(process_activator);
      m_transparent_proxy = m_real_proxy.GetTransparentProxy();
      m_lease 
        = (ILease)(((MarshalByRefObject)m_transparent_proxy).GetLifetimeService());
      ...
      ...
      ... 

      /* Start up a process in the remote machine site. */
      process_activator.Run("notepad.exe", "");

      /* Sleep for 270 seconds (4 and a half minutes). */
      /* The timer will continue to monitor the Remote Object lease. */
      Thread.Sleep(270 * 1000);
      /* After the sleeping period, we attempt to call a remote method. */
      process_module_collection = process_activator.GetModules();
      /* Perform processing of the returned value from GetModules(). */
      for (int i = 0; i < process_module_collection.Count; i++)
      {
        IModuleEntry me = (IModuleEntry)process_module_collection[i];
        Console.WriteLine(me.FileName);
      }
      ...
      ...
      ...
    }
    catch(FileNotFoundException ex)
    {
      Console.WriteLine(ex.FileName);
    }
    catch(TargetInvocationException tie)
    {
      Console.WriteLine(tie.Message);
    }
    return;
  }
}

In the above code snippet, I have deliberately filtered out some codes for clarity. Although the client code does look a little more complicated compared with the client code of the server activated remote object (in my previous example), please note that half of the client source codes pertain to remote object lifetime management and a demonstration of this management, the other part pertains to actual remote object creation.

In a nutshell, for Client Activated Remote Object invocation, clients call the Activator.CreateInstance() method to create an instance of a remote object. This remote object instantiated via CreateInstance() will remain alive until something known as a lease time is expired (thereby causing garbage collection to occur and hence destruction of the remote object). We will be discussing the leasing mechanism later.

Take note that not all the overloaded Activator.CreateInstance() methods can be used to create remote objects. Most of them are for creating local ones. To create a remote object, we will need to use a version of CreateInstance() where Activation Attributes can be passed in.

Such Activation Attributes are best described by the MSDN documentation as: "an array of one or more attributes that can participate in activation".

Among the three versions of CreateInstance(), we will be using the simplest one which takes three parameters:

C#
public static ObjectHandle CreateInstance(
   string assemblyName,
   string typeName,
   object[] activationAttributes
);

We invoke this function in the following way:

C#
UrlAttribute[] attr = 
  { new UrlAttribute(@"http://localhost:8000/ProcessActivator_ClientActivated") };

object_handle = Activator.CreateInstance
(
  "ProcessActivator_ClientActivated",
  "ProcessActivator_ClientActivated.ProcessActivator_ClientActivated",
  attr
);

In our code, we use "ProcessActivator_ClientActivated" as the name of the assembly where the type named "ProcessActivator_ClientActivated.ProcessActivator_ClientActivated" is sought.

Note well that whatever assembly name we specified as the first parameter, the .NET framework must be able to locate it. Hence, because I have specified this parameter to be "ProcessActivator_ClientActivated", I have included "ProcessActivator_ClientActivated.exe" inside the same directory as the client executable "ProcessActivator_ClientActivated_Client.exe".

While it did not bother me that the actual typename of the remote object "ProcessActivator_ClientActivated.ProcessActivator_ClientActivated" must be specified in CreateInstance() (in fact, I believe this is rather necessary), it puzzled me greatly why the assembly of the remote object must be found by the .NET Framework in order for CreateInstance() to work. Please read the section "Some Comments On Activator.CreateInstance()" for a deeper analysis of this subject later.

The parameter "attr" is an array of UrlAttribute objects. This UrlAttribute object helps us to specify the channel and the object name of the Remote Object. Hence, it is no surprise that for the constructor for UrlAttribute, we specified:

http://localhost:8000/ProcessActivator_ClientActivated.

The HTTP protocol, the host address (localhost in our example code), and the port number (8000) being the channel used, and then name of the remote object being "ProcessActivator_ClientActivated".

The next thing to note about Client Activated Remote Object creation is the fact that an ObjectHandle is returned from the call to CreateInstance(). An ObjectHandle is best described by the MSDN documentation as ".. a remoted MarshalByRefObject that is tracked by the remoting lifetime service..."

The ObjectHandle object returned from the CreateInstance() method must be unwrapped in order to retrieve the contained actual remote object. Actually, the contained remote object is a still proxy of the real remote object created on the remote site.

After unwrapping, we will still need to cast the returned value (which is typed as an object) to the IProcessActivator_ClientActivated type:

C#
process_activator = 
  (IProcessActivator_ClientActivated.IProcessActivator_ClientActivated)
  (object_handle.Unwrap());

We can next call any of the exposed methods of the IProcessActivator_ClientActivated interface.

Some Comments On Activator.CreateInstance()

Recall the Activator.CreateInstance() method. I mentioned earlier on that I believe it is necessary that the actual type name of the remote object "ProcessActivator_ClientActivated.ProcessActivator_ClientActivated" must be specified as a parameter.

This is inline with the fact that the URI as specified in the server's call to RemotingConfiguration.ApplicationName and in the client's UrlAttribute passed to CreateInstance() is not tied in with the actual type of the remote object.

The same UrlAttribute can be used in another call to CreateInstance() with a different type name specified as the second parameter.

In my opinion, this design also goes in line with the design of object creation and interface implementation in C++ and COM.

In C++, an interface pointer is instantiated to a concrete implementation as follows:

IMyInterface* pIMyInterface = new CMyInterfaceImpl();

Note the reference to an actual implementation class name (CMyInterfaceImpl).

In COM, a similar style is used :

::CoCreateInstance(CLSID_MyClassID, NULL, CLSCTX_INPROC_SERVER , 
         IID_IMyInterface, (LPVOID *)&pIMyInterface);

The class ID of the COM object which implements the IMyInterface interface must be known to the client.

This style of indicating the name of the actual concrete class (or type) is continued in Activator.CreateInstance(). The fact that the assembly name itself must also be specified is merely indicative that the assembly name is to be used as part of the identifier of the actual type itself. Hence, the assembly name "ProcessActivator_ClientActivated" and the concrete type name "ProcessActivator_ClientActivated.ProcessActivator_ClientActivated" are all used together to identify the concrete .NET class to be remotely instantiated.

In C++, COM, and .NET Remoting, the actual implementation identifier (e.g., class name CMyInterfaceImpl, class ID CLSID_MyClassID, or the .NET type "ProcessActivator_ClientActivated.ProcessActivator_ClientActivated") need not be hard-coded, and can be read in from a configuration file (INI file or XML file, etc.).

This ensures the possibility of late binding and configurability.

However, it still troubled me greatly that the actual assembly itself must be found locally by the .NET Framework in order for CreateInstance() to work.

After some thoughts and discussing with fellow colleagues, I emerged some theories on this:

The fact that the name of the assembly is to be specified indicates one of the original intended usage of Remoting, i.e. that it is to be confined to closed LAN/intranet-based distributed systems.

Remoting objects are not intended for use by "the outside world". Only Web Services are exposed to the outside world. To use a remote object, you must have in your local client system a copy of the actual assembly which is used by the server.

I would really rather have it that the actual assembly be un-required, or that a path to a remotely located assembly be specifiable. But both are not possible as far as I know at this time.

What usage does the .NET Framework have on this assembly I really do not know at this time. If there are any readers out there who understand this usage, please share with us :-)

Object Marshalling, Parameterizing and Returning

Note that the parameter types of remote method calls are not limited to basic data types. Method return values are similarly not confined as can be seen by the fact that we can return an ArrayList of ModuleEntry objects.

In the context of .NET Remoting, we have to differentiate between two types of classes:

  • Classes which are Marshaled By Value - the instances of such classes are serialized and passed through the remoting channel. These classes must implement the ISerializable interface or be marked using the [Serializable] attribute. Note that objects instantiated from these classes are completely marshaled across the channel to the client. They are independent from the server from which they came. Marshal by value classes are also known as unbound classes because they do not contain any data that depend on the application domain from which they are marshaled.
  • Classes which are Marshaled by Reference - the instances of such classes have what is known as a Remote Identity. These objects are not passed across the wire, but instead, a proxy is returned to the client. Such classes must be derived from the MarshalByRefObject class. MarshalByRefObject instances are also known as application domain bound objects because they really only truly exist in the application domain in which they are created.

With the above definitions, I hope things have become clearer and we see where things fit in. In our example code, the ProcessActivator_ClientActivated class is derived from the MarshalByRefObject class, and hence is marshalled by reference to the client. The client receives a proxy to the actual object created in the server.

Our ArrayList of ModuleEntry objects are marshaled by value. They are first created in the server of course (during the GetModules() method). Thereafter, they are completely marshaled to the client. Actual ModuleEntry objects are re-created on the client side. This is why the ModuleEntry class implements the ISerializable interface.

Remote Object Lifetimes

No article on Client Activated Remote Objects is complete without a discussion on Remote Object Lifetime Management. Why is this topic important? Well, how does a client and a server know if the other side is no longer available?

If a server should die on a client, as soon as the client makes a method call on the remote object, an exception of type System.Runtime.Remoting.RemotingException is thrown. In this situation, the client should handle this exception and decide on the best course of action (e.g., re-create the object, etc.).

The situation can be more severe for a server. If a client should crash, the server usually has no way of detecting this (unless some predefined mechanism of mutual life detection, e.g., pinging, has been put in place). This could lead to the piling up of unused and unreleased resources.

For Remote Object Lifetime Management, the .NET Framework provides the Leasing Distributed Garbage Collector (LDGC). For every client activated remote object referenced outside the application domain in which it is created, something known as a lease is created. This lease has a lease time, and when it expires, the remote object gets disconnected and is garbage collected.

In the context of lifetime management, a few terms must be introduced:

  • LeaseTime - this refers to the length of time within which a remote object is alive. The default value is 300 seconds.
  • RenewOnCallTime - this is the time the lease is reset to once a remote method call is invoked (if the current lease time has a lower value than this). The default value is 120 seconds.
  • SponsorshipTimeout - this is the time in which the remoting infrastructure will search for an available sponsor. The default value is 120 seconds.
  • LeaseManagerPollTime - this defines the time interval between which the lease manager will perform a search for lease expired objects. The default is 10 seconds.

We are definitely concerned over lease times. Although it is possible to configure it, we cannot have a situation in which the remote object never expires its lease. This is bad design, and will open up the chance for the resource leakage issues discussed earlier.

If we need a remote object for longer than the default lease time, we cannot have a situation in which we simply let a remote object expire by itself and to re-create the object thereafter. If this is what you want, by all means let it happen in your project. However, I assume that the common requirement is to maintain the lifetime of remote objects until they are specifically no longer needed by the application.

Concerning lease renewal, there are three ways to accomplish it:

  • By implicit renewal - done when the client calls a remote method.
  • By explicit renewal - done via the ILease.Renew() method.
  • By sponsorship - the idea is for the client to create a sponsor object that extends the lease time automatically.

Of the three methods, implicit renewal is not suitable. We will run the risk of the remote object being lease expired when we call a remote method.

Sponsorship seemed like an attractive choice and it is, but it is also a little complicated to implement. .NET security features must be used. Sponsorship, with its complex association with security issues, is not attractive to us also because this article is meant for the beginner level developers.

The last and only choice for us is thus the explicit renewal method. Our client code uses the Renew() method to renew the lease of the remote object.

First and foremost, we must obtain the ILease interface of something known as the transparent proxy of the remote object. Without going into too much detail at this time, the following is how you would get hold of this ILease interface:

C#
m_real_proxy = RemotingServices.GetRealProxy(process_activator);
m_transparent_proxy = m_real_proxy.GetTransparentProxy();
m_lease 
  = (ILease)(((MarshalByRefObject)m_transparent_proxy).GetLifetimeService());

To facilitate the getting of the ILease object, we defined the following member data to the ProcessActivator_ClientActivated_Client class:

C#
static RealProxy m_real_proxy = null;
static object m_transparent_proxy = null;
static ILease m_lease = null;

The ILease object can be gotten by calling the GetLifetimeService() method of the transparent proxy of the remote object. I have not yet fully grasped Remoting Proxies, but I will be going deeper into it later on and will share with all my findings at that time.

Back to lease renewals, in order that we renew the remote object lease in a timely fashion, we will need to create a timer for our client application. This is why we defined the Timer object named "aTimer" and set the timeout to a suitable value (30 seconds):

C#
System.Timers.Timer aTimer = new System.Timers.Timer(30000);

We also set the timer event handler to our user-defined OnTimedEvent() function, and initially disabled the Timer object:

C#
aTimer.Elapsed += new ElapsedEventHandler(OnTimedEvent);
aTimer.AutoReset = true;
aTimer.Enabled = false;
/*Disable the timer first until m_lease has been properly initialized.*/

We need to disable the Timer object until we have gotten the ILease object (m_lease) appropriately by the call to GetLifetimeService(). After GetLifetimeService() has been successfully called, we enable the Timer again:

C#
/* Only now do we turn on the timer...*/
aTimer.Enabled = true;

Let's now examine our Timer event handler:

C#
/* When the timer event is raised, we perform a check on the current lease time
of the Remote Object. */
private static void OnTimedEvent(object source, ElapsedEventArgs e) 
{
  /* Perform something only when the ILease object is not null. */
  if (m_lease != null)
  {
    TimeSpan time_span_current_lease = m_lease.CurrentLeaseTime;
    double dbTime_current_lease = time_span_current_lease.TotalSeconds;
    LeaseState lease_state = m_lease.CurrentState;
    Console.WriteLine("Current Lease Time : {0}.", dbTime_current_lease);
    if ((lease_state == LeaseState.Active) && (dbTime_current_lease <= 60))
    {
      /* Once the lease time has fallen below 60 seconds,
      we seek to renew the lease. */
      TimeSpan time_span_renew = m_lease.Renew(TimeSpan.FromSeconds(300));
      double dbTimeRenew = time_span_renew.TotalSeconds;
      Console.WriteLine("Renewed Lease Time To : {0}.", dbTimeRenew);
    }
  }
}

Here, we first check to see if the ILease object (m_lease) is non-null. If so, it means that m_lease has been initialized properly. We then proceed to access the m_lease object's CurrentLeaseTime and CurrentState properties. We then check whether the lease's state is still in the active state and whether the lease time has currently been reduced to less than or equal to 60 seconds.

If the lease's state is active and the lease time has dropped down to less than or equal to 60 seconds, we perform a lease renewal (via the ILease.Renew() method).

In our example client code, I have attempted to show this lease renewal at work by causing the main thread to sleep for 4.5 minutes (270 seconds) after the Run() method has been invoked. During this time of sleep, the Timer object will continue to work, checking for the need for lease time renewal.

Before the ThreadSleep() function returns, our Timer callback function would have renewed the remote object lease time to 300 seconds again. Now, after Thread.Sleep() returns, I will call the remote object's GetModules() method and display all the loaded modules of "notepad.exe". The call to GetModules() succeeds without any lease time expiry.

I deliberately repeated the call to Thread.Sleep() (with the same timeout value of 4.5 minutes) and thereafter another call to GetModules(). As will be evident when you run the example code, the second call to GetModules() will succeed again.

In Conclusion

Once again, I really hope the reader has benefited from this article and its example codes. Client Activated Remote Objects are certainly more complex to create and use as compared with Server Activated ones. I do hope that my article has served well in explaining the internal intricacies, and that my example code will provide a good startup point for projects.

I certainly do want to touch on proxies in the near future and hope to write an article on this. I have not touched on configuration files yet and will also be looking into this.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Systems Engineer NEC
Singapore Singapore
Lim Bio Liong is a Specialist at a leading Software House in Singapore.

Bio has been in software development for over 10 years. He specialises in C/C++ programming and Windows software development.

Bio has also done device-driver development and enjoys low-level programming. Bio has recently picked up C# programming and has been researching in this area.

Comments and Discussions

 
GeneralVery neat article, but .. Pin
Mashadow2-Feb-09 11:02
Mashadow2-Feb-09 11:02 
QuestionIs it right Scenario for using Remoting? Pin
kazim bhai15-Aug-08 5:37
kazim bhai15-Aug-08 5:37 
GeneralMy Object reference times out... Pin
mmfranke30-Oct-07 11:14
mmfranke30-Oct-07 11:14 
GeneralRe: My Object reference times out... Pin
Paulo Zemek11-Nov-09 1:18
mvaPaulo Zemek11-Nov-09 1:18 
GeneralTechniqal question Pin
tino12903-May-06 10:27
tino12903-May-06 10:27 
GeneralI have a question Pin
host284-Jan-06 15:22
host284-Jan-06 15:22 
GeneralName space is not found Pin
manaf6-Jun-05 21:26
manaf6-Jun-05 21:26 
GeneralRe: Name space is not found Pin
3wishes7-Jun-05 19:26
3wishes7-Jun-05 19:26 
GeneralUnregister the channels Pin
mcmspark17-Feb-05 3:20
mcmspark17-Feb-05 3:20 
GeneralGetting the ILease Pin
John M. Dlugosz1-Feb-05 9:10
John M. Dlugosz1-Feb-05 9:10 
GeneralChannels Pin
Cleyton Messias31-Jan-05 9:39
Cleyton Messias31-Jan-05 9:39 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.