Microsoft .NET Remoting: A Technical Overview

 

Piet Obermeyer and Jonathan Hawkins
Microsoft Corporation

Summary: This article provides a technical overview of the Microsoft .NET remoting framework. It includes examples using a TCP channel or an HTTP channel. (15 printed pages)

Note This article includes updated Beta 2 code.

Contents

Introduction Remote Objects Proxy Objects Channels Activation Object Lifetime with Leasing Conclusion Appendix A: Remoting Sample Using a TCP Channel

Introduction

Microsoft® .NET remoting provides a framework that allows objects to interact with one another across application domains. The framework provides a number of services, including activation and lifetime support, as well as communication channels responsible for transporting messages to and from remote applications. Formatters are used for encoding and decoding the messages before they are transported by the channel. Applications can use binary encoding where performance is critical, or XML encoding where interoperability with other remoting frameworks is essential. All XML encoding uses the SOAP protocol in transporting messages from one application domain to the other. Remoting was designed with security in mind, and a number of hooks are provided that allow channel sinks to gain access to the messages and serialized stream before the stream is transported over the channel.

Managing the lifetime of remote objects without support from the underlying framework is often cumbersome. .NET remoting provides a number of activation models to choose from. These models fall into two categories:

  • Client-activated objects
  • Server-activated objects

Client-activated objects are under the control of a lease-based lifetime manager that ensures that the object is garbage collected when its lease expires. In the case of server-activated objects, developers have a choice of selecting either a "single call" or "singleton" model. The lifetime of singletons are also controlled by lease-based lifetime.

Remote Objects

One of the main objectives of any remoting framework is to provide the necessary infrastructure that hides the complexities of calling methods on remote objects and returning results. Any object outside the application domain of the caller should be considered remote, even if the objects are executing on the same machine. Inside the application domain, all objects are passed by reference while primitive data types are passed by value. Since local object references are only valid inside the application domain where they are created, they cannot be passed to or returned from remote method calls in that form. All local objects that have to cross the application domain boundary have to be passed by value and should be marked with the [serializable] custom attribute, or they have to implement the ISerializable interface. When the object is passed as a parameter, the framework serializes the object and transports it to the destination application domain, where the object will be reconstructed. Local objects that cannot be serialized cannot be passed to a different application domain and are therefore nonremotable.

Any object can be changed into a remote object by deriving it from MarshalByRefObject. When a client activates a remote object, it receives a proxy to the remote object. All operations on this proxy are appropriately indirected to enable the remoting infrastructure to intercept and forward the calls appropriately. This indirection does have some impact on performance, but the JIT compiler and execution engine (EE) have been optimized to prevent unnecessary performance penalties when the proxy and remote object reside in the same application domain. In cases where the proxy and remote objects are in different application domains, all method call parameters on the stack are converted into messages and transported to the remote application domain, where the messages are turned back into a stack frame and the method call is invoked. The same procedure is used for returning results from the method call.

Proxy Objects

Proxy objects are created when a client activates a remote object. The proxy object acts as a representative of the remote object and ensures that all calls made on the proxy are forwarded to the correct remote object instance. In order to understand exactly how proxy objects work, we need to examine them in more detail. When a client activates a remote object, the framework creates a local instance of the class TransparentProxy that contains a list of all classes, as well as interface methods of the remote object. Since the TransparentProxy class is registered with the CLR when it gets created, all method calls on the proxy are intercepted by the runtime. Here the call is examined to determine if it is a valid method of the remote object and if an instance of the remote object resides in the same application domain as the proxy. If this is true, a simple method call is routed to the actual object. If the object is in a different application domain, the call parameters on the stack are packaged into an IMessage object and forwarded to a RealProxy class by calling its Invoke method. This class (or rather an internal implementation of it) is responsible for forwarding messages to the remote object. Both the TransparentProxy and RealProxy classes are created under the covers when a remote object is activated, but only the TransparentProxy is returned to the client.

