|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionThis article is a follow-up to my previous article entitled: "Simple but potentially useful example of .NET Remoting". In my earlier article, I expounded on basic codes necessary to startup a .NET remoting project. The example code of that article used Server Activated Remote Objects. In this article, we shift our attention to Client Activated Remote Objects which are different from and are often much more useful than server activated ones. In this article, I will be discussing the following concepts:
Example codes are again provided, and the functionality remains the same as the first article, which is to demonstrate the launching of a process on a remote machine. I provided such an example because such programs are practical and are often used in real systems where remote control is required. I do hope that the reader will use my example codes to experiment further and emerge a truly professional and viable solution. Differences Between Server Activated And Client Activated Remote ObjectsThe key difference between a Server Activated and a Client Activated Remote Object is that a Server Activated Object cannot hold state for a client. The exception is for "Singleton" Server Activated Objects. Let's explore this one step at a time. A Server Activated Object comes in two varieties: "Single Call" and "Singleton" modes. Look up the Basically, using the This being the case, it is not hard to imagine that such objects cannot hold any state for the client. Using the Such objects can, in a strained sense, hold state for a client. Some sophisticated mechanism must be employed to ensure that each client holds some private piece of data which must be maintained by the Singleton Remote Object. This is certainly possible but can be tedious to maintain. The other disadvantage is that this arrangement does not take into account the fact that there is no lifetime management system in place to ensure that should a client terminate suddenly, the server will be able to clean up resources set aside for the client. The .NET Remoting System provides remote object lifetime management support specially for Client Activated Remote Objects, which makes it much more attractive for usage. In comes Client Activated Objects. Note that these objects are genuine Remote Objects. They are instantiated on the server and not on the client side. Once created, they are private discrete objects which survive normally until no further reference to it is required by the client after which the object is destroyed as per normal by the Garbage Collector (we will examine remote object lifetimes later in this article). Each created object is not shared among clients. This makes Client Activated Objects truly hold state for clients. They can be used normally albeit they are created and maintained on a remote machine. This presents some useful advantages to our example program (ProcessActivator). With a Client Activated approach, a We will examine these when we study our source codes in the next few sections below. The New Example CodeThe New InterfacesThe basic functionality of our current example code remains the same as that in my first article: it starts up processes on a remote machine together with command line arguments. For the new code, I have created a new interface named " In the " As an enhancement to the example code, and as a way to show statefulness of a Client Activated Remote Object, I defined a new method named To accomplish this, the public interface IModuleEntry : ISerializable
{
IntPtr BaseAddress
{
get;
}
IntPtr EntryPointAddress
{
get;
}
string FileName
{
get;
}
int ModuleMemorySize
{
get;
}
string ModuleName
{
get;
}
}
public interface IProcessActivator_ClientActivated
{
bool Run(string strProgramName, string strArgumentString);
ArrayList GetModules();
}
As mentioned, the The
These are just some of the example properties which can be defined by The Server CodeThe Server Code has been enhanced to contain new implementation code as well as code required to host Client Activated Remote Objects. Let's start examining the code by first studying the The class ProcessActivator_ClientActivated : MarshalByRefObject,
IProcessActivator_ClientActivated.IProcessActivator_ClientActivated
{
private Process m_process = new Process();
public ProcessActivator_ClientActivated()
{
Console.WriteLine("ProcessActivator_ClientActivated constructor.");
}
...
...
...
}
The To help in solidifying the fact that a separate object is created for every client, I have added console output strings in the constructor code to show when the constructor is invoked. This is purely for testing and demonstration purposes. The reader can delete it with no consequence. The New Run() Method.public bool Run(string strProgramName, string strArgumentString)
{
bool bRet = false;
m_process.StartInfo = new ProcessStartInfo(strProgramName,
strArgumentString);
bRet = m_process.Start();
if (bRet)
{
Console.WriteLine("Program {0} started.", strProgramName);
m_process.WaitForInputIdle();
Console.WriteLine("Program {0} now in idle mode.",
strProgramName);
}
return bRet;
}
The Note the documentation for the
The call to This makes any later calls to The New GetModules() Methodpublic ArrayList GetModules()
{
ProcessModuleCollection pmc = m_process.Modules;
ArrayList ar_ret = new ArrayList();
int i = 0;
Console.WriteLine("GetModules() started."); /* For testing purposes. */
for (i = 0; i < pmc.Count; i++)
{
ar_ret.Add(
(object)(new ModuleEntry(
pmc[i].BaseAddress,
pmc[i].EntryPointAddress,
pmc[i].FileName,
pmc[i].ModuleMemorySize,
pmc[i].ModuleName
)
));
}
Console.WriteLine("GetModules() ended.");
/* For testing purposes. */
return ar_ret;
}
The As can be seen from the Each of these Now, you may be wondering: why take the trouble of defining the Why not simply make the The ModuleEntry Class[Serializable]
class ModuleEntry : IModuleEntry
{
private IntPtr m_ipBaseAddress;
private IntPtr m_ipEntryPointAddress;
private string m_strFileName = "";
private int m_iModuleMemorySize = 0;
private string m_strModuleName = "";
public ModuleEntry
(
IntPtr ipBaseAddress,
IntPtr ipEntryPointAddress,
string strFileName,
int iModuleMemorySize,
string strModuleName
)
{
m_ipBaseAddress = ipBaseAddress;
m_ipEntryPointAddress = ipEntryPointAddress;
m_strFileName = strFileName;
m_iModuleMemorySize = iModuleMemorySize;
m_strModuleName = strModuleName;
}
private ModuleEntry
(
SerializationInfo info,
StreamingContext context
)
{
m_ipBaseAddress = (IntPtr)info.GetValue("m_ipBaseAddress",
typeof(IntPtr));
m_ipEntryPointAddress = (IntPtr)info.GetValue("m_ipEntryPointAddress",
typeof(IntPtr));
m_strFileName = (string)info.GetValue("m_strFileName", typeof(string));
m_iModuleMemorySize = (int)info.GetValue("m_iModuleMemorySize",
typeof(int));
m_strModuleName = (string)info.GetValue("m_strModuleName", typeof(string));
}
...
...
...
}
Recall the original specifications of the public interface IModuleEntry : ISerializable
This means than any implementation of Now, because the I have tried to keep the Much of the remainder of the private IntPtr m_ipBaseAddress;
private IntPtr m_ipEntryPointAddress;
private string m_strFileName = "";
private int m_iModuleMemorySize = 0;
private string m_strModuleName = "";
I have also created a convenient constructor for The Main() Function And The Remote Object Hosting CodeThe static void Main(string[] args)
{
TcpServerChannel channel = new TcpServerChannel(9000);
HttpServerChannel http_channel = new HttpServerChannel(8000);
ChannelServices.RegisterChannel(channel);
ChannelServices.RegisterChannel(http_channel);
ActivatedServiceTypeEntry remObj =
new ActivatedServiceTypeEntry(typeof(ProcessActivator_ClientActivated));
RemotingConfiguration.ApplicationName = "ProcessActivator_ClientActivated";
RemotingConfiguration.RegisterActivatedServiceType(remObj);
Console.WriteLine("Press [ENTER] to exit.");
Console.ReadLine();
}
As was used in my first example code, I have defined two channels to be used to transfer any request for a Note the way that a Client Activated Remote Object Server performs hosting of the object. Previously, in our Server Activated Remote Object hosting code, we created a In our new Client Activated Remote Object hosting code, we create a Note also that the URI is not tied in with the type of the Remote Object in our construction of the WellKnownServiceTypeEntry remObj = new WellKnownServiceTypeEntry
(
typeof(ProcessActivator),
"ProcessActivator",
WellKnownObjectMode.SingleCall
);
Later, in the client code of the Server Activated Remote Object, when we perform the following: IProcessActivator.IProcessActivator process_activator =
(IProcessActivator.IProcessActivator)Activator.GetObject
(
typeof(IProcessActivator.IProcessActivator),
"tcp://localhost:9000/ProcessActivator"
);
the URI part of the URL parameter (i.e., We will examine the way clients connect to Client-Activated Remote Objects later in the section on Client Code. The remainder of the server code remains the same as in the first example code. That is, we simply wait out until the user presses the ENTER key, after which the entire server application ends. We shall examine the client code next. The Client CodeThe client code is presented below: class ProcessActivator_ClientActivated_Client
{
static RealProxy m_real_proxy = null;
static object m_transparent_proxy = null;
static ILease m_lease = null;
...
...
...
/* The main entry point for the application. */
[STAThread]
static void Main(string[] args)
{
try
{
UrlAttribute[] attr = { new
UrlAttribute(@"http://localhost:8000/ProcessActivator_ClientActivated") };
ObjectHandle object_handle = null;
IProcessActivator_ClientActivated.IProcessActivator_ClientActivated
process_activator = null;
ArrayList process_module_collection = null;
...
...
...
/* Create a client channel from which to receive the Remote Object. */
TcpClientChannel tcp_channel = new TcpClientChannel();
/* Register the channel. */
ChannelServices.RegisterChannel(tcp_channel);
/* During the following call to Activator.CreateInstance(), the constructor
of the class that implements the IProcessActivator_ClientActivated
interface will be invoked. */
object_handle = Activator.CreateInstance
(
"ProcessActivator_ClientActivated",
"ProcessActivator_ClientActivated.ProcessActivator_ClientActivated",
attr
);
/* Unwrap the delivered object and cast it to the
IProcessActivator_ClientActivated interface.*/
process_activator
= (IProcessActivator_ClientActivated.IProcessActivator_ClientActivated)
(object_handle.Unwrap());
/* We can now get hold of the ILease object within the newly created
Remote Object. */
m_real_proxy = RemotingServices.GetRealProxy(process_activator);
m_transparent_proxy = m_real_proxy.GetTransparentProxy();
m_lease
= (ILease)(((MarshalByRefObject)m_transparent_proxy).GetLifetimeService());
...
...
...
/* Start up a process in the remote machine site. */
process_activator.Run("notepad.exe", "");
/* Sleep for 270 seconds (4 and a half minutes). */
/* The timer will continue to monitor the Remote Object lease. */
Thread.Sleep(270 * 1000);
/* After the sleeping period, we attempt to call a remote method. */
process_module_collection = process_activator.GetModules();
/* Perform processing of the returned value from GetModules(). */
for (int i = 0; i < process_module_collection.Count; i++)
{
IModuleEntry me = (IModuleEntry)process_module_collection[i];
Console.WriteLine(me.FileName);
}
...
...
...
}
catch(FileNotFoundException ex)
{
Console.WriteLine(ex.FileName);
}
catch(TargetInvocationException tie)
{
Console.WriteLine(tie.Message);
}
return;
}
}
In the above code snippet, I have deliberately filtered out some codes for clarity. Although the client code does look a little more complicated compared with the client code of the server activated remote object (in my previous example), please note that half of the client source codes pertain to remote object lifetime management and a demonstration of this management, the other part pertains to actual remote object creation. In a nutshell, for Client Activated Remote Object invocation, clients call the Take note that not all the overloaded Such Activation Attributes are best described by the MSDN documentation as: "an array of one or more attributes that can participate in activation". Among the three versions of public static ObjectHandle CreateInstance(
string assemblyName,
string typeName,
object[] activationAttributes
);
We invoke this function in the following way: UrlAttribute[] attr =
{ new UrlAttribute(@"http://localhost:8000/ProcessActivator_ClientActivated") };
object_handle = Activator.CreateInstance
(
"ProcessActivator_ClientActivated",
"ProcessActivator_ClientActivated.ProcessActivator_ClientActivated",
attr
);
In our code, we use " Note well that whatever assembly name we specified as the first parameter, the .NET framework must be able to locate it. Hence, because I have specified this parameter to be " While it did not bother me that the actual typename of the remote object " The parameter " http://localhost:8000/ProcessActivator_ClientActivated. The HTTP protocol, the host address (localhost in our example code), and the port number (8000) being the channel used, and then name of the remote object being " The next thing to note about Client Activated Remote Object creation is the fact that an The After unwrapping, we will still need to cast the returned value (which is typed as an process_activator =
(IProcessActivator_ClientActivated.IProcessActivator_ClientActivated)
(object_handle.Unwrap());
We can next call any of the exposed methods of the Some Comments On Activator.CreateInstance()Recall the This is inline with the fact that the URI as specified in the server's call to The same In my opinion, this design also goes in line with the design of object creation and interface implementation in C++ and COM. In C++, an interface pointer is instantiated to a concrete implementation as follows: IMyInterface* pIMyInterface = new CMyInterfaceImpl();
Note the reference to an actual implementation class name ( In COM, a similar style is used : ::CoCreateInstance(CLSID_MyClassID, NULL, CLSCTX_INPROC_SERVER ,
IID_IMyInterface, (LPVOID *)&pIMyInterface);
The class ID of the COM object which implements the This style of indicating the name of the actual concrete class (or type) is continued in In C++, COM, and .NET Remoting, the actual implementation identifier (e.g., class name This ensures the possibility of late binding and configurability. However, it still troubled me greatly that the actual assembly itself must be found locally by the .NET Framework in order for After some thoughts and discussing with fellow colleagues, I emerged some theories on this: The fact that the name of the assembly is to be specified indicates one of the original intended usage of Remoting, i.e. that it is to be confined to closed LAN/intranet-based distributed systems. Remoting objects are not intended for use by "the outside world". Only Web Services are exposed to the outside world. To use a remote object, you must have in your local client system a copy of the actual assembly which is used by the server. I would really rather have it that the actual assembly be un-required, or that a path to a remotely located assembly be specifiable. But both are not possible as far as I know at this time. What usage does the .NET Framework have on this assembly I really do not know at this time. If there are any readers out there who understand this usage, please share with us :-) Object Marshalling, Parameterizing and ReturningNote that the parameter types of remote method calls are not limited to basic data types. Method return values are similarly not confined as can be seen by the fact that we can return an In the context of .NET Remoting, we have to differentiate between two types of classes:
With the above definitions, I hope things have become clearer and we see where things fit in. In our example code, the Our Remote Object LifetimesNo article on Client Activated Remote Objects is complete without a discussion on Remote Object Lifetime Management. Why is this topic important? Well, how does a client and a server know if the other side is no longer available? If a server should die on a client, as soon as the client makes a method call on the remote object, an exception of type The situation can be more severe for a server. If a client should crash, the server usually has no way of detecting this (unless some predefined mechanism of mutual life detection, e.g., pinging, has been put in place). This could lead to the piling up of unused and unreleased resources. For Remote Object Lifetime Management, the .NET Framework provides the Leasing Distributed Garbage Collector (LDGC). For every client activated remote object referenced outside the application domain in which it is created, something known as a lease is created. This lease has a lease time, and when it expires, the remote object gets disconnected and is garbage collected. In the context of lifetime management, a few terms must be introduced:
We are definitely concerned over lease times. Although it is possible to configure it, we cannot have a situation in which the remote object never expires its lease. This is bad design, and will open up the chance for the resource leakage issues discussed earlier. If we need a remote object for longer than the default lease time, we cannot have a situation in which we simply let a remote object expire by itself and to re-create the object thereafter. If this is what you want, by all means let it happen in your project. However, I assume that the common requirement is to maintain the lifetime of remote objects until they are specifically no longer needed by the application. Concerning lease renewal, there are three ways to accomplish it:
Of the three methods, implicit renewal is not suitable. We will run the risk of the remote object being lease expired when we call a remote method. Sponsorship seemed like an attractive choice and it is, but it is also a little complicated to implement. .NET security features must be used. Sponsorship, with its complex association with security issues, is not attractive to us also because this article is meant for the beginner level developers. The last and only choice for us is thus the explicit renewal method. Our client code uses the First and foremost, we must obtain the m_real_proxy = RemotingServices.GetRealProxy(process_activator);
m_transparent_proxy = m_real_proxy.GetTransparentProxy();
m_lease
= (ILease)(((MarshalByRefObject)m_transparent_proxy).GetLifetimeService());
To facilitate the getting of the static RealProxy m_real_proxy = null;
static object m_transparent_proxy = null;
static ILease m_lease = null;
The Back to lease renewals, in order that we renew the remote object lease in a timely fashion, we will need to create a timer for our client application. This is why we defined the System.Timers.Timer aTimer = new System.Timers.Timer(30000);
We also set the timer event handler to our user-defined aTimer.Elapsed += new ElapsedEventHandler(OnTimedEvent);
aTimer.AutoReset = true;
aTimer.Enabled = false;
/*Disable the timer first until m_lease has been properly initialized.*/
We need to disable the /* Only now do we turn on the timer...*/
aTimer.Enabled = true;
Let's now examine our /* When the timer event is raised, we perform a check on the current lease time
of the Remote Object. */
private static void OnTimedEvent(object source, ElapsedEventArgs e)
{
/* Perform something only when the ILease object is not null. */
if (m_lease != null)
{
TimeSpan time_span_current_lease = m_lease.CurrentLeaseTime;
double dbTime_current_lease = time_span_current_lease.TotalSeconds;
LeaseState lease_state = m_lease.CurrentState;
Console.WriteLine("Current Lease Time : {0}.", dbTime_current_lease);
if ((lease_state == LeaseState.Active) && (dbTime_current_lease <= 60))
{
/* Once the lease time has fallen below 60 seconds,
we seek to renew the lease. */
TimeSpan time_span_renew = m_lease.Renew(TimeSpan.FromSeconds(300));
double dbTimeRenew = time_span_renew.TotalSeconds;
Console.WriteLine("Renewed Lease Time To : {0}.", dbTimeRenew);
}
}
}
Here, we first check to see if the If the lease's state is active and the lease time has dropped down to less than or equal to 60 seconds, we perform a lease renewal (via the In our example client code, I have attempted to show this lease renewal at work by causing the main thread to sleep for 4.5 minutes (270 seconds) after the Before the I deliberately repeated the call to In ConclusionOnce again, I really hope the reader has benefited from this article and its example codes. Client Activated Remote Objects are certainly more complex to create and use as compared with Server Activated ones. I do hope that my article has served well in explaining the internal intricacies, and that my example code will provide a good startup point for projects. I certainly do want to touch on proxies in the near future and hope to write an article on this. I have not touched on configuration files yet and will also be looking into this.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||