Writing Distributed .NET Applications

Rick RossPILLAR Technology Group, LLC.


Introduction

Distributed Computing

Making .NET applications communicate

COM/DCOM Interop

Platform Invoke

J2EE and CORBA

Web Services

What is .NET Remoting?

AppDomains

Why are AppDomains important?

Remoting Architecture Overview

Channels

Formatters

Marshaling Objects

Creating Remote Objects

Server-Activated Objects

SingleCall

Singleton

Published Objects

Client-Activated Objects

Lifetime Management

ObjRef Class

Proxy Objects

Configuration

Lifetime Tag

Channels & Channel Tags

Service Tag

Client Tag

Putting it all together

Additional Resources


Introduction

Before .NET, writing distributed applications was very difficult and required many different steps. While Delphi certainly provided useful tools for helping with these aspects of distributed applications, they still were cumbersome to install, configure, write and debug. Using Delphi for .NET and the Remoting namespace, this paper will demonstrate the power and ease of writing distributed applications — the .NET way.

Distributed Computing

The basic idea of distributed computing is to partition an application across several computer systems. By using these various resources, applications become more robust and scalable when properly designed and developed.

Distributed computing, in a nutshell, is the need for a client to execute a routine on another computer located somewhere on the network. From the client’s perspective, the routine appears to be executed locally. The actual execution of the routine on a server application is typically accomplished by using the following steps:

  1. Client packs up the name of the routine and any arguments needed.
  2. Client sends the package to the server.
  3. Server decodes the package.
  4. Server executes the requested routine.
  5. Server packs up any return information.
  6. Server sends the package to the client.
  7. Client decodes the package and handles it appropriately.

Throughout this document, there will be references to “Client” and “Server” applications. These references are not meant to put any limits on what a client or server can do. Rather, they are used for clarifying the specific role for a particular discussion. Furthermore, a server application may be talking to only one client application or, more likely, many client applications, depending on the circumstances. Also, note that Server applications can, and frequently do, become clients to other applications.

Making .NET applications communicate

Communicating between two or more applications requires some form of Interprocess Communication (IPC). .NET applications are no different. Options for communicating include COM/DCOM Interoperability, Platform Invoke, J2EE, CORBA, Web Services and .NET Remoting. While the focus on this paper is only on .NET Remoting, brief discussions of several other options are listed in the following section.

COM/DCOM Interop

The .NET framework provides the ability to communicate with COM objects as if they were native .NET objects. Similarly, COM/DCOM can use .NET objects as if they were native COM objects. This ability provides a mechanism for reusing existing COM and DCOM servers. Brian Long has a great paper that details COM Interoperability. His paper can be found here.

Platform Invoke

Platform Invoke (also known as PInvoke) is another method that .NET applications can use to communicate with each other. PInovke provides a mechanism for calling any exported functions that are located within a DLL. Using PInvoke, .NET applications can use native IPC APIs (e.g. named pipes) for communicating with each other or existing Win32 applications.

J2EE and CORBA

Many companies have invested heavily in J2EE and CORBA infrastructures. Fortunately, .NET applications can communicate with these existing business objects using Borland’s Janeva. Other alternatives exist like SeeBeyond or an open-source tool called Remoting.Corba.

Web Services

One of the most recent additions to distributed computing is SOAP, also known as web services. Web services are built on existing standards like XML, WSDL, and HTTP. .NET provides an extremely easy way of exposing an object as a web service. By simply using the appropriate attributes, the .NET framework takes care of the rest of work automatically. Later in this paper, the close relationship between .NET Remoting and SOAP will be explained.

What is .NET Remoting?

Remoting in the .NET framework consists of numerous services that provide the ability to invoke objects that exist anywhere on the network. These objects could be in the same machine, on the same network, or located around the world. Only objects that are hosted within a CLR environment can be accessed using .NET remoting. Anytime an object in one AppDomain needs to communicate with another AppDomain, .NET Remoting is used.

AppDomains

An AppDomain is sandbox for executing code in .NET. It provides an isolated environment, similar to the way Windows protects multiple concurrent Win32 processes from corrupting each other. However, that doesn’t mean that each AppDomain lives in its own Win32 process, since multiple AppDomains can (and typically do) live in the same Win32 process.

When a CLR process is created, it creates an AppDomain to host the assemblies that will be used. The first AppDomain that is created is called the default AppDomain. This default AppDomain cannot be unloaded until the process exits. Other AppDomains can be created as needed either by the host process or a managed assembly. Figure 1 shows the relationship between a Win32 process, AppDomains and assemblies.