In order to gain a better understanding of these proxy objects, we need to take a detour and briefly mention ObjRef. A detailed description of ObjRef is provided in the Activation section. The following scenario describes briefly how ObjRef and the two proxy classes are related. It is important to note that this is a very broad description of the process; some variations exist, depending on whether objects are client or server activated, and if they are singleton or single-call objects.

  • A remote object is registered in an application domain on a remote machine. The object is marshaled to produce an ObjRef. The ObjRef contains all the information required to locate and access the remote object from anywhere on the network. This information includes the strong name of the class, the class's hierarchy (its parents), the names of all the interfaces the class implements, the object URI, and details of all available channels that have been registered. The remoting framework uses the object URI to retrieve the ObjRef instance created for the remote object when it receives a request for that object.
  • A client activates a remote object by calling new or one of the Activator functions like CreateInstance. In the case of server-activated objects, the TransparentProxy for the remote object is produced in the client application domain and returned to the client, no remote calls are made at all. The remote object is only activated when the client calls a method on the remote object. This scenario will obviously not work for client-activated objects, since the client expects the framework to activate the object when asked to do so. When a client calls one of the activation methods, an activation proxy is created on the client and a remote call is initiated to a remote activator on the server using the URL and object URI as the endpoint. The remote activator activates the object, and an ObjRef is streamed to the client, where it is unmarshaled to produce a TransparentProxy that is returned to the client.
  • During unmarshaling, the ObjRef is parsed to extract the method information of the remote object and both the TransparentProxy and RealProxy objects are created. The content of the parsed ObjRef is added to the internal tables of the TransparentProxy before the latter is registered with the CLR.

The TransparentProxy is an internal class that cannot be replaced or extended. On the other hand, the RealProxy and ObjRef classes are public and can be extended and customized when necessary. The RealProxy class is an ideal candidate for performing load balancing for example, since it handles all function calls on a remote object. When Invoke is called, a class derived from RealProxy can obtain load information about servers on the network and route the call to an appropriate server. Simply request a MessageSink for the required ObjectURI from the Channel and call SyncProcessMessage or AsyncProcessMessage to forward the call to the required remote object. When the call returns, the RealProxy automatically handles the return parameter.

Here's a code snippet that shows how to use a derived RealProxy class.

   MyRealProxy proxy = new MyRealProxy(typeof(Foo));
   Foo obj = (Foo)proxy.GetTransparentProxy();
   int result = obj.CallSomeMethod();

The TransparentProxy obtained above can be forwarded to another application domain. When the second client attempts to call a method on the proxy, the remoting framework will attempt to create an instance of MyRealProxy, and if the assembly is available, all calls will be routed through this instance. If the assembly is not available, calls will be routed through the default remoting RealProxy.

An ObjRef can easily be customized by providing replacements for default ObjRef properties TypeInfo, EnvoyInfo, and ChannelInfo. The following code shows how this can be done.

public class ObjRef {
  public virtual IRemotingTypeInfo TypeInfo 
  {
    get { return typeInfo;}
    set { typeInfo = value;}
  }

  public virtual IEnvoyInfo EnvoyInfo
  {
    get { return envoyInfo;}
    set { envoyInfo = value;}
  }

  public virtual IChannelInfo ChannelInfo 
  {
    get { return channelInfo;}
    set { channelInfo = value;}
  }
}

Channels

Channels are used to transport messages to and from remote objects. When a client calls a method on a remote object, the parameters, as well as other details related to the call, are transported through the channel to the remote object. Any results from the call are returned back to the client in the same way. A client can select any of the channels registered on the "server" to communicate with the remote object, thereby allowing developers the freedom to select the channels that best suit their needs. It is also possible to customize any existing channel or build new ones that use different communication protocols. Channel selection is subject to the following rules:

  • At least one channel must be registered with the remoting framework before a remote object can be called. Channels must be registered before objects are registered.
  • Channels are registered per application domain. There can be multiple application domains in a single process. When a process dies, all channels that it registers are automatically destroyed.
  • It is illegal to register the same channel that listens on the same port more than once. Even though channels are registered per application domain, different application domains on the same machine cannot register the same channel listening on the same port. You can register the same channel listening on two different ports.
  • Clients can communicate with a remote object using any registered channel. The remoting framework ensures that the remote object is connected to the right channel when a client attempts to connect to it. The client is responsible for calling RegisterChannel on the ChannelService class before attempting to communicate with a remote object.

All channels derive from IChannel and implement either IChannelReceiver or IchannelSender, depending on the purpose of the channel. Most channels implement both the receiver and sender interfaces to enable them to communicate in either direction. When a client calls a method on a proxy, the call is intercepted by the remoting framework and changed into a message that is forwarded to the RealProxy class (or rather, an instance of a class that implements RealProxy). The RealProxy forwards the message to the channel sink chain for processing.

