.Net Remoting Architecture


Now that you've seen a simple client and server in action, this section discusses an overview of the .NET Remoting architecture before we dive into the details. Based on the previously created program, you look at the details of the architecture and the mechanisms for extensibility.

This section explores the following topics:

  • 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. .NET Framework 2.0 ships with channel classes that communicate using TCP, HTTP, or IPC. You 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, .NET Remoting Web services can listen to port 80 so that they can easily be used by these clients.

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 environment compared to the HTTP channel.

The IPC channel is best for communication on a single system across different processes. It uses the Windows interprocess communication mechanism and thus it is faster than the other channels.

Performing a method call on the remote object causes the client channel object to send 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 return 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.

Note

A very useful tool for testing to see what data is sent across the network is tcpTrace. tcpTrace can be downloaded from http://www.pocketsoap.com/tcptrace.

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

A server can listen to multiple channels. This code creates HTTP, TCP, and IPC channels 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; using System.Runtime.Remoting.Channels.Ipc; namespace Wrox.ProCSharp.Remoting {    public class HelloServer    { public static void Main(string[] args) { TcpServerChannel tcpChannel = new TcpServerChannel(8086); HttpServerChannel httpChannel = new HttpServerChannel(8085); IpcServerChannel ipcChannel = new IpcServerChannel("myIPCPort");  // register the channels ChannelServices.RegisterChannel(tcpChannel,false); ChannelServices.RegisterChannel(httpChannel,false); ChannelServices.RegisterChannel(ipcChannel,false); //... } 

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, TcpChannel, and IPCChannel 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(). The property ChannelData can be used to access the received data.

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

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

The method ShowChannelProperties() is called after creating the channels in the Main() method. When you start the server you will get the console output that is shown in Figure 29-5. As you can see here, the default name for the TcpServerChannel is tcp, the HTTP channel is called http server, and the IPC channel is called ipc server. The HTTP and TCP channels have a default priority of 1, whereas the IPC channel has the default priority 20. The IPC channel is faster than the other channels because it has a higher priority. The ports that have been set with the constructors are seen in the URI. The URI of the channels shows the protocol, IP address, and port number:

image from book
Figure 29-5

TcpServerChannel tcpChannel = new TcpServerChannel(8086); ShowChannelProperties(tcpChannel); HttpServerChannel httpChannel = new HttpServerChannel(8085); ShowChannelProperties(httpChannel); IpcServerChannel ipcChannel = new IpcServerChannel("myIPCPort"); ShowChannelProperties(ipcChannel); 

Setting channel properties

You can set all the properties of a channel in a list using the constructor HttpServerChannel (IDictionary, IServerChannelSinkProvider). The generic Dictionary class implements IDictionary, so you can set the Name, Priority, and Port property with the help of this class. To use the Dictionary class you have to import the System.Collections.Generic namespace.

With the constructor of the class HttpServerChannel, you can pass an object that implements the interface IServerChannelSinkProvider in addition to the IDictionary parameter. In the example, a BinaryServerFormatterSinkProvider is set instead of the SoapServerFormatterSinkProvider, which is the default of the HttpServerChannel. The default implementation of the BinaryServerFormatterSinkProvider class associates a BinaryServerFormatterSink class with the channel that uses a BinaryFormatter object to convert the data for the transfer:

 Dictionary<string, string> properties = new Dictionary<string, string>(); properties["name"] = "HTTP Channel with a Binary Formatter"; properties["priority"] = "15"; properties["port"] = "8085"; BinaryServerFormatterSinkProvider sinkProvider =  new BinaryServerFormatterSinkProvider(); HttpServerChannel httpChannel =  new HttpServerChannel(properties, sinkProvider); ShowChannelProperties(httpChannel); 

The new output from the server console shows the new properties of the HTTP channel (see Figure 29-6).

image from book
Figure 29-6

Depending on the channel types, different properties can be specified. Both the TCP and the HTTP channel support the name and priority channel property used in the 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 by using a transport protocol other than HTTP, TCP, or IPC, or the existing channels can be extended to offer more functionality:

  • The sending part must implement the IChannelSender interface. The most important part is the CreateMessageSink() method, with which the client sends a URL, and here a connection to the server can be instantiated. 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. You have to start the listening in the ChannelData get property. Then you can wait in a separate thread to receive data from the client. After unmarshaling the message, you 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 by 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 shown 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, the SoapClientFormatterSink class uses the SyncProcessMessage() and AsyncProcessMessage() methods of the SoapFormatter class to serialize the message. The SoapServerFormatterSink class deserializes the message, again using the SoapFormatter class.

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, you 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 you see later.

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

Here you can see the server code to register the HTTP, TCP, and IPC channels:

TcpChannel tcpChannel = new TcpChannel(8086); HttpChannel httpChannel = new HttpChannel(8085); IpcChannel ipcChannel = new IpcChannel("myIPCPort");  ChannelServices.RegisterChannel(tcpChannel,false); ChannelServices.RegisterChannel(httpChannel,false); ChannelServices.RegisterChannel(ipcChannel,false); 

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 you registered. You can also use the GetChannel() method to get to a specific channel by its name. With the help of ChannelServices you can write a custom administration utility that manages your channels. Here is a small example to show how the server channel can be stopped from listening to 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.

Server for well-known objects

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(), typeof(Hello), specifies the type of the remote object. The second argument, "Hi", is the uniform resource identifier of the remote object, used by 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 single-call 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 you needn't 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. You have to pay attention to the locking of resources held by the singleton object; you have to make sure that data can't be corrupted when clients are accessing the singleton object concurrently, but you also have to check that the locking is done efficiently enough to reach the required scalability.

Server for client-activated objects

If a remote object should hold state for a specific client, you can use client-activated objects. The next section looks 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.RegisterWellKnownServiceType(), you have to call RemotingConfiguration.RegisterActivatedServiceType(). With this method, only the type is specified, 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 a remote Activator class. You can get a proxy to a server-activated or well- known remote object by 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, you 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 

In the code samples, three URL examples are used continuously in the code. With the URL, the protocol is specified with http, tcp, or ipc. With the HTTP and TCP channels the server name is localhost and the port numbers are 8085 and 8086. With the IPC channel there's no need to define a hostname because IPC is possible only on a single system. With all protocols the URI is Hi, as follows.

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

Activating well-known objects

In the previous simple client example, well-known objects have been activated. Here, you 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 the class System.Activator that calls RemotingServices. Connect() to return a proxy object to the remote object. The first argument of this method 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 do on the real object. The second argument is the URL to the remote object. Here the string tcp://localhost:8086/Hi is used. tcp is the protocol, localhost:8086 is the hostname and the port number, and finally Hi is the URI of the object that is specified using RemotingConfiguration.RegisterWellKnownServiceType().

Instead of using Activator.GetObject(), you can also use RemotingServices.Connect() directly:

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

If you prefer to use the new operator to activate well-known remote objects, the remote object can be registered on the client using RemotingConfiguration.RegisterWellKnownClientType(). The arguments are similar: the type of the remote object and the URI. new doesn't really create a new remote object, it returns a proxy similar to Activator.GetObject() instead. 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. The leasing mechanism is discussed 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 the example. This method accepts two string parameters and an array of objects. The first parameter is the name of the assembly, and the second is the type of the remote object class. With the third parameter it is possible to pass arguments to the constructor of the remote object class. The channel and the object name are specified in the object array with the help of an UrlAttribute. To use the UrlAttribute class, the namespace System.Runtime.Remoting. Activation must be specified.

 object[] attrs = {new UrlAttribute("tcp://localhost:8086/HelloServer") }; ObjectHandle handle = Activator.CreateInstance( "RemoteHello", "Wrox.ProCSharp.Remoting.Hello", attrs); if (handle == null) { Console.WriteLine("could not locate server"); return; } 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, but then you 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. Actually, two proxies are used: the transparent proxy and the 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(), you can check if your object is really a transparent proxy. You can also get to the real proxy using RemotingServices.GetRealProxy(). Using the Visual Studio debugger, it's now easy to see 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; } 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.Proxies.RealProxy. The type of the remote object is received 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 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. This section takes a closer look at the messages.

The .NET Framework has 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, which packages the URI to the object, MethodName, MethodSignature, TypeName, Args, and CallContext.

Figure 29-7 shows the hierarchy of the message classes and interfaces. The message that is sent to the real proxy is an object of type MethodCall. With the interfaces IMethodCallMessage and IMethodMessage, you can have easier access to the properties of the message than through the IMessage interface. Instead of using the IDictionary interface, you have direct access to the method name, the URI, the arguments, and so on. The real proxy returns a ReturnMessage to the transparent proxy.

image from book
Figure 29-7

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.

The following sections look at the three different message sinks available for use.

Envoy sink

You can get to the chain of envoy sinks using the IEnvoyInfo interface. The marshaled object reference ObjRef has the EnvoyInfo property, which 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 server side 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 limited only to basic data types but can also be classes that you define yourself. For remoting, three types of classes must be differentiated:

  • Marshal-by-value classes — These classes are serialized through the channel. Classes that should be marshaled must be marked with 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. Serialization is discussed in detail in Chapter 34,"Manipulating Files and the Registry."

  • Marshal-by-reference classes — These classes do have a remote identity. The objects are not passed across the wire, but a proxy is returned instead. A class marshaled by reference must derive from MarshalByRefObject. MarshalByRefObjects 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.

  • Not-remotable classes — These classes are not serializable and don't derive from MarshalByRefObject. 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, change the remote object to send two objects to the client: the class MySerialized will be sent marshal-by-value, and the class MyRemote will be sent marshal-by-reference. In the methods a message is written to the console so that you can verify if the call was made on the client or on the server. In addition, the Hello class is extended 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. Invoke the methods GetMySerialized() and GetMyRemote() to retrieve the new objects. Also make use of the method RemotingServices.IsTransparentProxy() to check whether or not the returned object is a proxy:

ChannelServices.RegisterChannel(new TcpClientChannel(),false); 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 (see Figure 29-8), you 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.

image from book
Figure 29-8

The server output (see Figure 29-9) reveals that the Foo() method is called with the remote object MyRemote.

image from book
Figure 29-9

Security and serialized objects

One important difference with .NET Remoting and ASP.NET Web services is how objects are marshaled. With ASP.NET Web services, only the public fields and properties are transferred across the wire. .NET Remoting uses a different serialization mechanism to serialize all data, including all private data. Malicious clients could use the serialization and deserialization phases to harm the application.

To take this problem into account, two automatic deserialization levels are defined when passing objects across .NET Remoting boundaries: low and full.

By default, low-level deserialization is used. With low-level deserialization it is not possible to pass ObjRef objects and objects that implement the ISponsor interface. To make this possible, you can change the deserialization level to full. You can do this programmatically by creating a formatter sink provider, and assign the property TypeFilterLevel. For the binary formatter, the provider class is BinaryServerFormatterSinkProvider, whereas for the SOAP formatter the provider class is SoapServerFormatterSinkProvider.

The following code shows how you can create a TCP channel with full serialization support:

 BinaryServerFormatterSinkProvider serverProvider =  new BinaryServerFormatterSinkProvider(); serverProvider.TypeFilterLevel = TypeFilterLevel.Full; BinaryClientFormatterSinkProvider clientProvider = new BinaryClientFormatterSinkProvider(); Dictionary<string, string> properties = new Dictionary<string, string>();  properties["port"] = 6789;  TcpChannel channel = new TcpChannel(properties, clientProvider, serverProvider); 

At first, a BinaryServerFormatterSinkProvider is created where the property TypeFilterLevel is set to TypeFilterLevel.Full. The enumeration TypeFilterLevel is defined in the namespace System.Runtime.Serialization.Formatters, so you have to declare this namespace. For the client side of the channel, a BinaryClientFormatterSinkProvider is created. Both the client-side and the server-side formatter sink provider instances are passed to the constructor of the TcpChannel, as well as the IDictionary properties that define the attributes of the channel.

Directional attributes

Remote objects are never transferred over the wire, whereas value types and serializable classes are transferred. Sometimes the data should be sent in one direction only. 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 the 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.

C# has 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, and with the out parameter the data is sent from the server to the client. With no parameters the data is sent to the server.

Note

You can read more about the out and ref keywords in Chapter 3, "Objects and Types."

Lifetime Management

How do a client and a server detect if the other side is not available anymore, and what are the problems you might 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 you get an exception of type System.Runtime.Remoting.RemotingException. You 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? You 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, DCOM didn't send all the information about all objects, but just the difference from the previous ping.

This ping mechanism was efficient on a LAN, but it is not suitable for scalable solutions — imagine thousands 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 and well-known singleton objects. Single-call objects can be destroyed after every method call because they don't hold state. Client- activated objects do have state and you should be aware of the resources used. A lease is created for client-activated objects that are referenced outside the application domain. A lease has a lease time, and when the lease time reaches zero, the lease expires and the remote object is disconnected and, finally, it is garbage-collected.

Lease renewals

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

  • Implicit renewal — This 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.

  • Explicit renewal — With this renewal, the client can specify the new lease time. This is done with the Renew() method of the ILease interface. You can get to the ILease interface by calling the GetLifetimeService() method of the transparent proxy.

  • Sponsoring renewal — In the case of this renewal 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

Here are 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 the following table.

Lease Configuration

Default Value (seconds)

LeaseTime

300

RenewOnCallTime

120

SponsorshipTimeout

120

LeaseManagerPollTime

10

Classes used for lifetime management

The ClientSponsor class implements the ISponsor interface. It can be used on the client side for lease extension. With the ILease interface you 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, you 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, the lease information is accessed by calling the GetLifetimeService() method of the transparent proxy. For the ILease interface, you have to declare the namespace System. Runtime.Remoting.Lifetime.

The leasing mechanism can only be used with stateful (client-activated and singleton) objects. Single- call objects are instantiated with every method call anyway, so the leasing mechanism doesn't apply. To offer client-activated objects with the server you can change the remoting configuration to a call to RegisterActivatedServiceType() in the file HelloServer.cs:

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

In the client application, the instantiation of the remote object must be changed, too. Instead of using the method Activator.GetObject(), Activator.CreateInstance() is used to invoke client-activated objects:

ChannelServices.RegisterChannel(new TcpClientChannel()); object[] attrs = {new UrlAttribute("tcp://localhost:8086/Hello") }; Hello obj = (Hello)Activator.CreateInstance(typeof(Hello), null, attrs); 

To show the leasing time, you can use the ILease interface returned by calling GetLifetimeService() from the proxy object:

 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); } 

Figure 29-10 shows the output in the client console window.

image from book
Figure 29-10

Changing default lease configurations

The server itself can change the default lease configuration for all remote objects of the server 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 by using a configuration file that is discussed next.




Professional C# 2005
Pro Visual C++ 2005 for C# Developers
ISBN: 1590596080
EAN: 2147483647
Year: 2005
Pages: 351
Authors: Dean C. Wills

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