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

Simple but potentially useful example of .NET Remoting

Rate me:
Please Sign up or sign in to vote.
4.82/5 (28 votes)
21 Nov 2004CPOL10 min read 222.7K   3.1K   120   29
Introduces .NET remoting via a simple but potentially useful example.

Introduction

This article is inspired by Nishant S's article entitled "Absolute beginner's introduction to remoting" :-

Nishant's article presents a very simple example use case of .NET Remoting. Having thoroughly enjoyed Nishant's article, I decided to provide to all readers a -slightly- more advanced example. Note that, like Nishant, my target audience remains the "beginners" level developers.

My example program is entitled "ProcessActivator". I provide a server program which exposes an object named "ProcessActivator". This server object is an implementation of an interface named "IProcessActivator ". This implementation defines a single method named "Run" which, when invoked by a client, will cause the server to start up a process (on the machine in which the server is running).

Without further ado, let's start examining our example code...

The Interface

Like Nishant, I have defined the interface "IProcessActivator" inside a C# DLL project. This DLL is what I like to refer to as an "empty interface dll", i.e. it only defines and exposes interface(s) but does not provide any implementation code.

Later we will derive our C# remote server class from this same interface. This DLL file will also be copied to the client machine in order for the client to reference it in order to compile successfully.

Let's have a look at the "IProcessActivator" interface :

C#
public interface IProcessActivator
{
    bool Run(string strProgramName, string strArgumentString);
}