Why are AppDomains important?

AppDomains are to .NET as processes are to Win32. So why are AppDomains important? There are four key reasons listed below

Remoting Architecture Overview

The .NET Remoting framework provides incredible flexibility for communicating between applications. While this flexibility is certainly a nice feature, the remoting capabilities that already come with the framework are equally impressive. This section will provide an introduction to the architecture of remoting in .NET, from the bottom layer on up.

In the figure below, two .NET applications are communicating with each other using remoting.

Channels

At the lowest level, applications communicate to each other on a channel. A channel is a connection between applications. It is used for sending and receiving messages — the actual data stream. All communicating applications must agree to use the same channel. Two channels that are built into the framework include HTTP and TCP, the building blocks of the Internet. These channels are located in System.Runtime.Remoting.Channels.Http.HttpChannel and System.Runtime.Remoting.Channels.Tcp.TcpChannel respectively.

Formatters

Once a channel is available for communication, applications must agree on the format of the data being transported over the channel. Think of this as the language layer. Imagine what would happen between two applications speaking German and French. Not much information will be exchanged. However, if they can agree to use Sanskrit, the results will be much more satisfactory.

Prior to being transported, messages are arranged in an appropriate way using a Formatter. A Formatter is responsible for encoding a message being sent and similarly decoding the message after being received. Formats currently available include SOAP (System.Runtime.Serialization.Formatters.Soap.SoapFormatter) and Binary (System.Runtime.Serialization.Formatters.Binary.BinaryFormatter).

Usually, SOAP formatted messages are sent over an HTTP channel. They are not, however, limited to just the HTTP channel. SOAP messages can be sent over a TCP channel as well. Similarly, Binary messages can be sent over HTTP and are not limited to the TCP channel. The important point to remember is that the Formatter and Channel are independent of each other.

Marshaling Objects

Up to this point, the only discussion has been about exchanging messages between client and server applications and no mention has been made about objects. Most likely, a good majority of these messages are in fact remotable .NET objects. Remotable objects need to be marshaled across AppDomain boundaries. Marshaling describes the manner in which methods are invoked upon remote objects.

There are two types of marshaling available when remoting .NET objects: Marshal By Value (MBV) and Marshal By Reference (MBR). The difference between the two lies in where the object is physically located when the method is invoked.

Objects that are marshaled by value are executed in the requested (client) domain. The entire object is copied from the remote server and the method is executed on the local copy of the object. Only objects that are marked as serializable can be marshaled by value.

Note

Serialization is a method of persisting an object to some storage location. The state of the object is saved by streaming it. Marking an object as Serializable is accomplished by using either [Serializable] attribute or implementing the ISerializable interface. The example below demonstrates one way of using Serializable objects:

program SerializableExample;
{$APPTYPE CONSOLE}

uses
  System.IO,
  System.Runtime.Serialization,
  System.Runtime.Serialization.Formatters.Binary;

type
  [Serializable]
  TSerializeThis = class
  strict private
    FStrictPrivate : integer;
  public
    constructor Create; overload;
    constructor Create( a : integer ); overload;
    procedure ShowValues; 
  end;

constructor TSerializeThis.Create;
begin
  inherited Create;
  FStrictPrivate   := 100;
end;

constructor TSerializeThis.Create( a : integer );
begin
  inherited Create;
  FStrictPrivate   := a;
end;

procedure TSerializeThis.ShowValues; 
begin
  writeln('Value: ', FStrictPrivate);
end;

const
  FILENAME = 'se.dat';

var
  myobj     : TSerializeThis;
  fs        : FileStream;
  formatter : IFormatter;
  
begin
  formatter := BinaryFormatter.Create();

  if ParamCount >= 1 then
  begin
    writeln('Creating file...');
    myobj := TSerializeThis.Create( 1 );
    myobj.ShowValues;
    fs := FileStream.Create( FILENAME, FileMode.Create, FileAccess.Write, FileShare.None );
    try
      formatter.Serialize( fs, myobj );
    finally
      fs.Close();
    end;
  end
  else
  begin
    writeln('Opening file...');
    fs := FileStream.Create( FILENAME, FileMode.Open, FileAccess.Read, FileShare.Read);
    try
      myobj := TSerializeThis( formatter.Deserialize(fs) );
    finally
      fs.Close();
    end;
    myobj.ShowValues;
  end;
end.

