This is an in-depth look at Remoting infrastructures in the Common Language Runtime and the .NET Framework. This study provides detailed information of the implementation of Remoting. I have given only a small description about each topic, or else it will eat up very huge numbers of pages. And since I am not that much good in graphics, I've grabbed the required images from other sources to depict some situations in this document.
.NET Remoting enables you to build widely distributed applications easily, whether the application components are all on one computer or spread out across the entire world. You can build client applications that use objects in other processes on the same computer or on any other computer that is reachable over its network. In short, .NET Remoting provides an abstract approach to inter-process communication
Before getting deep into Remoting functionalities, it would be good if I explain about some other terms in .NET.
An application domain is a partition in the Operating System process where one or more applications reside, or in other words, application domains are the isolated environments where managed applications execute. .NET has the concept of Appdomains just to decrease the overhead of Win32 process isolation and memory handling tasks. You can create as many Appdomains as you need, and there will be a default "Appdomain". Applications running under CLR (Managed environment) can be accessed using the "Appdomain.CurrentDomain
" property.
Thus, Appdomains keep the required assemblies separate within the same Win32 process. Objects in the same application domain communicate directly. Objects in different application domains communicate either by transporting copies of objects across application domain boundaries, or by using a proxy to exchange messages. This is accomplished by the process of marshalling and serialization.
Marshalling
Marshalling and unmarshalling refer to packing and unpacking of parameters and return-values of a particular method call. There are two types of marshalling available in .NET. Marshal by reference and Marshal by value. A simple synonym for marshalling would be "packaging data for transfer".
Marshal by reference:
The first time an application in a remote application domain accesses a MarshalByRefObject
, a memory reference proxy is passed to the remote application.
A proxy is an object that exists and acts locally on behalf of a remote object. The proxy looks like and accepts calls as if it where the "real" object.
The proxy exposes the same set of interfaces that the original object exposes. However, the proxy does not process the calls locally, but it holds the address or path to the actual objects and thus sends them to the remote object it represents.
Subsequent calls on the proxy are marshalled back to the object residing in the local application domain. The same process is maintained regardless of whether the remote application resides in the same physical machine, in same process, or a different physical machine.
This process of communicating between Appdomains is called Remoting, and we can come to this point later on.
In order to make an object a MarshalByRef
component, you need to inherit it from the MarshalByRefObject
class which internally handles the marshalling and lifetime manipulation task of that object. MarshalByRefObject
is the base class for objects that communicate across application domain boundaries by exchanging messages using a proxy.
Marshal by value:
Objects that do not inherit from MarshalByRefObject
are implicitly marshal by value. When a remote application references a marshal by value object, a copy of the object is passed across application domain boundaries.
The basic difference is that a MarshalByRef
component is not usable outside the application domain where they were created where a MarshalByvalue
component is usable just because it is having its state packed along.
Note: - Static variables and static methods cannot be proxied; instead, it acts as a marshal by value, sending a copy of the whole stuff to the client, and will be executed in the client's context. |
Serialization is the process of converting the state of an object into a form that can be persisted or transported. The complement of serialization is deserialization, which converts a stream into an object. Together, these processes allow data to be easily stored and transferred.
The .NET Framework features two serializing technologies:
Binary serialization preserves type fidelity, which is useful for preserving the state of an object between different invocations of an application. For example, you can share an object between different applications by serializing it to the Clipboard. You can serialize an object to a stream, to a disk, to memory, over the network, and so forth. Remoting uses serialization to pass objects "by value" from one computer or application domain to another.
XML serialization serializes only public properties and fields, and does not preserve type fidelity. This is useful when you want to provide or consume data without restricting the application that uses the data. Because XML is an open standard, it is an attractive choice for sharing data across the Web. SOAP is likewise an open standard, which makes it an attractive choice.
Remoting ...
Now that you got some knowledge about "Appdomains" and all, we can continue with Remoting. As explained before, Remoting provides an abstract approach to inter-process communication. In either case, if your remote objects reside in entirely different or the same physical machine, you need to host that application using some hosts which describe the type, location, and other simple metadata about that object.
You must provide the following information to the remoting system to make your type remotable:
- The type of activation required for your type.
- The complete metadata that describes your type.
- The channel registered to handle requests for your type.
- The URL that uniquely identifies the object of that type. In the case of server activation, this means a Uniform Resource Identifier (URI) that is unique to that type. In the case of client activation, a URL that is unique to that instance will be assigned.
You can provide this information either programmatically, or through application configuration files so that you can change the configuration in future to point to any of the other objects or machine without touching or recompiling the code.
The methods that will be called from the client are implemented in a remote object class. In the figure below, we can see an instance of this class as the Remote Object. Because this remote object runs inside a process that is different from the client process – usually also on a different system – the client can't call it directly. Instead, the client uses a proxy. For the client, the proxy looks like the real object with the same public methods. When the methods of the proxy are called, messages will be created. These are serialized using a formatter class, and are sent into a client channel. The client channel communicates with the server part of the channel to transfer the message across the network. The server channel uses a formatter to deserialize the message so that the methods can be dispatched to the remote object:
Activation – Remote objects can be activated using the new
operator. Of course, there are other ways to activate remote objects like Activator.GetObject()
, Activator.Createinstance()
etc., which will vary according to the activation type.
There are two types, mainly, server activated types and client activated types.
Server-activated types
Server-activated objects are objects whose lifetimes are directly controlled by the server. The server application domain creates these objects only when the client makes a method call on the object rather than when the client calls new
or Activator.GetObject
.
There are two activation modes (or WellKnownObjectMode
values) for server-activated objects: Singleton and SingleCall.
Singleton types, as you know, never have more than one instance at any point of time. If an instance exists, all client requests are serviced by that instance. If an instance does not exist, the server creates an instance, and all subsequent client requests will be serviced by that instance.
SingleCall types always have one instance per client request. The next method invocation will be serviced by a different server instance, even if the previous instance has not expired in the system. SingleCall types do not participate in the lifetime lease system.
To create an instance of a server-activated type, clients either configure their application programmatically, or use a configuration file and then pass the remote object's configuration (like type and object URI) in a call to Activator.GetObject
.
Client-activated types
Client-activated objects are objects whose lifetimes are controlled by the calling application domain, just as they would be if the object were local to the client. Client-activated objects use lifetime leases to determine how long they should continue to exist.
Before telling about lease, I would like to tell something about the garbage collection in .NET.
The .NET Framework's garbage collector manages the allocation and release of memory for your application. Each time you use the new
operator (or Activator.CreateInstance
) to create an object, the runtime allocates memory for the object from the managed heap. As long as address space is available in the managed heap, the runtime continues to allocate space for new objects. However, memory is not infinite. Eventually, the garbage collector must perform a collection in order to free some memory. The garbage collector's optimizing engine determines the best time to perform a collection, based upon the allocations being made. When the garbage collector performs a collection, it checks for objects in the managed heap that are no longer being used by the application (the objects that have been marked as unreachable in its object header) and performs the necessary operations to reclaim their memory.
In our scenario, the client has reference to a proxy of the remote object in the heap, and there should be a way by which the server can determine whether the reference to this server object is still needed for the application. We achieve this particular function by maintaining a leased lifetime for each server activated singleton and client activated objects.
In short, leasing and sponsorship is the solution for managing the lifecycle of a remote object in .NET. Each object has a lease that prevents the local garbage collector from destroying it, and most distributed applications rely upon leasing. A sponsor is a third party that the .NET Remoting Framework consults when a lease expires, giving that party an opportunity to renew the lease.
Whenever an MBR (Marshal-by-reference) object is remoted outside an application domain, a lifetime lease is created for that object. Each application domain contains a lease manager that is responsible for administering leases in its domain. A server object can have many sponsors associated with its lease. The lease manager keeps a list of all the sponsors. The lease manager periodically examines all leases for expired lease times. If a lease has expired, the lease manager sends a request to its list of sponsors for that object, and queries whether any of them wants to renew the lease. If no sponsor renews the lease, the lease manager removes the lease, the object is deleted, and its memory is reclaimed by garbage collection. An object's lifetime, then, can be much longer than its lifetime lease, if renewed more than once by a sponsor, or by continually being called by clients.
The default lease time for an object is 5 minutes, which is configured by overriding MarshalByRefObject.InitializeLifetimeService
and specifying our own TimeSpan
object as required.
public override ILease InitializeLifetimeService ()
{
ILease lease = (ILease) base.InitializeLifetimeService ();
if (lease.CurrentState == LeaseState.Initial)
{
lease.InitialLeaseTime = TimeSpan.FromMinutes (1);
lease.SponsorshipTimeout = TimeSpan.FromMinutes (2);
lease.RenewOnCallTime = TimeSpan.FromSeconds (2);
}
return lease;
}
If the lease is about to expire, the .NET Remoting Framework will automatically extend the lease on every call by the value set in the MarshalByRefObject.RenewOnCallTime
property. The default call time renewal is two minutes. The value of the current lease time (the time the object has to live unless the lease is extended) is a product of the lease time (the initial lease time for the first time) and renew on call time, according to the formula shown in the following line of code:
Current lease time = MAX (lease time - expired time, RenewOnCallTime)
The RenewOnCallTime
will have an effect only if it is greater than the lease time minus the expired time. In that case, the expired time property is reset, and the lease time is set to the RenewOnCallTime
. The result is that even if an object is very busy, its lease time does not grow in proportion to the amount of traffic it has. Even if the object has a temporary spike in its load, its lease will expire after some quiet time.
If you know that a particular object will not be called for a longer period of time than the default timeouts, but you do not want to change the mechanism globally, you can create a "sponsor" for that object and register it on the object's ILease
interface that you can obtain through the GetLifetimeService()
method that every AppDomain bound MarshalByRefObject
derived class and every contextful class inherits.
If an object's lease expires, the object is marked for garbage collection, and all existing remote references to this object become invalid.
The sponsor must implement the ISponsor
interface, defined as:
public class ClientSponsor: MarshalByRefObject,ISponsor
{
public TimeSpan Renewal(ILease lease)
{
Debug.Assert(lease.CurrentState == LeaseState.Active);
return TimeSpan.FromMinutes(5);
}
}
After this, we need to create the instance of the remotable object and register it with a sponsor, as follows:
ISponsor sponsor = new ClientSponsor ();
MyClass obj = new MyClass();
ILease lease = (ILease)RemotingServices.GetLifetimeService(obj);
lease.Register(sponsor);
The lease manager calls ISponsor
's single method, Renewal
, when the lease expires, asking for new lease time.
If the exception RemotingException: Object <URI> has been disconnected or does not exist at the server occurs, this may be because of an expired lease time. |
Configuration files
Both the server and the client channels can be configured programmatically or by using a configuration file.
Using configuration files for Remoting clients and servers has the advantage that the channel and remote object can be configured without changing a single line of code and without the need to recompile. Another advantage is that the Remoting code we have to write is very short. Specifying the options programmatically has the advantage that we could get to the information during runtime. One way to implement this can be that the client uses a directory service to locate a server that has registered its long running objects there.
|
The server configuration file will look like this. We can call this file as SimpleServer.exe.config:
<configuration>
<system.runtime.remoting>
<application name="SimpleServer">
<service>
<wellknown
mode="SingleCall"
type="Samples.MyRemoteObject, MyRemoteObject"
objectUri="MyRemoteObject" />
</service>
<channels>
<channel ref="tcp server" port="9000" />
</channels>
</application>
</system.runtime.remoting>
</configuration>
All the server has to do is read the configuration file and activate the channel. This can be done with a single call to the static method RemotingConfiguration.Configure()
. RemotingConfiguration.Configure()
reads the configuration file SimpleServer.exe.config to configure and activate the channel. The creation of the remote object and communication with the client is done by the Remoting infrastructure. We just have to make sure that the process doesn't end. We do this with Console.ReadLine()
that will end the process when the user enters the Return key:
using System;
using System.Runtime.Remoting;
namespace Samples
{
class SimpleServer
{
static void Main(string[] args)
{
RemotingConfiguration.Configure("SimpleServer.exe.config");
Console.WriteLine("Press return to exit");
Console.ReadLine();
}
}
}
The client channel would look like as follows. We can call this file SimpleClient.exe.config:
<configuration>
<system.runtime.remoting>
<application name="SimpleClient">
<client url="tcp://localhost:9000/SimpleServer">
<wellknown
type="Samples.MyRemoteObject, MyRemoteObject"
url =
"tcp://localhost:9000/SimpleServer/MyRemoteObject"
/>
</client>
<channels>
<channel ref="tcp client" />
</channels>
</application>
</system.runtime.remoting>
</configuration>
As in the server, we can activate the client channel by calling RemotingConfiguration.Configure()
. Using configuration files, we can simply use new
to create the remote object. Next, we call the method Hello()
of this object:
using System;
using System.Runtime.Remoting;
namespace Samples
{
Public class SimpleClient
{
static void Main(string[] args)
{
RemotingConfiguration.Configure("SimpleClient.exe.config");
MyRemoteObject obj = new MyRemoteObject();
Console.WriteLine(obj.Hello());
}
}
}
System.Runtime.Remoting.RemotingConfiguration class
The RemotingConfiguration
class provides facilities for Remoting configuration as well as methods to register "well known objects" and client-activation objects.
To expose (or publish) objects to clients, you need to register them with the RemotingServices
class using a "well-known" name. The RemotingServices
itself binds any registered object to the channels registered on the ChannelServices
, so that each object gets a "well-known" endpoint.
"Well-known" means that once you make that information available through your program documentation, web-site, DISCO documents, or UDDI, the clients can connect to an object using a published and, hence, well-known name.
Objects can only be published using these well-known names, and therefore, the COM concept of programmatic identifiers (ProgIDs) or class-identifiers (CLSIDs) is neither supported nor required.
RemotingServices.RegisterWellKnownServiceType
This is the way by which we register Singleton or Single Call objects programmatically (without the help of configuration files). This is done with a call to RegisterWellKnownServiceType()
. That you register an object is, in fact, not entirely accurate. You register the "type" (or better: class) of the object, and the RemotingConfiguration
infrastructure will use the type information to either create a new instance of that class for every call, or to immediately create a new instance to be used as a Singleton object. In either case, you can pass arguments to the object at creation time – just as in COM.
The example shown here illustrates the registration of a well-known object and the resulting URI that a client can use to connect to the object's endpoint.
Registering a well-known object and making it available for remote clients doesn't take more than three simple steps:
- Create a new channel object instance, providing an IP port to listen on, or some other address information if you have a custom channel.
- Register the channel with the
ChannelServices
class. - Register the class of the object and the endpoint identifier, which must be unique for your application. The last argument for the call indicates whether the object shall be SingleCall or Singleton. Here, it's a SingleCall object.
The result from these registration steps is that your .NET application exposes an object on the shown endpoint: "http://myserver:8080/MyEndpointURI".
Now, your .NET application is independent of whether it is a service process, a Windows Forms application, or a Console application. It is a full-blown HTTP server without using Internet Information Server or another HTTP infrastructure. You can hit this address with a standard web-browser, appending "?WSDL" to the endpoint address, and we will see the WSDL service contract.
The whole Remoting architecture comprises of Messages, Sinks, Proxies, and Configuration objects.
Messages are objects that implement IMessage
. Messages are the objects through which the information is transferred to another object. From a programming standpoint, messages are objects that implement the IMessage
interface.
IMessage: The IMessage
interface is a very simple dictionary of key/value pairs. The message dictionary may contain any number of entries, each individually keyed by a string. The values that are held in the message may be of any .NET object type. The Remoting infrastructure's task is to remote these dictionaries across Remoting boundaries
To make implementation of method calls consistent, .NET defines a few additional message types that all implement the IMessage
interface. The message types include construction call messages and responses, as well as method call messages and responses.
If messages are being delivered synchronously, they represent a request that expect an immediate response. If they are delivered asynchronously, they represent a request that expects a delayed response or no response at all.
As told earlier, the proxy exposes the same set of interfaces that the original object exposes. However, the proxy does not process the calls locally, but it holds the address or path to the actual objects, and thus sends them to the remote object it represents.
There are mainly two types of proxies used in the Remoting context: he "RealProxy" and the "Transparent proxy".
Transparent proxies
A transparent proxy is a dynamically created block of code that exists in the client application domain (or context) on behalf of the remote object, and exposes all of its features, including all fields, properties, events, delegates, and methods. The transparent proxy is an instance of the class System.Runtime.Remoting.Proxies.__ TransparentProxy
. This class is internal to the mscorlib assembly, so we cannot derive custom proxies from it.
When you call a method of the transparent proxy, the proxy formats all of the arguments and the method name into a message object, and submits this through the real proxy's Invoke()
method to the remote object. Once the method returns, it decodes the message elements, and places them back on the local stack so that the calling code can pick them up.
This diagram illustrates the dependencies between the real proxy and the transparent proxy as well as the message sink and the channel. A client that uses an object across any kind of a Remoting boundary is actually using a transparent proxy for the object. The transparent proxy provides the illusion that the actual object resides in the client's space. It achieves this by forwarding calls made on it to the real object using the Remoting infrastructure.
RealProxy
A RealProxy is an instance of a class that inherits from the abstract base class RealProxy
that is defined in the System.Runtime.Remoting
namespace.
The real proxy accepts messages (IMessage
) through its Invoke()
method and forwards them to a remote object. With this functionality, real proxies are the transport layer for transparent proxies. However, they not only do the bulk of the work for the transparent proxies, they are also the "factories" to create them. The RealProxy
base class is able to dynamically create a transparent proxy for any arbitrary .NET class-type through metadata and .NET Reflection and have that transparent proxy call the real proxy's own Invoke()
method.
The RealProxy implements a part of the functionality that is needed to forward the operations from the transparent proxy. Note that a proxy object inherits the associated semantics of managed objects such as garbage collection, support for fields and methods, and can be extended to form new classes. The proxy has a dual nature: it acts as an object of the same class as the remote object (transparent proxy), and it is a managed object itself.
Channels are responsible for transporting messages. As such, they are an abstraction of a specific transport protocol, and wrap the protocol to make it available to the .NET Remoting infrastructure.
The .NET Framework comes with two built-in sets of channels: A TCP channel that uses permanently-connected raw TCP/IP sockets, and an HTTP channel that uses the .NET HTTP infrastructure.
The channel model is also extensible; if you require support for other transport protocols, you can implement your own channels and use them with all the parts of the .NET Remoting infrastructure.
A channel is responsible for establishing endpoint-to-endpoint communication. The transport portion of the channel implementation is handed a formatted data stream, and is responsible for getting that formatted data stream to the other side. On the other hand, it will receive a formatted data stream from another endpoint, and is responsible to deliver that to its host application domain.
Channels can listen for messages and send messages. The listener portion of a channel implements the IChannelReceiver
interface, and the sender portion implements the IChannelSender
interface. A channel is only bi-directional if it implements both interfaces. If you want, for some reason, to implement a receive-only channel, you only need to implement the IChannelReceiver
interface and IChannelSender
for a send-only channel, respectively. A receive-only channel can, of course, not relay any call backs.
As previously mentioned, you can also write your own channels if you need to transmit data over protocols not covered by the core .NET Framework, for instance, APPC, IPX, or Windows Named Pipes.
For your own channel implementation, you must implement either the IChannelReceiver
or the IChannelSender
interfaces, or both, depending on whether the channel is going to be bi-directional or unidirectional.
System.Runtime.Remoting.ChannelServices
The ChannelServices
class serves to manage and register .NET Remoting channels. You can register any number of channels with the channel services. These channels are global for your application domain, and define the transport endpoints that your application exposes.
If you register a TCP channel on port 8085 and an HTTP channel on port 8080, all object endpoints that have been registered with RemotingServices will be equally available on both channels.
Most of the functionality that was explained above will not work until this is actually implemented in a chain of channel sinks. A channel sink is a pluggable component that is provided to the channel through a channel sink provider class.
A channel sink provider class implements the interface IClientChannelSinkProvider
if it generates sinks for the client-side of a channel, and implements IServerChannelSinkProvider
if it generates sinks for the server side. The sinks themselves implement IClientChannelSink
and or IServerChannelSink
.
When a channel is created, you will, as in the case of the HTTP Channel or TCP Channel, be able to pass an implementation of either or both interfaces to the channel, letting you install your own channel sink.
Channel sinks may implement interception, security checks, logging, or whatever else you want to control or monitor regarding Remoting traffic.
A very special case is of formatter sinks. These are provided by implementations of either IClientFormatterSinkProvider
or the IServerFormatterSinkProvider
interfaces, and perform the conversion from and to the wire-format. A formatter sink implements IClientFormatterSink
and/or IServerFormatterSink
.
When a channel is constructed, the providers are asked to provide channel sinks, which are then linked into a chain of sinks. A call from a client will then be routed from the proxy to a formatter sink (or a custom formatter sink that is placed before the formatter sink), then to possibly existing custom sinks, and finally to the transport sink that will typically be provided by the channel implementation. On the receiving end, the channel architecture works similarly in the other direction.
Formatters
Formatters are dynamically used by the channel architecture. You configure a channel to transport messages in a certain wire format by associating it with a formatter other than its default through the Remoting configuration facility. The channel will then pick the associated formatter to render messages into the desired format.
Formatters are implemented through so-called sinks. Essentially, a channel employs a sequence of these sinks, which work hand-in-hand to inspect an IMessage
for logging and security purposes, turn it into a wire-format, and send it off to the receiver.
On the receiving end, the formatter acts as a de-serializer component that understands a certain wire format and is able to translate that into a .NET message object implementing IMessage
.
The .NET Framework has two built-in formatters, namely binary and SOAP. The binary formatter uses a very compact binary format that is designed for best performance in high-speed LAN environments and to interconnect .NET systems. The SOAP formatter uses the XML-based Simple Object Access Protocol to transport object calls over the Internet and to integrate .NET systems with all other systems in the enterprise environment. The built-in formatter types are, of course, pre-registered with the system.
Custom formatters allow talking to any endpoint.
The formatter model is extensible. If you want to integrate systems directly using other wire formats (to name a few: IIOP, RMI, ORPC, XP, XML-RPC), you could implement your own formatters and have .NET systems talk natively to any endpoint you want.
A TCP channel uses plain TCP sockets, and transmits, by default, a compact, binary wire format, provided by the BinaryFormatter
object.
However, if you want to link .NET Framework-based systems in a LAN environment, the TCP channel and binary formatter will yield the highest performance.
The default wire format is an implementation of the SOAP 1.1 standard, which is provided by the SoapFormatter
class located in the namespace "System.Remoting. Serialization.Formatters.Soap
".
By now, we have learned about messages that are key/value pair dictionaries, and about channels that somehow receive these messages, pass them to formatters to render them into a wire format, and then transport them to the destination.
On the receiving end, the wire format message is received by a channel, and deserialized by the formatter into the message object.
To use channels, the client code drops off messages into the Remoting infrastructure using the SyncDispatchMessage()
or AsyncDispatchMethod()
method on the ChannelServices
class, which resides in the System.Runtime.Remoting
namespace. As its name implies, SyncProcessMessage
processes the request message in a synchronous manner by passing that message to the next sink's SyncProcessMessage
method, where NextSink
is a property in the same class pointing to the next sink in the channel sink chain.
The IMessageSink
interface is implemented by channels and the Remoting infrastructure to accept messages.
The IMessageSink
interface has a NextSink
property, which enables chains of message sinks to be built. Message chains are a very common design pattern throughout the Remoting infrastructure. When implementing your own message sink, you must therefore honor the NextSink
property, by passing onward to the next sink any message which you do not want to handle yourself.
Member
| Member Type
| Description
|
NextSink
| Read-only property
| The next message sink in the chain, or null if this is the last sink in the chain
|
AsyncProcessMessage
| Method
| Processes the message asynchronously
|
SyncProcessMessage
| Method
| Processes the message synchronously
|
It is, however, perfectly legal to intercept, modify, or process any message received in a message sink implementation instead of passing it through to the ultimate destination.
If an unbound class has no serialization support, it cannot be marshalled, and will cause a remote method invocation to fail, if it is used as an argument or return value.
Adding serialization support to a class requires setting the "[serializable]
" attribute on the class and, optionally, implementing the ISerializable
interface of the class. This allows Formatters to use the serialization support of the Runtime to render the class content in XML or a binary data stream, which can then be translated into the formatter's native wire format.
The Dispatcher is the proxy's counterpart at the receiving end of the communication and located at the channel endpoint.
The Dispatcher's capabilities are provided by the StackBuilderSink
class that is located in the System.Runtime.Remoting.Messaging
namespace.
The dispatcher
- receives messages,
- builds the stack-frame,
- invokes a method,
- collects results,
- and creates a response message.
Whenever one of those two methods (Method A or Method B shown in the above figure) is called, the passed message object is decoded and the Dispatcher locates the object that is referenced in the call, dynamically creates a stack frame that contains all the arguments, and executes the requested method.
When the method call returns, it collects the output parameters and the return value, and creates a response message with these parameters. The dispatcher will, of course, convert marshal by reference objects into references, which in turn are passed over the network, and "proxied" on the receiving end. The response message is then handed back to the caller.
Below is a graphical representation of the flow. The message which arrives at the receiving end of a channel is serialized and formatted into a message object, and is passed to the StackBuilderSink.SyncDispatchMessage()
method. The call is dispatched on the real object, and all values that must be returned are collected and sent back as a response through the channel.
Let us assume we already have a transparent proxy that we can call. When we call the method "MethodA()
" on the transparent proxy, it formats the method name and the arguments if such are present, into a IMessage
format, and forwards them to the real proxy's Invoke()
method. The real proxy then drops this message into the IMessageSink
of the channel through the sink's SyncProcessMessage()
method. Since .NET makes no assumptions about the endpoint architecture, we're not doing this either, and just assumes that the message will somehow be responded. Actually, this message object will be parsed by each sink shown in the above figure until the sink chain ends or the server object is reached, and in a similar manner, it navigates back to the client object too. Once the real proxy receives the response message, it will pass the response message on to the transparent proxy, which will push the return values on the stack. With that, the calling code will believe that it just called a local method.
When a channel receives a message that contains an object reference (an instance of the ObjRef
class), the reference information also contains the type of the referenced object. With that data type in hand, the channel will create a new RealProxy
instance and asks it to create a new transparent proxy based on the metadata of that data type. This transparent proxy object is then passed on to the called method.
You can declare methods one-way using the "[oneway]
" attribute on the message. When this attribute is present on the method, the dispatcher will not collect any return value information, and the proxies will not wait for such information to arrive, and return immediately to the caller.
If a OneWay method throws an exception, this exception will not be propagated to the client. |
A very valuable tool is the call context. (Do not confuse this with the .NET Remoting context.) The call context allows you to add invisible arguments to a method call. While there will only be rare uses in end-to-end communication, the call context is a true lifesaver when you are using context interception, and take advantage of the extensibility mechanisms and the message chain that we are going to discuss in the following section.
Each message object automatically contains a dictionary entry "__CallContext
" that holds an instance of the type "CallContext
".
The CallContext
class is a utility class in the namespace System.Runtime.Remoting.Messaging
. We can add data to the context with CallContext.SetData()
, and reading the data can be done with CallContext.GetData()
.
A class that can be passed with the call context must implement the interface ILogicalThreadAffinative
. This interface doesn't define a single method; it is just a marker to the .NET Remoting runtime. A logical thread can spawn multiple physical threads as a call can cross process boundaries, but it is bound to a flow of method calls. Implementing the interface ILogicalThreadAffinative
in a class means that objects of this class can flow with the logical thread. Additionally, the class is marked with the [Serializable]
attribute so that the object can be marshaled into the channel.
Remoting Services
The System.Runtime.Remoting
namespace contains a couple of so-called service classes that allow you to configure and tweak the runtime behaviour of the Remoting system.
If the activator has determined that an object needs to be created or accessed remotely, it forwards the call to the Connect()
method on the RemotingServices
class.
Connect()
provides low-level access to Remoting, and lets you activate objects remotely even if the class is not configured to be remote in your application domain. The Connect()
method takes an argument of type System.Type
(or a string representation thereof) and an endpoint URI as arguments. The return value is a properly proxied object, if the call succeeds.
The type information is used to extract the metadata from an assembly to create the transparent proxy from the real proxy. Therefore, the metadata of the remote class must be available to the client at runtime. This does not mean, though, that the client must possess the actual server implementation assembly. The client side may rather have a reduced copy of the metadata that only describes the full structure and that is sufficient for the proxy to build a structural image of the remote class.
One way to do this is to use the Soapsuds utility that is included with the .NET Framework SDK. The easiest way to build a metadata-only assembly is:
soapsuds –ia:<inputAssemblyName> -oa:<outputFile>
If the connection string that is used with the Connect()
method is wrong, we get an error the first time that a remote method is called. The possible error messages we can get are listed here:
1.SocketException: No such host is known.
This error message occurs when the host name cannot be resolved.
2.SocketException: No connection could be made because the target machine actively refused it.
If the specified port number is incorrect or the server is not started, we will get this error message.
3.RemotingException: Object <Name> has been disconnected or does not exist at the server.
This error message occurs if the name of the endpoint is incorrectly specified, or when the leasing time has expired when using a client-activated object.
|
System.Runtime.Remoting.TrackingServices
The tracking services serve to "track" almost everything that is happening in the Remoting subsystem. Whenever an object becomes disconnected (after its lease has expired or a connection is broken) or when an object has been marshalled or unmarshaled (a proxy has been built), the tracking services will invoke a user provided implementation of the ITrackingHandler
interface. This interface defines three methods that are called by the Remoting infrastructure when marshalling and unmarshalling occurs:
We can register an implementation of ITrackingHandler
by the following code snippet:
TrackingServices.RegisterTrackingHandler(new MyTrackingHandler());
where MyTrackingHandler
is the implementation of ITrackingHandler
.
Calling remote methods across the network can take some time. We can call the methods asynchronously, which means that we start the methods, and during the time the method call is running on the server, we can do some other tasks on the client. .NET Remoting can be used asynchronously in the same way as local methods.
Calling remote methods asynchronously
The same programming model used for local calls can also be used for remote calls. We will extend the Remoting example by using some arguments and return values with a remote method. First, we extend the remote object class RemoteObject
with the long-running method MyMethod()
. Nothing else changes with the server.
public int Mymethod(int val1, int val2, int ms)
{
Thread.Sleep(ms);
return val1 + val2;
}
In the client program, we have to declare a delegate with the same parameters as the Mymethod()
method:
class SimpleClient
{
private delegate int MyMethodDelegate(int val1, int val2,int ms);
In the Main()
method, after creating the remote object, we create an instance of the delegate, and pass the method Mymethod
to the constructor:
static void Main(string[] args)
{
RemotingConfiguration.Configure("SimpleClient.exe.config");
RemoteObject obj = new RemoteObject();
MymethodDelegate d = new MyMethodDelegate (obj.Mymethod);
}
The remote method can now be started asynchronously with the BeginInvoke()
method of the delegate class. Here we pass the values 3 and 4 to add with a sleep time of 100 ms. The method BeginInvoke()
will return the control immediately, initialising the execution of the Mymethod
method. The main thread will continue its execution with other statements.
IAsyncResult ar = d.BeginInvoke(3, 4, 100, null, null);
Console.WriteLine("Method started");
To get the result that is returned with the method Mymethod()
, we have to wait until the method is completed. This is done using ar.AsyncWaitHandle.WaitOne()
. Calling the delegate method EndInvoke()
, we get the result that is returned from Mymethod()
.
ar.AsyncWaitHandle.WaitOne();
if (ar.IsCompleted)
{
Console.WriteLine("Method finished");
int result = d.EndInvoke(ar);
Console.WriteLine("result: " + result);
}
Call backs with delegates
.NET Remoting supports two types of callbacks: with remote objects that are passed to the server, and with delegates. Using a delegate, we can pass an AsyncCallback
delegate when calling the method BeginInvoke()
to have a call-back when the asynchronous remote method completes. We have to define a method that has the same signature and return type that the delegate AsyncCallback
defines. The delegate AsyncCallback
is defined with this signature in the assembly mscorlib:
public delegate void AsyncCallback(IAsyncResult ar);
We implement the method MyMethodCallback
that has the same signature and return type. The implementation of this method is similar to how we handled the asynchronous method in the last code sample. It doesn't matter if the method is implemented as a class method or an instance method. We only need access to the delegate that is used to start the remote method. To make the delegate available to the static method, we'll now declare a static member variable:
private delegate int MymethodDelegate(int val1, int val2, int ms) {
private static MymethodDelegate d;
public static void MymethodCallback(IAsyncResult ar)
{
if (ar.IsCompleted)
{
Console.WriteLine("Method finished");
int result = d.EndInvoke(ar);
Console.WriteLine("result: " + result);
}
}
In the Main()
method, we have to create a new instance of the AsyncCallback
delegate and pass a reference to the function that should be called asynchronously. With the BeginInvoke()
method, we pass the instance of the AsyncCallback
so that the method MymethodCallback()
will be invoked when the remote method Mymethod()
completes.
static void Main(string[] args)
{
RemotingConfiguration.Configure("SimpleClient.exe.config");
RemoteObject obj = new RemoteObject();
d = new MymethodDelegate(obj.Mymethod);
AsyncCallback cb =
new AsyncCallback(SimpleClient.MymethodCallback);
IAsyncResult ar = d.BeginInvoke(3, 4, 3000, cb, null);
Console.WriteLine("Method started");
With this, I am concluding my topic here. I think I was able to give you an insight to almost all main areas in .NET Remoting. If you find this article good, then don't forget to vote.