This interface defines a method named "Run". The specification for Run() is that it should cause the object (which implements IProcessActivator) to startup a process (identified by the "strProgramName" parameter with arguments specified by "strArgumentString ".

You can find the source codes to this "empty interface dll" in the "IProcessActivator " folder (contained in the source codes zip file accompanying this article). There is a C# solution file (IProcessActivator.sln) located in this folder. The project settings must be such that this project file will be compiled into a DLL (albeit without any implementation).

Once this DLL has been successfully compiled, our client and server programs can reference this DLL for compilation purposes.

The Remote Server Object

The remote server class that we create will be derived from "IProcessActivator" of course. This is so that our server class will implement this interface. Our remote server class must also be derived from the MarshalByRefObject class.

MarshalByRefObject is the base class for objects that can be created by a remote client and then transported across application domain boundaries to the remote client. Communications between the remote client and the MarshalByRefObject is achieved by exchanging messages using a proxy.

It is the proxy that makes the object "marshalled by reference". By being "marshalled by reference", the remote server object becomes "stateful" to the client application.

The first time a (remote) client application accesses the MarshalByRefObject , the proxy is passed to the remote client application. The client uses this proxy to make method calls on the remote object.

These method calls and their parameters are actually marshalled across application domain boundaries to the remote server object itself. Much of it is reminiscent of COM's way of invoking remote server function calls.

Let's look at our server code :

C#
using System;
using System.Diagnostics;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using IProcessActivator;
namespace ProcessActivator
{
/// <summary>
/// Summary description for ProcessActivator.
/// </summary>
public class ProcessActivator : MarshalByRefObject, 
  IProcessActivator.IProcessActivator
{
public ProcessActivator()
{
//
// TODO: Add constructor logic here
//
}
public bool Run(string strProgramName, string strArgumentString)
{
Process.Start(strProgramName, strArgumentString);
return false;
}
public static void Main()
{
   TcpServerChannel channel = new TcpServerChannel(9000);
   ChannelServices.RegisterChannel(channel);
   WellKnownServiceTypeEntry remObj = new WellKnownServiceTypeEntry
   (
   typeof(ProcessActivator),
   "ProcessActivator",
   WellKnownObjectMode.SingleCall
   );
   RemotingConfiguration.RegisterWellKnownServiceType(remObj);
   Console.WriteLine("Press [ENTER] to exit.");
   Console.ReadLine();
}
}
}

The ProcessActivator class' implementation of the Run() method uses the "Process" class which provides access to local and remote processes and enables an application to start and stop local system processes.

The Process class is very useful for starting, stopping, controlling, and monitoring applications. Using the Process class, an application can also obtain a list of the processes (including system processes) that are currently running. Various information, including threads, loaded modules (.dll and .exe files), and performance data (e.g. memory usage) are also available.

This opens up a lot of potential enhancements for the IProcessActivator interface. We can enhance IProcessActivator to include methods (e.g. GetProcesses()) that return an array of information on processes currently running on a machine.

The reader is encouraged to experiment with the attached source codes to achieve this.

The Main() function implements typical code for registering Remoting Objects. The typical code uses TcpServerChannel, ChannelServices, WellKnownServiceTypeEntry and RemotingConfiguration.

I must admit that when I first studied this code (which I copied from one of the many example codes available in MSDN), I was most puzzled by the lack of obvious connection between the call to ChannelServices.RegisterChannel() and the call to RemotingConfiguration.RegisterWellKnownServiceType().

This was so until I further studied the ChannelServices documentation. My findings are analysed in the next section.

An Analysis Of Channel Services And RemotingConfiguration

Note the documentation for the ChannelServices class :

"(ChannelServices) Provides static methods to aid with remoting channel registration, resolution, and URL discovery."

And the documentation for the ChannelServices.RegisterChannel() method states :

"You cannot register two channels with the same name in an AppDomain. By default, the name of an HttpChannel is "http", and the name of a TcpChannel is "tcp". Therefore, if you want to register two channels of the same type, you must specify a different name for one of them through configuration properties."

Here is my take for the connection between ChannelServices and

RemotingConfiguration
:

ChannelServices are intrinsically used in a server application for exposing Remoting Objects to clients.

The WellKnownServiceTypeEntry and the RemotingConfiguration classes are used to generically register an object TYPE on the server and to expose this type to all clients. The registration is done by the call to

RemotingConfiguration.RegisterWellKnownServiceType()
function.

In order for clients to connect with this type of object, it must know the URL of the object. This URL will include the URI of the object. This is where the second parameter to the WellKnownServiceTypeEntry constructor comes in. It specifies the URI of the object.

In our example code, this second parameter is "ProcessActivator" and clients must know this piece of information. This is why it is "well-known".

Note also that this well-known service type registration is possible on condition that the server application first create a channel from which clients can connect to. This is done by the call to ChannelServices.RegisterChannel() method.

Although there is no direct connection between ChannelServices and RemotingConfiguration, they are, nevertheless connected (albeit internally) by the .NET framework.

That the connection between ChannelServices and RemotingConfiguration is totally internal can be appreciated by the fact that a server can define and register as many types of channels as it wishes and these need not be of any concern when we call RemotingConfiguration.RegisterWellKnownServiceType().

As an example, we can code our Main() function as follows :

C#
TcpServerChannel channel = new TcpServerChannel(9000);
HttpServerChannel http_channel = new HttpServerChannel(8000);
ChannelServices.RegisterChannel(channel);
ChannelServices.RegisterChannel(http_channel);
WellKnownServiceTypeEntry remObj = new WellKnownServiceTypeEntry
(
typeof(ProcessActivator),
"ProcessActivator",
WellKnownObjectMode.SingleCall
);
RemotingConfiguration.RegisterWellKnownServiceType(remObj);
Console.WriteLine("Press [ENTER] to exit.");
Console.ReadLine();

The above code demonstrates the registering of a TcpServerChannel as well as a HttpServerChannel. With this enhancement, a client application can reach the Remoting object either via TCP/IP (using port number 9000) or via HTTP (using port number 8000). Either way, our WellKnownServiceTypeEntry object remains passive and need not be created nor registered with any indication of the specific channel type.

When we explore our client code next, we will present an example where the client makes good use of the multiply registered channels of the server and connects with the it via HTTP.

The Client Code

Let's now examine our client code :

C#
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using IProcessActivator;
namespace ProcessActivatorClient
{
class ProcessActivatorClient
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
   //
   // TODO: Add code to start application here
   //
   TcpClientChannel channel = new TcpClientChannel();
   ChannelServices.RegisterChannel(channel);
   IProcessActivator.IProcessActivator process_activator = 
      (IProcessActivator.IProcessActivator)Activator.GetObject
   (
   typeof(IProcessActivator.IProcessActivator),
   "tcp://localhost:9000/ProcessActivator"
   );
   process_activator.Run("IExplore.exe",
      @"http://www.codeproject.com");
}
}
}

Our client code also uses "typical" code to connect to the remote object. This typical code involves the use of the TcpClientChannel,

ChannelServices
and the Activator classes.

Once again, the connection between ChannelServices and Activator seem to be implicit and handled internally by the .NET framework.

The recurring pattern seem to be that a client channel (used by the client to connect with the remote object server) must be registered by the

ChannelServices
class before Activator can succeed.