Another requirement of marshal by value objects is that the client application must be able to locate the assembly in which the objects are defined. This requirement imposes installation and distribution issues. Other issues to consider include the size of the object, how many methods need to be called, does a copy of the object really make sense, and security. In general, use marshal by value objects when the object is small, many methods need to be executed on the object, security is not a big deal and the object does not reference remote resources like database connections, files, etc.

Marshal by Reference objects remain on the server application. Every method invocation requires the parameters to be encoded, sent to the server, decoded, and executed. Any return values the method has must also follow the same path.

Instead of copying the remote object locally, the remoting framework creates reference to the remote object (ObjRef). This reference is a local proxy object that looks exactly like the remote object. Even though the client proxy knows it is referencing a remote object (this will be discussed later), method calls to the remote object look just like a method call to a local object.

On the server, a stub is created with which the proxy object communicates. The stub object, in turn, communicates to the actual server object. This proxy/stub pair is responsible for encoding and decoding the parameters, method identifiers and return values between the client and server. See Figure 3 below:

Figure 3 - Remoting and Proxy/Stub objects
Figure 3 – Marshal By Reference and Proxy/Stub objects.

Marshal by reference objects are those objects that are descendants of the System.MarshalByRefObject class. Any other object that is remoted is implicitly treated as a Marshal by value, as long as it is serializable.

System.MarshalByRefObject = class ( System.Object )
public
  function CreateObjRef( requestedType : Type ) : ObjRef; virtual; 
  function InitializeLifetimeService(  ) : ILease; virtual; 
  function GetLifetimeService(  ) : ILease; virtual; sealed; 
end;

When the CreateObjRef() method is called by the framework, it provides the information necessary for creating the proxy object. InitializeLifetimeService() method returns an ILease interface, creating one if necessary, for managing the lifetime of remote objects. GetLifetimeService() method also returns the instance responsible for managing the lifetime of remote objects. See the Lifetime management section for more information about ILease and lifetime management.

Using marshal by reference objects requires careful attention to parameters and return values. Custom classes should be marshal by value objects. Likewise, when using framework classes, make sure that they are either serializable or descendants of MarshalByRefObject.

Note

In order to make certain classes within Delphi for .NET friendly to remoting, Borland decided to map TPersistent to System.MarshalByRefObject. This opens up the door for descendants of TPersistent, like TStrings and TCollection to be remotable without any additional code.

Creating Remote Objects

Its time to look at how remote objects are created in Delphi for .NET. This section will demonstrate the techniques for creating remote objects. For the purpose of this paper, an assembly has been created using C# that defines the interface for the client and server.

Note

The examples in this paper use best practices for writing distributed .NET applications. There are other methods of performing the same results that are not discussed in this paper. See the Additional Resources section for further sources of information.

using System;

namespace RickRoss.BorCon2003
{
  public interface IExample1
  {
    void SetValue( int aNewValue );
    int  GetValue();
  }

  public interface IExample1Factory
  {
    IExample1 GetNewInstance();
    IExample1 GetNewInstance( int anInitialValue );
  }
}

Creating the assembly is easy by using the command line C# compiler (csc.exe) that is included in the framework SDK. To compile, use the following command:



c:\borcon2003>csc /t:library BorCon2003.cs

A copy of BorCon2003.dll must exist on the client and server. Executing either application without this dll will raise a System.IO.FileNotFoundException.

Server-Activated Objects

Remote objects that are controlled by a server are called Server-Activated Objects. There are two types of Server-Activated Objects: SingleCall and Singleton. This section will explain the differences and demonstrate how to create them.

SingleCall

Remote objects that are designated as SingleCall are stateless objects. Between method calls, the server creates a new instance of the object. SingleCall objects work extremely well in situations where high-scalability is required. Listed below is the code that demonstrates how to set up a remote object as SingleCall.

program SingleCallServer;
{$APPTYPE CONSOLE}

uses
  RickRoss.BorCon2003, // imported from BorCon2003.dll
  System.Runtime.Remoting,
  System.Runtime.Remoting.Channels.Http,
  System.Runtime.Remoting.Channels;  

type
  TMyRemoteObject = class( MarshalByRefObject, IExample1 )
  private
    FValue : integer;
  public
    procedure SetValue( value : integer );
    function  GetValue : integer;
  end;

procedure TMyRemoteObject.SetValue( value : integer );
begin
  writeln('Server: Setting value to ', value);
  FValue := value;
end;

function  TMyRemoteObject.GetValue : integer;
begin
  writeln('Server: getting value ', FValue );
  Result := FValue;