This first sink in the chain is normally a formatter sink that serializes the message into a stream of bytes. The message is then passed from one channel sink to the next until it reaches the transport sink at the end of the chain. The transport sink is responsible for establishing a connection with the transport sink on the server side and sending the byte stream to the server. The transport sink on the server then forwards the byte stream through the sink chain on the server side until it reaches the formatter sink, at which point the message is deserialized from its point of dispatch to the remote object itself.

One confusing aspect of the remoting framework is the relationship between remote objects and channels. For example, how does a SingleCall remote object manage to listen for clients to connect to if the object is only activated when a call arrives?

Part of the magic relies on the fact that remote objects share channels. A remote object does not own a channel. Server applications that host remote objects have to register the channels they require as well as the objects they wish to expose with the remoting framework. When a channel is registered, it automatically starts listening for client requests at the specified port. When a remote object is registered, an ObjRef is created for the object and stored in a table. When a request comes in on a channel, the remoting framework examines the message to determine the target object and checks the table of object references to locate the reference in the table. If the object reference is found, the framework target object is retrieved from the table or activated when necessary, and then the framework forwards the call to the object. In the case of synchronous calls, the connection from the client is maintained for the duration of the message call. Since each client connection is handled in its own thread, a single channel can service multiple clients simultaneously.

Security is an important consideration when building business applications, and developers must be able to add security features like authorization or encryption to remote method calls in order to meet business requirements. To accommodate this need, channels can be customized to provide developers with control over the actual transport mechanism of messages both to and from the remote object.

HTTP Channel

The HTTP channel transports messages to and from remote objects using the SOAP protocol. All messages are passed through the SOAP formatter, where the message is changed into XML and serialized, and the required SOAP headers are added to the stream. It is also possible to configure the HTTP Channel to use the binary formatter. The resulting data stream is then transported to the target URI using the HTTP protocol.

TCP Channel

The TCP channel uses a binary formatter to serialize all messages to a binary stream and transport the stream to the target URI using the TCP protocol. It is also possible to configure the TCP channel to the SOAP formatter.

Activation

The remoting framework supports server and client activation of remote objects. Server activation is normally used when remote objects are not required to maintain any state between method calls. It is also used in cases where multiple clients call methods on the same object instance and the object maintains state between function calls. On the other hand, client-activated objects are instantiated from the client, and the client manages the lifetime of the remote object by using a lease-based system provided for that purpose.

All remote objects have to be registered with the remoting framework before clients can access them. Object registration is normally done by a hosting application that starts up, registers one or more channels with ChannelServices, registers one or more remote objects with RemotingConfiguration, and then waits until it is terminated. It is important to note that the registered channels and objects are only available while the process that registered them is alive. When the process quits, all channels and objects registered by this process are automatically removed from the remoting services where they were registered. The following four pieces of information are required when registering a remote object with the framework:

  1. The assembly name in which the class is contained.
  2. The Type name of the remote object.
  3. The object URI that clients will use to locate the object.
  4. The object mode required for server activation. This can be SingleCall or Singleton.

A remote object can be registered by calling RegisterWellKnownServiceType, passing the information above as parameters, or by storing the above information in a configuration file and then calling Configure, thus passing the name of the configuration file as a parameter. Either of these two functions can be used to register remote objects as they perform exactly the same function. The latter is more convenient to use since the contents of the configuration file can be altered without recompiling the host application. The following code snippet shows how to register the HelloService class as a SingleCall remote object.

RemotingConfiguration.RegisterWellKnownServiceType(
  Type.GetType("RemotingSamples.HelloServer,object"), 
  "SayHello", 
  WellKnownObjectMode.SingleCall);