Recall that our server code can register more than one type of channel. If we had registered both a TCP/IP channel as well as a HTTP channel, our client code can be done this way :

C#
HttpClientChannel http_channel = new HttpClientChannel();
ChannelServices.RegisterChannel(http_channel);
IProcessActivator.IProcessActivator process_activator = 
 (IProcessActivator.IProcessActivator)Activator.GetObject
(
typeof(IProcessActivator.IProcessActivator),
"http://localhost:8000/ProcessActivator"
);
process_activator.Run("IExplore.exe", @http://www.codeproject.com);

In the course of my learning experience with .NET Remoting, I noted two important points about the Activator.GetObject() method. Both are significant findings. The first is a little odd (could signal a bug in .NET) but is not disastrous in my opinion. The second indicates similarities between .NET Remoting, COM and the Object-Oriented features of C++ in general which gives me a surge of confidence that the COM and C++ stuff I learnt will be re-usable in .NET.

The First Parameter To Activator.GetObject()

In this section, I'll discuss my first observation about the GetObject() method. The first parameter to GetObject() is documented in MSDN as :

"The type of the well-known object to which you want to connect."

In my client code, I used :

typeof(IProcessActivator.IProcessActivator)

When I first experimented with the client code, I had hoped that I can supply another interface type (provided of course that the Remoting Object implements that interface). To cut a long story short, I found that, in general, this parameter seem to make no difference whatsoever no matter what I supplied (for the first parameter) as the type I want from the Remoting Object. This seem to be the case as long as the supplied type is found in the Remoting server.

What does make a difference is the URI of the Remoting Object that I supply as part of the URL which is supplied as the second parameter. Here, the URI must be the well-known name of the actual object that you want the server to create and return.

The fact that the first parameter seemed insignificant may be some kind of design oversight by the Microsoft Engineers but I could well be wrong. If any readers out there understand the significance of the first parameter in the context I just mentioned, please share with us :-)

The Similarities Between Activator.GetObject(), COM's CoCreateInstance() and C++'s "new" Keyword

In this section, I'll discuss the second observation I made concerning the Activator.GetObject() method. Note the entire call to Activator.GetObject() :

C#
IProcessActivator.IProcessActivator process_activator = 
  (IProcessActivator.IProcessActivator)Activator.GetObject 
  (typeof(IProcessActivator.IProcessActivator),""); 

This piece of code indicates to the .NET Framework to create an object of "class" "ProcessActivator" and then return to me the IProcessActivator interface of that object.

This same style of object creation and interfacing return is evident in COM's CoCreateInstance() API. For example :

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

Here, we tell the COM sub-system to create a COM object with class ID CLSID_MyClassID and then to return to us a pointer to that COM object's IMyInterface implementation. Notice the same style of object creation and interface pointer return.

Finally, to present the idea solidly into the reader's mind, note C++'s style of object creation and interface pointer return in the following example code :

IMyInterface* pIMyInterface = new CMainObject();

The above code assumes, of course, that the C++ class CMainObject is derived from IMyInterface. Here, we instantiate a new instance of the CMainObject class and a pointer to the CMainObject's IMyInterface vtable is returned to pIMyInterface.

Notice the same general approach used in object creation and interface pointer return in C++. It is the same as that in .NET Remoting and COM.

This same trend of object creation is also evident in Visual Basic.

In Conclusion

I certainly hope that my article will be of benefit to other readers. I have attempted to present my own research work and share with all my findings.

I also hope that my example code will be of practical use to projects that require some remote controlling of machines. Such remote controlling is not uncommon and is particularly useful in automated systems.

Drop a message to me anytime you have a good suggestion on the example code or on Remoting in general.

License

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


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

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

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