end;

var
  aChannel : HttpChannel;

begin
  writeln('RemoteServer (SingleCall) starting...');
  aChannel := HttpChannel.Create( 2003 ); // port number

  writeln('Server: Registering channel...');
  ChannelServices.RegisterChannel( aChannel );

  writeln('Server: Registering type...');
  RemotingConfiguration.RegisterWellKnownServiceType( typeof(TMyRemoteObject), 
         'BorCon2003RemoteObject.soap',
         WellKnownObjectMode.SingleCall );

  writeln('Press return to shut down the server...');
  readln;  
end.

In the SingleCallServer, an HTTP channel is created using port 2003. This channel is then registered. Finally, the server registers the object with the .NET remoting framework, specifying the type of the remote object, a URI that designates the location, and what server-activated mode is required (in this case, SingleCall).

Clients connect to a server using code similar to the following:

program ExampleClient1;
{$APPTYPE CONSOLE}

uses
  RickRoss.BorCon2003, // imported from BorCon2003.dll
  System.Collections,
  System.Runtime.Remoting,
  System.Runtime.Remoting.Channels.Http,
  System.Runtime.Remoting.Channels;

const
  SERVER_URI = 'http://localhost:2003/BorCon2003RemoteObject.soap';
  VALUE_TO_SET = 2003;

var
  aChannel : HttpChannel;
  iex1     : IExample1; 

begin
  writeln('Remote client is creating HTTP channel...');
  aChannel := HttpChannel.Create;

  writeln('registering client channel');
  ChannelServices.RegisterChannel( aChannel );

  writeln('obtaining reference to server...');
  iex1 := Activator.GetObject( 
             typeof(IExample1), SERVER_URI ) as 
             IExample1;
  writeln('object reference has been acquired...');

  writeln('Getting a value ', iex1.GetValue() );
  writeln('Setting a value');
  iex1.SetValue(77);
  writeln('Getting value again...', iex1.GetValue() );
end.

First the client creates a channel. Notice that a specific port is not request, since a client application does not care about which port it communicates on. The channel is then registered, identical to the server application. A remote object is requested using Activator.GetObject, specifying the appropriate type and where the object is located (take note that it is the full URI). The remote object is then coerced into IExample1 interface. Now methods on the remote object can be called.

As expected, running the SingleCallServer and the ExampleClient1 produces the following results from the client:



C:\BorCon2003> ExampleClient1
Remote client is creating HTTP channel…
registering client channel
obtaining reference to server…
object reference has been acquired…
Getting a value 0
Setting a value
Getting value again…0

Notice that any calls to GetValue will always return a zero, since the server is creating a new instance for each method.

Singleton

Great for sharing data among client applications, Singleton objects are guaranteed to have either zero or one instance. They are only created after a client has made a method call. Once called, the .NET framework checks to see if an instance already exists, and if not, automatically creates a new instance. Be aware that Singleton objects will not live forever. They will also be garbage collected unless a leasing sponsor is used. An example that demonstrates how to use a sponsor to renew a lease is shown later in this paper.

Similar to the SingleCallServer example above, the SingletonServer code is identical except for the call to RegisterWellKnownServiceType method. This line is listed below:

  RemotingConfiguration.RegisterWellKnownServiceType( typeof(TMyRemoteObject), 
         'BorCon2003RemoteObject.soap',
         WellKnownObjectMode.Singleton );

The client application does not need to know how the server has implemented the remote object. Therefore, the client example shown above works with the Singleton and SingleCall server objects. Running the SingletonServer and the client more than once results in the following output:



C:\BorCon2003> ExampleClient1
Remote client is creating HTTP channel…
registering client channel
obtaining reference to server…
object reference has been acquired…
Getting a value 0
Setting a value
Getting value again…77

C:\BorCon2003> ExampleClient1
Remote client is creating HTTP channel…
registering client channel
obtaining reference to server…
object reference has been acquired…
Getting a value 77
Setting a value
Getting value again…77

Notice how the state of the object is now retained between method calls and multiple clients.

Note

The Activator.GetObject() method is not the only way of creating a proxy object to a remote object. Alternatively, the RemotingConfiguration.RegisterWellKnownClientType() method can be used, specifying the object type and the server URI. Once registered, the proxy object is created by constructing the type that was registered, looking identically to a locally created object. A disadvantage of using RegisterWellKnownClientType is that the type must be an object and not an interface. If this functionality is needed, use a public abstract class rather than an interface.

Published Objects

