.NET Remoting Architecture

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

After we've seen a simple client and server in action, we need an overview of the .NET architecture before we can go into the details. Based on the previously created program we will look at the details of the architecture and will see mechanisms for extensibility.

In this section, we shall explore all the topics in this list:

  • The functionality of a channel and how a channel can be configured

  • Formatters and how they are used

  • The utility classes ChannelServices and RemotingConfiguration

  • Different ways to activate remote objects, and how stateless and stateful objects can be used with .NET Remoting

  • Functionality of Message Sinks

  • How to pass objects by value and by reference

  • Lifetime Management of stateful objects with .NET Remoting leasing mechanisms

Channels

A channel is used to communicate between a .NET client and a server. The .NET Framework ships with channel classes that communicate using TCP or HTTP. We can create custom channels for other protocols.

The HTTP channel is used by most web services. It uses the HTTP protocol for communication. Because firewalls usually have port 80 opened so that the clients can access web servers, our .NET Remoting web services can listen to port 80 so that they can be used by these clients easily.

It's also possible to use the TCP channel on the Internet, but here the firewalls must be configured so that clients can access a specified port that's used by the TCP channel. The TCP channel can be used to communicate more efficiently in an intranet compared to the HTTP channel.

When performing a method call on the remote object, the client channel object sends a message to the remote channel object.

Both the server and the client application must create a channel. This code shows how a TcpServerChannel can be created on the server side:

   using System.Runtime.Remoting.Channels.Tcp;     ...     TcpServerChannel channel = new TcpServerChannel(8086);   

The port on which the TCP socket is listening is specified in the constructor argument. The server channel must specify a well-known port, and the client must use this port when accessing the server. For creating a TcpClientChannel on the client, however, it isn't necessary to specify a well-known port. The default constructor of TcpClientChannel chooses an available port, which is passed to the server at connection-time so that the server can send data back to the client.

Creating a new channel instance immediately switches the socket to the listening state, which can be verified by typing netstat -a at the command line.

The HTTP channels can be used similarly to the TCP channels. We can specify the port where the server can create the listening socket.