Where RemotingSamples is the namespace, HelloServer is the name of the class and Object.dll is the name of the assembly. SayHello is the Object URI where our service will be exposed. The Object URI can be any text string for direct hosting, but requires a .rem or .soap extension if the service will be hosted in IIS. It is therefore advisable to these extensions for all remoting endpoints (URI's).

When the object is registered, the framework creates an object reference for this remote object and then extracts the required metadata about the object from the assembly. This information, together with the URI and assembly name, is then stored in the object reference that is filed in a remoting framework table used for tracking registered remote objects. It is important to note that the remote object itself is not instantiated by the registration process. This only happens when a client attempts to call a method on the object or activates the object from the client side.

Any client that knows the URI of this object can now obtain a proxy for this object by registering the channel it prefers with ChannelServices and activating the object by calling new, GetObject, or CreateInstance. The following code snippet shows an example of how this is done.

""      ChannelServices.RegisterChannel(new TcpChannel());
      HelloServer obj =  (HelloServer)Activator.GetObject(
        typeof(RemotingSamples.HelloServer), 
        "tcp://localhost:8085/SayHello");

Here "tcp://localhost:8085/SayHello" specifies that we wish to connect to the remote object at the SayHello endpoint using TCP on port 8085. The compiler obviously requires type information about the HelloServer class when this client code is compiled. This information can be provided in one of the following ways:

  • Provide a reference to the assembly where the HelloService class is stored.
  • Split the remote object into an implementation and interface class and use the interface as a reference when compiling the client.
  • Use the SOAPSUDS tool to extract the required metadata directly from the endpoint. This tool connects to the endpoint provided, extracts the metadata, and generates an assembly or source code that can then be used to compile the client.

GetObject or new can be used for server activation. It is important to note that the remote object is not instantiated when either of these calls is made. As a matter of fact, no network calls are generated at all. The framework obtains enough information from the metadata to create the proxy without connecting to the remote object at all. A network connection is only established when the client calls a method on the proxy. When the call arrives at the server, the framework extracts the URI from the message, examines the remoting framework tables to locate the reference for the object that matches the URI, and then instantiates the object if necessary, forwarding the method call to the object. If the object is registered as SingleCall, it is destroyed after the method call is completed. A new instance of the object is created for each method called. The only difference between GetObject and new is that the former allows you to specify a URL as a parameter, where the latter obtains the URL from the configuration.

CreateInstance or new can be used for client-activated objects. Both allow instantiating an object using constructors with parameters. An activation request is sent to the server when a client attempts to activate a client-activated object. The lifetime of client-activated objects is controlled by the leasing service provided by the remoting framework. Object leasing is described in the following section.

Object Lifetime with Leasing

Each application domain contains a lease manager that is responsible for administrating leases in its domain. All leases are examined periodically for expired lease times. If a lease has expired, one or more of the lease's sponsors are invoked where they are given the opportunity to renew the lease. If none of the sponsors decides to renew the lease, the lease manager removes the lease and the object is garbage collected. The lease manager maintains a lease list with leases sorted by remaining lease time. The leases with the shortest remaining time are stored at the top of the list.

Leases implement the ILease interface and store a collection of properties that determine which policies and methods to renew. Leases can be renewed on call. Each time a method is called on the remote object, the lease time is set to the maximum of the current LeaseTime plus the RenewOnCallTime. When the LeaseTime elapses, the sponsor is asked to renew the lease. Since we have to deal with unreliable networks from time to time, the situation might arise where the lease sponsor is unavailable, and to ensure that we don't leave zombie objects on a server each lease has a SponsorshipTimeout. This value specifies the amount of time to wait for a sponsor to reply before the lease is terminated. If the SponsorshipTimeout is null, the CurrentLeaseTime will be used to determine when the lease should expire. If the CurrentLeaseTime has a value of zero, the lease will not expire. Configuration or APIs can be used to the override the default values for InitialLeaseTime, SponsorshipTimeout, and RenewOnCallTime.

The lease manager maintains a list of the sponsors (they implement the ISponsor interface) stored in order of decreasing sponsorship time. When a sponsor is needed for renewing the lease's time, one or more sponsors from the top of the list is asked to renew the time. The top of the list represents the sponsor that previously requested the largest lease renewal time. If a sponsor doesn't respond in the SponsorshipTimeOut time span, it will be removed from the list. An object's lease can be obtained by calling GetLifetimeService, passing the object for which the lease is required as a parameter. This call is a static method of the RemotingServices class. If the object is local to the application domain, the parameter to this call is a local reference to the object, and the lease returned is a local reference to the lease. If the object is remote, the proxy is passed as a parameter, and a transparent proxy for the lease is returned to the caller.

Objects can provide their own leases and thereby control their own lifetime. They do this by overriding the InitializeLifetimeService method on MarshalByRefObject as follows:

public class Foo : MarshalByRefObject {
  public override Object InitializeLifetimeService()
  {
    ILease lease = (ILease)base.InitializeLifetimeService();
    if (lease.CurrentState == LeaseState.Initial)  {
      lease.InitialLeaseTime = TimeSpan.FromMinutes(1);
      lease.SponsorshipTimeout = TimeSpan.FromMinutes(2);
      lease.RenewOnCallTime = TimeSpan.FromSeconds(2);
    }
    return lease;
  }
}

The lease properties can only be changed when a lease is in the initial state. The implementation of InitializeLifetimeService normally calls the corresponding method of the base class to retrieve the existing lease for the remote object. If the object has never been marshaled before, the lease returned will be in its initial state and the lease properties can be set. Once the object has been marshaled, the lease goes from the initial to the active state and any attempt to initialize the lease properties will be ignored (an exception is thrown). InitializeLifetimeService is called when the remote object is activated. A list of sponsors for the lease can be supplied with the activation call and additional sponsors can be added at any time while the lease is active.

Lease times can be extended as follows:

  • A client can invoke the Renew method on the Lease class.
  • The lease can request a Renewal from a sponsor.
  • When a client invokes a method on the object, the lease is automatically renewed by the RenewOnCall value.

Once a lease has expired, its internal state changes from Active to Expired, no further calls to the sponsors are made, and the object will be garbage collected. Since it is often difficult for remote objects to perform a callback on a sponsor if the sponsor is deployed on the Web or behind a firewall, the sponsor does not have to be at the same location as the client. It can be on any part of the network that is reachable by the remote object.

Using leases to manage the lifetime of remote objects is an alternative approach to reference counting, which tends to be complex and inefficient over unreliable network connections. Although one could argue that the lifetime of a remote object is extended longer than required, the reduction in network traffic devoted to reference counting and pinging clients makes leasing a very attractive solution.

Conclusion

Providing a perfect remoting framework that meets the needs of the majority of business applications is certainly a difficult, if not impossible, endeavor. By providing a framework that can be extended and customized as required, Microsoft has taken a key step in the right direction.

Appendix A: Remoting Sample Using a TCP Channel

This appendix shows how to write a simple "Hello World" remote application. The client passes a String to the remote object that appends the words "Hi There" to the string and returns the result back to the client. In order to modify this sample to use HTTP rather than TCP, simply replace TCP with HTTP in the source files.

Save this code as server.cs:

using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;

namespace RemotingSamples {
  public class Sample {

    public static int Main(string [] args) {

      TcpChannel chan = new TcpChannel(8085);
      ChannelServices.RegisterChannel(chan);
      RemotingConfiguration.RegisterWellKnownServiceType
      (Type.GetType("RemotingSamples.HelloServer,object"), 
      "SayHello", WellKnownObjectMode.SingleCall);
      System.Console.WriteLine("Hit <enter> to exit...");
      System.Console.ReadLine();
      return 0;
    }
  }
}

Save this code as client.cs:

using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;

namespace RemotingSamples {
  public class Client
  {
    public static int Main(string [] args)
    {
      TcpChannel chan = new TcpChannel();
      ChannelServices.RegisterChannel(chan);
      HelloServer obj = 
   (HelloServer)Activator.GetObject(typeof(RemotingSamples.HelloServer)
   , "tcp://localhost:8085/SayHello");
      if (obj == null) 
      System.Console.WriteLine("Could not locate server");
      else Console.WriteLine(obj.HelloMethod("Caveman"));
      return 0;
    } 
  }
}

Save this code as object.cs:

using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;

namespace RemotingSamples {
  public class HelloServer : MarshalByRefObject {

    public HelloServer() {
      Console.WriteLine("HelloServer activated");
    }

    public String HelloMethod(String name) {
      Console.WriteLine("Hello.HelloMethod : {0}", name);
      return "Hi there " + name;
    }
  }
}

Here's the makefile:

all: object.dll server.exe client.exe

object.dll: share.cs
   csc /debug+ /target:library /out:object.dll object.cs

server.exe: server.cs
   csc /debug+ /r:object.dll /r:System.Runtime.Remoting.dll server.cs

client.exe: client.cs server.exe
   csc /debug+ /r:object.dll /r:server.exe 
   /r:System.Runtime.Remoting.dll client.cs