Both SingleCall and Singleton objects require objects that have an empty constructor, since the .NET framework is responsible for instantiating objects as needed. In addition, if the creation of an object takes some time, the client is forced to wait until the object has been created. Fortunately, there is another method of creating an object on the server and registering it with the remoting framework. These objects are known as Published Objects.

Instead of calling the RegisterWellKnownServerType() method, an instance is created and passed to the RemotingServices.Marshal() method, along with the specified URI. The code fragment from the PublishedServer.dpr file is shown below that demonstrates how this is accomplished.

  myobj  := TMyRemoteObject.Create( 2003 );
  writeln('Server: marshalling created object...');
  RemotingServices.Marshal(myobj, 'BorCon2003RemoteObject.soap');

Objects that are registered using the Marshal() method are Singleton objects, sharing the instance between all client applications.

Client-Activated Objects

All of the examples to this point have been Server-Activated Objects (SAO). While the client application indirectly causes SAO’s to be activated, it is really the server that has ultimate control. Certain types of applications require that the client have control when a remote object is created. Client-Activated Objects (CAO) are those remote objects that are directly created by the client application.

Server-Activated Objects are extremely limited in what they can do. Only two choices are available for dealing with remote objects. Either every client gets its own stateless object, or all clients share a common object. These two options are at opposite ends of the spectrum. Furthermore, only objects that have default, parameterless constructor can be used. Clearly these limitations are too restrictive. Fortunately, client-activated objects give an alternative method.

First, Client-Activated Objects can have constructors containing any number of parameters. Second, CAO’s are statefull, that is, they retain their state between method calls. Finally, the semantics for using CAO’s are identical to how local .NET objects are used. Examples that demonstrate how to create Client-Activated Objects will be shown following the next section.

Lifetime Management

Managing the lifetime of distributed objects is a non-trivial, complex issue. Knowing when to clean up server resources is an especially difficult issue to manage. Fortunately, the remoting framework provides the flexibility needed for managing remote objects.

Marshal by reference objects can be activated by the client or server. Server-Activated Objects are not immediately created. When a client proxy object makes its first call to one of its methods, the stub object then creates the server object, whereas Client-Activated Objects are created on the server when the client creates the object.

Other than SingleCall objects, some technique is needed for knowing when clients are finished using remote objects. Instead of using a form of reference counting, the .NET remoting framework introduces the concepts of a lease manager and sponsorship for controlling the lifetime of remote objects.

Located on the server, the lease manager watches the activity on a remote object. If a period of time goes by without any activity, the lease manager removes all references to the remote object, thereby making it available for garbage collection. Any activity on the remote object restarts the countdown.

Sponsorship can occur in either the client or the server, by registering with the lease manager. When a specific object is sponsored, the lease manager contacts the sponsor(s) prior to deactivating the object. This allows the sponsor to determine if the lease should be extended.

While lease sponsorship can occur in the client or server, it should, in most cases, only occur in the server. This will eliminate the chatty conversation between client’s sponsor and the lease manager on the server.

The following example shows how to use Client-Activated Objects, lease manager, and sponsorship for controlling the lifetime of remote objects. First up is the code for the server application:

program CAOServer;
{$APPTYPE CONSOLE}

uses
  RickRoss.BorCon2003,
  System.Runtime.Remoting,
  System.Runtime.Remoting.Channels.Http,
  System.Runtime.Remoting.Channels,
  System.Runtime.Remoting.Lifetime;  

type
  // notice that this class now implements ISponsor as well
  TMyRemoteObject = class( MarshalByRefObject, IExample1, ISponsor )
  private
    FValue : integer;
  public
    constructor Create; overload;
    constructor Create( anInitialValue : integer); overload;
    function  InitializeLifetimeService : System.Object; override;
    function  Renewal( lease : ILease ) : TimeSpan;
    procedure SetValue( value : integer );
    function  GetValue : integer;
  end;

  TMyRemoteObjectFactory = class( MarshalByRefObject, IExample1Factory )
  public
    function GetNewInstance : IExample1; overload;
    function GetNewInstance( anInitialValue : integer ) : IExample1; overload;
  end;

constructor TMyRemoteObject.Create; 
begin
  inherited Create;
  FValue := 1;
end;

constructor TMyRemoteObject.Create( anInitialValue : integer); 
begin
  inherited Create;
  FValue := anInitialValue;
end;

function  TMyRemoteObject.InitializeLifetimeService : System.Object; 
var
  lease : Ilease;