A server can listen to multiple channels. Here we are creating both an HTTP and a TCP channel in the file HelloServer.cs :

 using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Tcp; using System.Runtime.Remoting.Channels.Http; namespace Wrox.ProCSharp.Remoting {    public class HelloServer    {       [STAThread]       public static void Main(string[] args)       {   TcpServerChannel tcpChannel = new TcpServerChannel(8086);     HttpServerChannel httpChannel = new HttpServerChannel(8085);     //...     // register the channel and remote object as we have done previously     }   

A channel class must implement the IChannel interface. The IChannel interface has these two properties:

  • ChannelName is a read-only property that returns the name of the channel. The name of the channel depends on the type - for example the HTTP channel is named HTTP .

  • ChannelPriority is a read-only property. More than one channel can be used for communication between a client and a server. The priority defines the order of the channel. On the client, the channel with the higher priority is chosen first to connect to the server. The bigger the priority value, the higher the priority. The default value is 1, but negative values are allowed to create lower priorities.

Additional interfaces are implemented depending on whether the channel is a client-channel or a server-channel. The server versions of the channels implement the IChannelReceiver interface, the client versions implement the IChannelSender interface.

The HttpChannel and TcpChannel classes can be used for both the client and the server. They implement IChannelSender and IChannelReceiver . These interfaces derive from IChannel .

The client-side IChannelSender has, in addition to IChannel , a single method called CreateMessageSink() , which returns an object that implements IMessageSink . The IMessageSink interface can be used for putting synchronous as well as asynchronous messages into the channel. With the server-side interface IChannelReceiver , the channel can be put into listening mode using StartListening() , and stopped again with StopListening() . We also have a ChannelData property to access the received data.

We can get information about the configuration of the channels using properties of the channel classes. For both channels, we have a ChannelName , a ChannelPriority , and a ChannelData property. The ChannelData property can be used to get information about the URIs that are stored in the ChannelDataStore class. With the HttpChannel there's also a Scheme property. The code below shows a helper method, ShowChannelProperties() , in our file HelloServer.cs that displays this information:

   protected static void ShowChannelProperties(IChannelReceiver channel)     {     Console.WriteLine("Name: " + channel.ChannelName);     Console.WriteLine("Priority: " + channel.ChannelPriority);     if (channel is HttpChannel)     {     HttpChannel httpChannel = channel as HttpChannel;     Console.WriteLine("Scheme: " + httpChannel.ChannelScheme);     }     ChannelDataStore data = (ChannelDataStore)channel.ChannelData;     foreach (string uri in data.ChannelUris)     {     Console.WriteLine("URI: " + uri);     }     Console.WriteLine();     }   

The method ShowChannelProperties() is called after creating the channels in our Main() method:

 TcpServerChannel tcpChannel = new TcpServerChannel(8086);   ShowChannelProperties(tcpChannel);   HttpServerChannel httpChannel = new HttpServerChannel(8085);   ShowChannelProperties(httpChannel);   

With our TCP and HTTP channels, we get this information:

click to expand

As we can see here, the default name for the TcpServerChannel is tcp , and the HTTP channel is called http . Both channels have a default priority of 1 and I've set the ports 8085 and 8086 in the constructors. The URI of the channels shows the protocol, hostname (in my case CNagel ), and port number.

Setting Channel Properties

We can set all the properties of a channel in a list using the constructor TcpServerChannel(IDictionary, IServerChannelSinkProvider) . The Hashtable class implements IDictionary , so I'm setting the Name , Priority , and Port property with help of this class.

In order to use the Hashtable class we have to declare the use of the System.Collections namespace. In addition to the IDictionary parameter we can pass an IServerChannelSinkProvider parameter. I pass a SoapServerFormatterSinkProvider instead of the BinaryServerFormatterSinkProvider , which is the default of the TcpServerChannel . The default implementation of the SoapServerFormatterSinkProvider class associates a SoapServerFormatterSink class with the channel that uses a SoapFormatter to convert the data for the transfer:

   IDictionary properties = new Hashtable();     properties["name"] = "TCP Channel with a SOAP Formatter";     properties["priority"] = "20";     properties["port"] = "8086";     SoapServerFormatterSinkProvider sinkProvider =     new SoapServerFormatterSinkProvider();     TcpServerChannel tcpChannel =     new TcpServerChannel(properties, sinkProvider);     ShowChannelProperties(tcpChannel);   

The new output we get from our server startup code shows the new properties of the TCP channel:

click to expand

Depending on the channel types, different properties can be specified. Both the TCP and the HTTP channel support the name and priority channel property that we used in our example. These channels also support other properties such as bindTo , which specifies an IP address for binding that can be used if the computer has multiple IP addresses configured. rejectRemoteRequests is supported by the TCP server channel to allow client connections only from the local computer.

Pluggability of a Channel

A custom channel can be created to send the messages using a transport protocol other than HTTP or TCP, or you can extend the existing channels:

  • The sending part must implement the IchannelSender interface. The most important part is the CreateMessageSink() method, which the client sends a URL, and with this a connection to the server can be instantiated . Here a message sink must be created, which is then used by the proxy to send messages to the channel.

  • The receiving part must implement the IchannelReceiver interface. We have to start the listening in the ChannelData get property. Then we can wait in a separate thread to receive data from the client. After unmarshaling the message, we can use ChannelServices.SyncDispatchMessage() to dispatch the message to the object.

Formatters

The .NET Framework delivers two formatter classes:

  • System.Runtime.Serialization.Formatters.Binary.BinaryFormatter

  • System.Runtime.Serialization.Formatters.Soap.SoapFormatter

Formatters are associated with channels through formatter sink objects and formatter sink providers.

Both of these formatter classes implement the interface System.Runtime.Remoting.Messaging.IRemotingFormatter , which defines the methods Serialize() and Deserialize () to transfer the data to and from the channel.

The formatter is also pluggable. When you're writing a custom formatter class, an instance must be associated with the channel you want to use. This is done using a formatter sink and a formatter sink provider. The formatter sink provider, for example, SoapServerFormatterSinkProvider , can be passed as an argument when creating a channel as we saw earlier. A formatter sink provider implements the interface IServerChannelSinkProvider for the server, and IClientChannelSinkProvider for the client. Both of these interfaces define a CreateSink() method where a formatter sink must be returned. The SoapServerFormatterSinkProvider returns an instance of the class SoapServerFormatterSink. On the client side, we have the SoapClientFormatterSink class that uses the SyncProcessMessage() and AsyncProcessMessage() methods of the SoapFormatter class to serialize the message. The SoapServerFormatterSink deserializes the message, again using the SoapFormatter .

All these sink and provider classes can be extended and replaced with custom implementations .

ChannelServices and RemotingConfiguration

The ChannelServices utility class is used to register channels into the .NET Remoting runtime. With this class we can also access all registered channels. This is extremely useful if configuration files are used to configure the channel, because here the channel is created implicitly, as we will see later.

A channel is registered using the static method ChannelServices.RegisterChannel() .

You can see here the server code to register our HTTP and TCP channels:

 TcpChannel tcpChannel = new TcpChannel(8086); HttpChannel httpChannel = new HttpChannel(8085);   ChannelServices.RegisterChannel(tcpChannel);     ChannelServices.RegisterChannel(httpChannel);   

The ChannelServices utility class can now be used to dispatch synchronous and asynchronous messages, and to unregister specific channels. The RegisteredChannels property returns an IChannel array of all the channels we registered. We can also use the GetChannel() method to get to a specific channel by its name. With the help of ChannelServices we could write a custom administration utility that manages our channels. Here is a small example that shows how the server channel can be stopped from listening for incoming requests :

   HttpServerChannel channel =(HttpServerChannel)ChannelServices.GetChannel("http");     channel.StopListening(null);   

The RemotingConfiguration class is another .NET Remoting utility class. On the server side it's used to register remote object types for server-activated objects, and to marshal remote objects to a marshaled object reference class ObjRef . ObjRef is a serializable representation of an object that's sent over the wire. On the client side, RemotingServices is used to unmarshal a remote object in order to create a proxy from the object reference.

Here is the server-side code to register a well-known remote object type to the RemotingServices :

   RemotingConfiguration.RegisterWellKnownServiceType(     typeof(Hello),                   // Type     "Hi",                            // URI     WellKnownObjectMode.SingleCall); // Mode   

The first argument of RegisterWellKnownServiceType() , Hello specifies the type of the remote object. The second argument, " Hi ", is the uniform resource identifier of the remote object that will be used from the client to access the remote object. The last argument is the mode of the remote object. The mode can be a value of the WellKnownObjectMode enumeration: SingleCall or Singleton .

  • SingleCall means that the object holds no state. With every call to the remote object a new instance is created. A SingleCall object is created from the server with the RemotingConfiguration.RegisterWellKnownServiceType() method, and a WellKnownObjectMode.SingleCall argument. This is very efficient on the server because it means that we don't need to hold any resources for maybe thousands of clients.

  • With a Singleton the object is shared for all clients of the server; typically, such object types can be used if you want to share some data between all clients. This shouldn't be a problem for read-only data, but with read-write data you have to be aware of locking issues and scalability. A Singleton object is created by the server with the RemotingConfiguration.RegisterWellKnownServiceType() method and a WellKnownObjectMode.Singleton argument. We have to pay attention to locking of resources held by the Singleton object; we have to make sure that data can't be corrupted when clients are accessing the Singleton concurrently, but we also have to check that the locking is done efficiently enough so that the required scalability is reached.

Server for Client Activated Objects

If a remote object should hold state for a specific client, we can use client-activated objects. In the next section we will look at how to call server-activated or client-activated objects on the client side. On the server side client-activated objects must be registered in a different way from server-activated objects.

Instead of calling RemotingConfiguration.RegisterWellKnownType() , we have to call RemotingConfiguration.RegisterActivatedServiceType() . With this method, only the type is specified, and not the URI. The reason for this is that for client-activated objects, the clients can instantiate different object types with the same URI. The URI for all client-activated objects must be defined using RemotingConfiguration.ApplicationName :

   RemotingConfiguration.ApplicationName = "HelloServer";     RemotingConfiguration.RegisterActivatedServiceType(typeof(Hello));   

Object Activation

Clients can use and create remote objects using the Activator class. We can get a proxy to a server-activated or well-known remote object using the GetObject() method. The CreateInstance() method returns a proxy to a client-activated remote object.

Instead of using the Activator class, the new operator can also be used to activate remote objects. To make this possible, the remote object must also be configured within the client using the RemotingConfiguration class.

Application URL

In all activation scenarios, we have to specify a URL to the remote object. This URL is the same one you'd use when browsing with a web browser. The first part specifies the protocol followed by the server name or IP address, the port number, and a URI that was specified when registering the remote object on the server in this form:

 protocol://server:port/URI 

We are continually using two URL examples in our code. We specify the protocols http and tcp , the server name is localhost , the port numbers are 8085 and 8086 , and the URI is Hi , as follows :

   http://localhost:8085/Hi     tcp://localhost:8086/Hi   

Activating Well-Known Objects

In our previous, simple client example we activated well-known objects. Now let us take a more detailed look at the activation sequence.

 using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Tcp; // ... TcpClientChannel channel = new TcpClientChannel(); ChannelServices.RegisterChannel(channel);   Hello obj = (Hello)Activator.GetObject(typeof(Hello),     "tcp://localhost:8086/Hi");   

GetObject() is a static method of System.Activator that calls RemotingServices.Connect() to return a proxy object to the remote object. The first argument specifies the type of the remote object. The proxy implements all public and protected methods and properties, so that the client can call these methods as it would on the real object. The second argument is the URL to the remote object. We are using the string tcp://localhost:8086/Hi . tcp is the protocol, localhost:8086 is the hostname and the port number, and finally Hi is the URI of the object that was specified using RemotingConfiguration.RegisterWellKnownServiceType() .

Instead of using Activator.GetObject() , we could also use RemotingServices.Connect() directly:

   Hello obj = (Hello)RemotingServices.Connect(typeof(Hello),     "tcp://localhost:8086/Hi");   

If you prefer to do a simple new to activate well-known remote objects, the remote object can be registered on the client using RemotingConfiguration.RegisterWellKnownClientType() . The arguments needed here are similar: the type of the remote object and the URI. new doesn't really create a new remote object, instead it returns a proxy similar to Activator.GetObject() . If the remote object is registered with a flag WellKnownObjectMode.SingleCall , the rule always stays the same - the remote object is created with every method call:

   RemotingConfiguration.RegisterWellKnownClientType(typeof(Hello),     "tcp://localhost:8086/Hi");     Hello obj = new Hello();   

Activating Client-Activated Objects

Remote objects can hold state for a client. Activator.CreateInstance() creates a client-activated remote object. Using the Activator.GetObject() method, the remote object is created on a method call, and is destroyed when the method is finished. The object doesn't hold state on the server. The situation is different with Activator.CreateInstance() . With the static CreateInstance() method an activation sequence is started to create the remote object. This object lives until the lease time is expired and a garbage collection occurs. We will talk about the leasing mechanism later in this chapter.

Some of the overloaded Activator.CreateInstance() methods can only be used to create local objects. To create remote objects a method is needed where it's possible to pass activation attributes. One of these overloaded methods is used in our example. This method accepts two string parameters, the first is the name of the assembly and the second is the type, and a third parameter, an array of objects. The channel and the object name are specified in the object array with the help of a UrlAttribute . To use the UrlAttribute class the namespace System.Runtime.Remoting.Activation must be specified:

   object[] attrs = {new UrlAttribute("tcp://localhost:8086/Hello") };     ObjectHandle handle = Activator.CreateInstance(     "RemoteHello", "Wrox.ProCSharp.Remoting.Hello", attrs);     if (handle == null)     {     Console.WriteLine("could not locate server");     return 0;     }     Hello obj = (Hello)handle.Unwrap();     Console.WriteLine(obj.Greeting("Christian"));   

Of course, for client-activated objects it's again possible to use the new operator instead of the Activator class. This way we have to register the client-activated object using RemotingConfiguration.RegisterActivatedClientType() . In the architecture of client-activated objects the new operator not only returns a proxy but also creates the remote object:

   RemotingConfiguration.RegisterActivatedClientType(typeof(Hello),     "tcp://localhost:8086/HelloServer");     Hello obj = new Hello();   

Proxy Objects

The Activator.GetObject() and Activator.CreateInstance() methods return a proxy to the client. We actually get two proxies, a transparent proxy and a real proxy. The transparent proxy looks like the remote object - it implements all public methods of the remote object. These methods just call the Invoke() method of the RealProxy , where a message containing the method to call is passed. The real proxy sends the message to the channel with the help of message sinks.

With RemotingServices.IsTransparentProxy() , we can check if our object is really a transparent proxy. We can also get to the real proxy using RemotingServices.GetRealProxy() . Using the Visual Studio .NET debugger, it's now easy to get all the properties of the real proxy:

 ChannelServices.RegisterChannel(new TCPChannel()); Hello obj = (Hello)Activator.GetObject(typeof(Hello),                                        "tcp://localhost:8086/Hi"); if (obj == null) {    Console.WriteLine("could not locate server");    return 0; }   if (RemotingServices.IsTransparentProxy(obj))     {     Console.WriteLine("Using a transparent proxy");     RealProxy proxy = RemotingServices.GetRealProxy(obj);     // proxy.Invoke(message);     }   
Pluggability of a Proxy

The real proxy can be replaced with a custom proxy. A custom proxy can extend the base class System.Runtime.Remoting.RealProxy . We receive the type of the remote object in the constructor of the custom proxy. Calling the constructor of the RealProxy creates a transparent proxy in addition to the real proxy. In the constructor, the registered channels can be accessed with the help of the ChannelServices class to create a message sink IChannelSender.CreateMessageSink() . Besides implementing the constructor, a custom channel has to override the Invoke() method. In Invoke() a message is received that can be analyzed and sent to the message sink.

Messages

The proxy sends a message into the channel. On the server side, a method call can be made after analyzing the message - so let's look at messages.

We have some message classes for method calls, responses, return messages, and so on. What all the message classes have in common is that they implement the IMessage interface. This interface has a single property: Properties . This property represents a dictionary with the IDictionary interface where the URI to the object, the called MethodName , MethodSignature , TypeName , Args , and the CallContext are packaged.

Below is the hierarchy of the message classes and interfaces:

click to expand

The message that is sent to the real proxy is a MethodCall . With the interfaces IMethodCallMessage and IMethodMessage we have easier access to the properties of the message than through the IMessage interface. Instead of having to use the IDictionary interface, we have direct access to the method name, the URI, the arguments, and so on. The real proxy returns a ReturnMessage to the transparent proxy.

Message Sinks

The Activator.GetObject() method calls RemotingServices.Connect() to connect to a well-known object. In the Connect() method, an Unmarshal() happens where not only the proxy, but also envoy sinks, are created. The proxy uses a chain of envoy sinks to pass the message to the channel. All the sinks are interceptors that can change the messages and perform some additional actions such as creating a lock, writing an event, performing security checking, and so on.

All message sinks implement the interface IMessageSink . This interface defines one property and two methods:

  • The property NextSink is used by a sink to get to the next sink and pass the message along.

  • For synchronous messages, SyncProcessMessage() is invoked by a previous sink or by the remoting infrastructure. It has an IMessage parameter to send a message and to return a message.

  • For asynchronous messages, AsyncProcessMessage() is invoked by a previous sink in the chain, or by the remoting infrastructure. AsyncProcessMessage() has two parameters: a message and a message sink that receives the reply.

Let's take a look at the three different message sinks available for use.

Envoy Sink

We can get to the chain of envoy sinks by the IEnvoyInfo interface. The marshaled object reference ObjRef has a EnvoyInfo property that returns the IEnvoyInfo interface. The envoy list is created from the server context, so the server can inject functionality into the client. Envoys can collect identity information about the client and pass that information to the server.

Server-Context Sink

When the message is received on the serverside of the channel, it is passed to the server-context sinks. The last of the server-context sinks routes the message to the object-sink chain.

Object Sink

The object sink is associated with a particular object. If the object class defines particular context attributes, context sinks are created for the object.

Passing Objects in Remote Methods

The parameter types of remote method calls aren't just limited to basic data types, but can also be classes that we define ourselves . For remoting we have to differentiate between three types of classes:

  • Marshal- by-value classes are serialized through the channel. Classes that should be marshaled must implement either the ISerializable interface, or must be marked using the [Serializable] attribute. Objects of these classes don't have a remote identity, because the complete object is marshaled through the channel, and the object that is serialized to the client is independent of the server object (or the other way around). Marshal-by-value classes are also called unbound classes because they don't have data that depends on the application domain.

  • Marshal-by-reference classes do have a remote identity. The objects are not passed across the wire, but instead a proxy is returned. A class that is marshaled by reference must derive from MarshalByRefObject . MarshalByRefObject s are known as application domain bound objects . A specialized version of MarshalByRefObject is ContextBoundObject : the abstract class ContextBoundObject is derived from MarshalByRefObject . If a class is derived from ContextBoundObject , a proxy is needed even in the same application domain when context boundaries are crossed. Such objects are called context-bound objects , and they are only valid in the creation context.

  • Classes that are not serializable and don't derive from MarshalByRefObject are not remotable . Classes of these types cannot be used as parameters in a remote object's public methods. These classes are bound to the application domain where they are created. Non-remotable classes should be used if the class has a data member that is only valid in the application domain, such as a Win32 file handle.

To see marshaling in action, we will change the remote object to send two objects to the client: the class MySerialized will be sent marshal-by-value, the class MyRemote marshal-by-reference. In the methods a message is written to the console so that we can verify if the call was made on the client or on the server. In addition, the Hello class is changed to return a MySerialized and a MyRemote instance:

 using System; namespace Wrox.ProCSharp.Remoting {   [Serializable]     public class MySerialized     {     public MySerialized(int val)     {     a = val;     }     public void Foo()     {     Console.WriteLine("MySerialized.Foo called");     }     public int A     {     get     {     Console.WriteLine("MySerialized.A called");     return a;     }     set     {     a = value;     }     }     protected int a;     }     public class MyRemote : System.MarshalByRefObject     {     public MyRemote(int val)     {     a = val;     }     public void Foo()     {     Console.WriteLine("MyRemote.Foo called");     }     public int A     {     get     {     Console.WriteLine("MyRemote.A called");     return a;     }     set     {     a = value;     }     }     protected int a;     }   public class Hello : System.MarshalByRefObject    {       public Hello()       {          Console.WriteLine("Constructor called");       }       ~Hello()       {          Console.WriteLine("Destructor called");       }       public string Greeting(string name)       {          Console.WriteLine("Greeting called");          return "Hello, " + name;       }   public MySerialized GetMySerialized()     {     return new MySerialized(4711);     }     public MyRemote GetMyRemote()     {     return new MyRemote(4712);     }   } } 

The client application also needs to be changed to see the effects when using marshaled-by-value and marshaled-by-reference objects. We are calling the methods GetMySerialized() and GetMyRemote() to retrieve the new objects. We're also checking if the transparent proxy is used in the Main() method of the file HelloClient.cs :

 ChannelServices.RegisterChannel(new TcpChannel());          Hello obj = (Hello)Activator.GetObject(typeof(Hello),                                                 "tcp://localhost:8086/Hi");          if (obj == null)          {             Console.WriteLine("could not locate server");             return;          }   MySerialized ser = obj.GetMySerialized();     if (!RemotingServices.IsTransparentProxy(ser))     {     Console.WriteLine("ser is not a transparent proxy");     }     ser.Foo();     MyRemote rem = obj.GetMyRemote();     if (RemotingServices.IsTransparentProxy(rem))     {     Console.WriteLine("rem is a transparent proxy");     }     rem.Foo();   

In the client console window, we can see that the ser object is called on the client. This object is not a transparent proxy because it's serialized to the client. In contrast, the rem object on the client is a transparent proxy. Methods called on this object are transferred to the server:

click to expand

In the server output we see that the Foo() method is called with the remote object MyRemote :

click to expand

Directional Attributes

Remote objects are never transferred over the wire, whereas value types and serializable classes are transferred. Sometimes we want to send the data only in one direction. This can be especially important when the data is transferred over the network. For example, if you want to send data in a collection to the server for the server to perform some calculation on this data and return a simple value to the client, it would not be very efficient to send the collection back to the client. With COM it was possible to declare directional attributes [in] , [out] , and [in, out] to the arguments if the data should be sent to the server, to the client, or in both directions.

With C# we have similar attributes as part of the language: ref and out method parameters. The ref and out method parameters can be used for value types and for reference types that are serializable. Using the ref parameter, the argument is marshaled in both directions, out goes from the server to the client, and using no parameter sends the data to the server.

You can read more about the out and ref keywords in Chapter 2.

Lifetime Management

How do a client and a server detect if the other side is not available anymore, and what are the problems we get into?

For a client, the answer can be simple. As soon as the client does a call to a method on the remote object we get an exception of type System.Runtime.Remoting.RemotingException . We just have to handle this exception and do what's necessary, for example perform a retry , write to a log, inform the user , and so on.

What about the server? When does the server detect if the client is not around anymore, meaning that the server can go ahead and clean up any resources it's holding for the client? We could wait until the next method call from the client - but maybe it will never arrive . In the COM realm, the DCOM protocol used a ping mechanism. The client sent a ping to the server with the information about the object referenced. A client can have hundreds of objects referenced on the server, and so the information in the ping can be very large. To make this mechanism more efficient it wasn't the information about all objects that was sent, but just the difference from the last ping.

This ping mechanism was efficient on a LAN, but is not suitable for Internet solutions - imagine thousands or millions of clients sending ping information to the server! .NET Remoting has a much more scalable solution for lifetime management: the Leasing Distributed Garbage Collector (LDGC) .

This lifetime management is only active for client-activated objects . SingleCall objects can be destroyed after every method call because they don't hold state. Client-activated objects do have state and we should be aware of the resources used. For client-activated objects that are referenced outside the application domain a lease is created. A lease has a lease time. When the lease time reaches zero the lease expires and the remote object is disconnected and, finally, is garbage-collected .

Lease Renewals

If the client calls a method on the object when the lease has expired, we get an exception. If we have a client where the remote object could be needed for more than 300 seconds (the default value for lease-times), we have three ways to renew a lease:

  • An implicit renewal of the lease is automatically done when the client calls a method on the remote object. If the current lease time is less than the RenewOnCallTime value, the lease is set to RenewOnCallTime .

  • With an explicit renewal the client can specify the new lease time. This is done with the Renew() method of the ILease interface. We can get to the ILease interface by calling the GetLifetimeService() method of the transparent proxy.

  • Sponsoring is the third possibility to renew leases. The client can create a sponsor that implements the ISponsor interface and registers the sponsor in the leasing services using the Register() method of the ILease interface. The sponsor defines the lease extension time. When a lease expires the sponsor is asked for an extension of the lease. The sponsoring mechanism can be used if you want long-lived remote objects on the server.

Leasing Configuration Values

Let's look at the values that can be configured:

  • LeaseTime defines the time until a lease expires.

  • RenewOnCallTime is the time the lease is set on a method call if the current lease time has a lower value.

  • If a sponsor is not available within the SponsorshipTimeout , the remoting infrastructure looks for the next sponsor. If there are no more sponsors, the lease expires.

  • The LeaseManagerPollTime defines the time interval at which the lease manager checks for expired objects.

The default values are listed in this table:

Lease Configuration

Default Value (seconds)

LeaseTime

300

RenewOnCallTime

120

SponsorshipTimeout

120

LeaseManagerPollTime

10

Classes Used for Lifetime Management

The ClientSponsor is one sponsor that implements the ISponsor interface. It can be used on the client side for lease-extension. With the ILease interface we can get all information about the lease, all the lease properties and the current lease time and state. The state is specified with the LeaseState enumeration. With the LifetimeServices utility class we can get and set the properties for the lease of all remote objects in the application domain.

Getting Lease Information Example

In this small code example we are accessing the lease information by calling the GetLifetimeService() method of the transparent proxy. For the ILease interface we have to use the namespace System.Runtime.Remoting.Lifetime :

Important 

Remember, you can use this only for client-activated objects. SingleCall objects are instantiated with every method call anyway, so the leasing mechanism doesn't apply.

   ILease lease = (ILease)obj.GetLifetimeService();     if (lease != null)     {     Console.WriteLine("Lease Configuration:");     Console.WriteLine("InitialLeaseTime: " +     lease.InitialLeaseTime);     Console.WriteLine("RenewOnCallTime: " +     lease.RenewOnCallTime);     Console.WriteLine("SponsorshipTimeout: " +     lease.SponsorshipTimeout);     Console.WriteLine(lease.CurrentLeaseTime);     }   

This is the output we see in the client console window:

Changing Default Lease Configurations

The server itself can change the default lease configuration for all remote objects using the System.Runtime.Remoting.Lifetime.LifetimeServices utility class:

   LifetimeServices.LeaseTime = TimeSpan.FromMinutes(10);     LifetimeServices.RenewOnCallTime = TimeSpan.FromMinutes(2);   

If you want different default lifetimes depending on the type of the remote object, you can change the lease configuration of the remote object by overriding the InitializeLifetimeService() method of the base class MarshalByRefObject :

 public class Hello : System.MarshalByRefObject {    public Hello()    {       Console.WriteLine("Constructor called");    }    ~Hello()    {       Console.WriteLine("Destructor called");    }   public override Object InitializeLifetimeService()     {     ILease lease = (ILease)base.InitializeLifetimeService();     lease.InitialLeaseTime = TimeSpan.FromMinutes(10);     lease.RenewOnCallTime = TimeSpan.FromSeconds(40);     return lease;     }   

The lifetime services configuration can also be done using a configuration file as we see next.

  


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

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