Comments and Discussions

 
GeneralInvoke a new Form Pin
dave_270511-Aug-05 0:44
dave_270511-Aug-05 0:44 
GeneralForms Question Pin
Tommy Visic16-Mar-05 14:13
Tommy Visic16-Mar-05 14:13 
AnswerRe: Forms Question Pin
Emre Guldogan3-Sep-10 10:25
Emre Guldogan3-Sep-10 10:25 
GeneralNo connection could be made.... Pin
Lam Ngo25-Feb-05 3:53
Lam Ngo25-Feb-05 3:53 
GeneralRe: No connection could be made.... Pin
Lim Bio Liong25-Feb-05 4:14
Lim Bio Liong25-Feb-05 4:14 
GeneralRe: No connection could be made.... Pin
cudp27-Sep-06 3:05
cudp27-Sep-06 3:05 
GeneralRe: No connection could be made.... Pin
cudp27-Sep-06 4:32
cudp27-Sep-06 4:32 
GeneralRe: No connection could be made.... Pin
Emre Guldogan3-Sep-10 10:45
Emre Guldogan3-Sep-10 10:45 
QuestionThe server is a window service : how ? Pin
MisterG2-Dec-04 1:29
MisterG2-Dec-04 1:29 
AnswerRe: The server is a window service : how ? Pin
Lim Bio Liong2-Dec-04 5:12
Lim Bio Liong2-Dec-04 5:12 
GeneralRe: The server is a window service : how ? Pin
MisterG2-Dec-04 5:30
MisterG2-Dec-04 5:30 
AnswerRe: The server is a window service : how ? Pin
KVenom5-Jan-09 6:51
KVenom5-Jan-09 6:51 
AnswerRe: The server is a window service : how ? Pin
Hardy Wang3-Dec-04 7:27
Hardy Wang3-Dec-04 7:27 
GeneralRe: The server is a window service : how ? Pin
Lim Bio Liong3-Dec-04 13:42
Lim Bio Liong3-Dec-04 13:42 
GeneralTcpClientChannel Pin
Rouzbeh_d21-Nov-04 5:37
Rouzbeh_d21-Nov-04 5:37 
GeneralRe: TcpClientChannel Pin
Lim Bio Liong21-Nov-04 21:52
Lim Bio Liong21-Nov-04 21:52 
GeneralRe: TcpClientChannel Pin
atuloak14-Apr-05 4:55
atuloak14-Apr-05 4:55 
GeneralRe: TcpClientChannel Pin
patel mayank24-Apr-07 20:16
patel mayank24-Apr-07 20:16 
GeneralClarity Pin
Anonymous17-Nov-04 10:57
Anonymous17-Nov-04 10:57 
GeneralRe: Clarity Pin
Lim Bio Liong17-Nov-04 20:13
Lim Bio Liong17-Nov-04 20:13 
Hello,

IProcessActivator.IProcessActivator -is- in the [namespace].[class] format. The namespace is IProcessActivator and the interface name is also IProcessActivator.

Look at the IProcessActivator.cs file and you will see the following :

using System;

namespace IProcessActivator
{
/// <summary>
/// Summary description for interface IProcessActivator.
/// </summary>
public interface IProcessActivator
{
bool Run(string strProgramName, string strArgumentString);
}
}

Notice that the namespace name and the interface name are the same.

I must agree that this can potentially cause confusion even to the C# compiler. Notice in the server source codes :

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

namespace ProcessActivator
{
/// <summary>
/// Summary description for ProcessActivator.
/// </summary>
public class ProcessActivator : MarshalByRefObject,
IProcessActivator.IProcessActivator
{
...
...
...


Notice that I also had to use "IProcessActivator.IProcessActivator" as part of the derivation for the class ProcessActivator despite the fact that I have already specified the I'm using the IProcessActivator namespace :

using IProcessActivator;

The same compilation problem is faced in the client code.

If I had coded the namespace for the IProcessActivator interface as IProcessActivatorNamespace, for example :

using System;

namespace IProcessActivatorInterface
{
/// <summary>
/// Summary description for interface IProcessActivator.
/// </summary>
public interface IProcessActivator
{
bool Run(string strProgramName, string strArgumentString);
}
}

... this problem would not have occurred. I would've been able to code the client and server codes using the "IProcessActivatorInterface" namespace and I would have no need to specify the fully qualified interface name "IProcessActivatorInterface.IProcessActivator".

Thanks for reporting this problem.

Best Regards,
Bio.





GeneralRe: Clarity Pin
Anonymous19-Nov-04 3:54
Anonymous19-Nov-04 3:54 
GeneralRe: Clarity Pin
Anonymous19-Nov-04 3:55
Anonymous19-Nov-04 3:55 
GeneralRe: Clarity Pin
Anonymous19-Nov-04 6:37
Anonymous19-Nov-04 6:37 
GeneralExcellent Starting Point Pin
Ohmigosh11-Nov-04 17:19
Ohmigosh11-Nov-04 17:19 
GeneralRe: Excellent Starting Point Pin
Lim Bio Liong11-Nov-04 19:18
Lim Bio Liong11-Nov-04 19:18 

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

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