begin
  writeln('Server: Initializing lifetime service...');
  lease := inherited InitializeLifetimeService() as ILease;
  if (lease.CurrentState = LeaseState.Initial) then
  begin
    lease.InitialLeaseTime := TimeSpan.FromMinutes(1);
    lease.Register( self );
  end;
  Result := lease;
end;

function TMyRemoteObject.Renewal(lease : ILease ) : TimeSpan;
begin
  // renew this lease for another minute...
  writeln('Request for renewal...');
  lease.Renew( TimeSpan.FromMinutes( 1 ) );
end;

procedure TMyRemoteObject.SetValue( value : integer );
begin
  writeln('Server: Setting value to ', value);
  FValue := value;
end;

function  TMyRemoteObject.GetValue : integer;
begin
  writeln('Server: getting value ', FValue );
  Result := FValue;
end;

function TMyRemoteObjectFactory.GetNewInstance : IExample1;
begin
  writeln('Server: Creating a new instance with default value ');
  Result := TMyRemoteObject.Create;
end;

function TMyRemoteObjectFactory.GetNewInstance( anInitialValue : integer ) : IExample1;
begin
  writeln('Server: Creating a new instance with value: ', anInitialValue);
  Result := TMyRemoteObject.Create( anInitialValue );
end;

var
  aChannel : HttpChannel;

begin
  writeln('RemoteServer (SingleCall) starting...');
  aChannel := HttpChannel.Create( 2003 ); // port number
  writeln('Server: Registering channel...');
  ChannelServices.RegisterChannel( aChannel );
  writeln('Server: Registering type...');
  RemotingConfiguration.RegisterWellKnownServiceType( typeof(TMyRemoteObjectFactory), 
         'BorCon2003RemoteObjectFactory.soap',
         WellKnownObjectMode.Singleton );
  writeln('Press return to shut down the server...');
  readln;  
end.

Notice the definition of TMyRemoteObject in the example above. In addition to implementing the IExample1 interface, it also implements ISponsor, which is needed for managing the lifetime of remote objects. By overriding the InitializeLifetimeService() method, TMyRemoteObject sponsors itself.

In this case, once a TMyRemoteObject instance has been created, it will never be destroyed, since it keeps renewing the lease one minute at a time. For most applications, this behavior is not acceptable, especially for Client-Activated Objects. Consider the case when many clients are creating remote objects. The server will quickly fill all available memory.

Another item to notice is that the factory object is the only type that is registered with the .NET framework. Furthermore, the factor object is set up as a singleton object, since only one factory object is needed to handle all of the client requests.

Now it’s time to look at the code for a client that creates remote objects.

program CAOClient;
{$APPTYPE CONSOLE}

uses
  RickRoss.BorCon2003,
  System.Collections,
  System.Runtime.Remoting,
  System.Runtime.Remoting.Channels.Http,
  System.Runtime.Remoting.Channels;

const
  SERVER_URI = 'http://localhost:2003/BorCon2003RemoteObjectFactory.soap';
  VALUE_TO_SET = 2003;

var
  aChannel : HttpChannel;
  iexf1    : IExample1Factory;
  iex1     : IExample1;
  iex2     : IExample1;

begin
  writeln('Remote client is creating HTTP channel...');
  aChannel := HttpChannel.Create;

  writeln('registering client channel');
  ChannelServices.RegisterChannel( aChannel );

  writeln('obtaining reference to server factory object...');
  iexf1 := Activator.GetObject( 
             typeof(IExample1Factory), SERVER_URI ) as 
             IExample1Factory;

  writeln('creating a new instance from the factory...');
  iex1  := iexf1.GetNewInstance();

  writeln('object reference has been acquired...');
  writeln('Getting a value ', iex1.GetValue() );
  writeln('Setting a value');
  iex1.SetValue(77);
  writeln('Getting value again...', iex1.GetValue() );

  writeln('creating a new instance from the factory...');  
  iex2 := iexf1.GetNewInstance( 55 );

  writeln('object reference has been acquired...');
  writeln('Getting a value ', iex2.GetValue() );
  writeln('Setting a value');
  iex2.SetValue(33);
  writeln('Getting value again...', iex2.GetValue() );
end.

Since a class factory is being used, the IExample1Factory interface is returned like it was before. Once obtained, the appropriate GetNewInstance() method is called to retrieve the IExample1 interface. Methods are then called just like any other .NET object.

Client-Activated Objects are very nice, but they come with an expensive price tag. Since they are statefull, they do not scale. For servers that need high-availability, clustering or fail-over, Client-Activated Objects will not work. As an alternative, use Server-Activated Objects. If state information is needed, store the state information in a database and pass around a session identifier. Then use the session identifier to retrieve the state information from the database.

ObjRef Class

System.Runtime.Remoting.ObjRef is the key to understanding how remoting works. Behind the scenes, ObjRef is the worker bee of .NET remoting. An ObjRef is created when a MarshalByRef object is registered with the .NET remoting framework. Once registered, an ObjRef is a reference to a specific MarshalByRef object, serialized for mobility between AppDomains.

Vital information is kept within the ObjRef. Location and accessibility of the remote object is stored which contains the URI and a list of all registered channels that are available for communicating with it. In addition, the ObjRef contains the strong name of the class, the complete class hierarchy, any and all interfaces that it implements.

ObjRef provides the flexibility of overriding and extending its functionality. Listed below, ObjRef has the following methods:

System.Runtime.Remoting.ObjRef = class ( System.Object, IObjectReference, ISerializable )
public
  constructor Create( o : MarshalByRefObject; requestedType : Type ); 
  constructor Create(  ); 
  procedure  GetObjectData( info : SerializationInfo; context : StreamingContext ); virtual; 
  function   GetRealObject( context : StreamingContext ) : Object; virtual; 
  function   IsFromThisProcess(  ) : Boolean; 
  function   IsFromThisAppDomain(  ) : Boolean; 
  property   URI : System.String read; write;
  property   TypeInfo : System.Runtime.Remoting.IRemotingTypeInfo read; write;
  property   EnvoyInfo : System.Runtime.Remoting.IEnvoyInfo read; write;
  property   ChannelInfo : System.Runtime.Remoting.IChannelInfo read; write;
end;

Notice that an ObjRef can determine if it is a local or remote reference by using the IsFromThisAppDomain() method.

Proxy Objects

There are two proxy classes that are used in remoting. TransparentProxy and RealProxy. A TransparentProxy is an internal class that is created when a client activates a remote object. Since it is an internal class, it cannot be extended or replaced. On the other hand, System.Runtime.Remoting.Proxies.RealProxy is a class that provides opportunity for extending and/or replacing. A great use for creating a custom RealProxy class is for load balancing. RealProxy handles the requests from TransparentProxy to forward the requests on to a remote object.

TransparentProxy is located in the AppDomain of the client. It provides a facade for the remote object, looking like the remote object is in fact, local. Once a client application activates are remote object, an ObjRef is marshalled to the client. After unmarshalling the ObjRef, a TransparentProxy and RealProxy are created. When method calls are made on the TransparentProxy object, it determines if the call is local or remote. If it is local the TransparentProxy calls the real object directly, otherwise it uses the information in the ObjRef to call the remote object, using the RealProxy.

System.Runtime.Remoting.Proxies.RealProxy = class ( System.Object )
public
  class procedure SetStubData( rp : RealProxy; stubData : Object ); 
  class function  GetStubData( rp : RealProxy ) : Object; 

  function  GetTransparentProxy(  ) : Object; virtual; 
  function  SupportsInterface( var iid : Guid ) : IntPtr; virtual; 
  procedure SetCOMIUnknown( i : IntPtr ); virtual; 
  function  GetCOMIUnknown( fIsMarshalled : Boolean ) : IntPtr; virtual; 
  procedure GetObjectData( info : SerializationInfo; context : StreamingContext ); virtual; 
  function  CreateObjRef( requestedType : Type ) : ObjRef; virtual; 
  function  Invoke( msg : IMessage ) : IMessage; virtual; abstract; 
  function  InitializeServerObject( ctorMsg : IConstructionCallMessage ) : IConstructionReturnMessage; 
  function  GetProxiedType(  ) : Type; 
end;

Configuration

All of the examples shown have hard-coded values that specify the URI, port number, and channel of the server. For trivial applications, hard coding is normal. However, it would make much more sense to place this configuration information into either the registry or an ini file. Built into the framework, is the ability to read remoting configuration entries directly from a properly formatted XML file. These configuration files are read using the RemotingConfiguration.Configure() method, which takes a filename as its only parameter.

Remoting configuration files are very flexible. Almost every aspect of remoting configuration can be addressed within it. For the purpose of this section, a simplified configuration file format is shown in the following example:



<configuration>
<system.runtime.remoting>
<application>
<lifetime />
<channels>
<channel />
</channels>
<service>
<wellknown />
<activated />
<service />
<client />
</application>
</system.runtime.remoting>
</configuration>

Lifetime Tag

The lifetime section allows for setting default values for the lease time, sponsorship timeout, renew on call time, and the lease manager poll time. An example of a lifetime section looks like this:




<lifetime leaseTime=”2M” sponsorshipTimeout=”1M”
renewOnCallTime=”5M” leaseManagerPollTime=”1M” />

In the example above, all of the time values are in minutes. These values can also be specified in days (D), hours (H), minutes (M), seconds (S) and milliseconds (MS).

Channels & Channel Tags

Since a remoting application can have one or more channels that it listens on, the <channels> tag contains a list of channels.

Each channel is configured with <channel> tag. Configuration options include a channel type, a display name (used only in the framework configuration tool), and a port. Look at the following configuration file fragment for an example of what a channel section might look like:




<channels>
<channel ref=”tcp” displayName=”BinaryChannel” port=”2013″ />
<channel ref=”http” displayName=”SoapChannel” port=”2014″ />
</channels>

Many additional attributes are available if an http channel is specified. The TCP channel has one additional attribute as well. In addition, other tags, like <serverProviders> and <clientProviders> are optional and can be specified for each <channel> tag. Detailed information is located in the .NET Framework documentation.

Service Tag

Server Activated Objects and Client Activated Objects are specified in the <service> section. Server-Activated Objects are specified with the <wellknown> tag and <activated> tag is used for identifying Client-Activated Objects. A <service> tag should only exist in a Server application.

The <wellknown> tag has attributes for identifying the type of the underlying remote object, whether it is a SingleCall or Singleton object, the URI of the object and an optional display name that is used only in the .NET framework configuration tool. One example might look like this:




<service>
<wellknown type=”MyNameSpace.ObjectName, AssemblyName”
mode=”SingelCall” objectUri=”MyCoolURI.soap” />
</service>

In contrast to the <wellknown> tag, the <activated> tag only has one attribute. This attribute is used to identify the type of the published class.




<service>
<activated type=”MyNameSpace.ClientActivatedObjectName, AssemblyName” />
</service>

Client Tag

Client applications have the ability to specify what server side objects they wish to use. By using the <client> tag, clients have the ability to configure both Server-Activated and Client-Activated objects. Just like the <service> tag, the <client> tag contains any number of <wellknown> and <activated> tags.

As before, the <wellknown> tag corresponds to Server-Activated objects. Required attributes include a URL (which includes the channel type, server name, port and object URI) and the type of the target object the client wishes to communicate, as seen in the example below:




<client>
<wellknown url=”http://localhost:2003/MyCoolURI.soap”
type=”MyNameSpace.ObjectName, AssemblyName” />
</client>

For Client-Activated objects, the <client> tag must have a URL attribute. In addition, the type of the remote object is an attribute for the <activated> tag, as shown in the example below:




<client url=”http://localhost:2003/MyCoolURI.soap”>
<activated type=”MyNameSpace.ObjectName, AssemblyName” />
</client>

Remoting configuration files are very flexible, as seen in the examples above. However, they do have some limitations. One limitation is that only classes can be specified in the type attribute with the <wellknown> and <activated> tags, which leaves out interfaces. Another limitation is that any object specified in the type must have a default (empty) constructor. So Singleton objects that use RemotingServices.Marshal() method to register objects with the framework must be done with code. These limitations do not automatically eliminate the possibility of using remoting configuration files. Instead, a scaled down version can be used to configure any lifetime management and channel configuration(s). See the source code for the distributed Prime Number application for an example of a partial configuration file.

Putting it all together

Included with the source code of this paper, is a distributed application that calculates prime numbers. The server application is a Windows.Forms application that waits for clients to connect and request work. A client application then runs through the range of numbers, looking for all primes within that range. When finished, the client application contacts the server application with the list of prime numbers found.

Additional Resources

Advanced .NET Remoting by Ingo Rammer, published by Apress. ISBN 1-59059-025-2
Ingo Rammer
MSDN: An Introduction to Microsoft .NET Remoting Framework
MSDN: .NET Remoting Architectural Assessment
MSDN: .NET Remoting Overview
MSDN: Format for .NET Remoting Configuration Files
MSDN: Design and Develop Seamless Distributed Applications for the Common Language Runtime
.NET Remoting Customization Made Easy: Custom Sinks
Using Remote Object Models in .NET

Additional online resources can be found at http://www.rick-ross.com

Source Code

Source Code

Leave a Reply