.NET Remoting

This article has been excerpted from book "The Complete Visual C# Programmer's Guide" from the Authors of C# Corner.

Remote procedure call (RPC), the Component Object Model (COM), the Distributed Component Object Model (DCOM), Common Object Request Broker Architecture (CORBA), and remote method invocation (RMI) are technologies that provide a means to transfer data across boundaries, in what has come to be called distributed applications. A boundary can be another process, a different machine, a remote network, or, in the case of .NET, an application domain. Generally, distributed applications can be simply illustrated as shown in Figure 25.1.

Figure-25.1.gif

Figure 25.1: Typical Distributed Application

The .NET Remoting Framework provides an extensive array of classes enabling developers to create distributed applications with minimal effort and complexity. The framework is also highly extensible, presenting developers with a wide range of customization options that extend to Internet solutions.

The .NET Framework Developer Specifications, published by Microsoft, describes .NET Remoting as enabling Web Services Anywhere and CLR (Common Language Runtime) Object Remoting. Web Services Anywhere means that remoting functionality and services can be used by any application over any transport and can employ any encoding scheme. CLR Object Remoting rides on top of Web Services Anywhere. It provides all the language semantics normally associated with a local object, such as constructors, delegates, the new operator, and overloaded methods. Activation, lease-based lifetime, distributed identity, and call context are also dealt with in CLR Object Remoting.

The remoting framework comprises a large segment of the System.Runtime namespace. Remoting encompasses 11 namespaces containing in excess of 50 classes and interfaces. The wealth of material in the remoting framework deserves an entire book. Unfortunately, the time and space allotted to this article permit only a survey of a few of those namespaces, classes, and interfaces.

This article explores .NET Remoting and relies heavily on examples to demonstrate various topics. Among the subjects highlighted are these:

  • Remoting components
  • A simple client/server sample
  • Lifetime management
  • Passing objects and collections as method parameters and return types
  • Remoting events and delegates
  • Call contexts
  • Configuration files in remoting
  • Remoting over the Web
  • Creating a custom sink

.NET Remoting Components

At its core the .Net Remoting Framework communicates between server and client objects via object references. To qualify as a remote object, the class must derive from System.MarshalByRefObject. Much in the manner of COM, the remoting system uses proxies in the client's application domain. A proxy is an abstract representation of the actual remote object on the server. In the case of .NET, two proxies are created-a transparent proxy and a real proxy. The transparent proxy is the remote object reference returned to the client. The real proxy handles the forwarding of messages to the channel. Sound confusing? The diagram in Figure 25.2 will, hopefully, make the sequencing clearer.

The transparent proxy is returned when the client activates a remote object using the new operator or a System.Activator method. All method calls on the proxy are intercepted by the runtime to ensure the call is a valid method or property on the remote object. When a remote object's method is called, the transparent proxy calls System.Runtime.Remoting.Proxies.RealProxy.Invoke() with a System.Runtime.Remoting.Messaging.IMessage interface, which contains the called method's parameter types and their values. The actual implementation is the private Message class in mscorlib.dll.

A real proxy is created along with the transparent proxy. To create a customized proxy, a developer derives the class from the RealProxy class and overrides Invoke().The real proxy's primary responsibility is to forward messages to the channel. It does this by first creating a message sink on the channel with the call System.Runtime.Remoting.Channels.IChannelSender.CreateMessageSink and then storing the returned IMessageSink object. Then in the Invoke method, it calls either the message sink's SyncProcessMessage() or AsyncProcessMessage().

Figure-25.2.gif

Figure 25.2: Remoting Sequence

The Channel Sink and Formatter Sink boxes in the diagram are somewhat misleading in two ways. First, neither is an independent entity but rather a member of the channel. Second, sinks are linked together in a chain. Each member in the chain retains a reference to the next sink in the chain. In the client's application domain, the first sink is a formatter and the last sink is the transport sink. In the server's domain the order is reversed. Between the formatter and transport sinks, customized user sinks may be dropped into the chain. Each sink in the chain implements the ProcessMessage() and AsyncProcessMessage() methods and is responsible for calling the respective method on the next sink in the chain.

The function of formatter sinks is to serialize and deserialize IMessage to and from a stream. The client must implement IClientChannelSink, while the server implements IServerChannelSink.

The transport sink is responsible for transporting the serialized message.

A channel can receive messages, send messages, or both. Every channel derives from IChannel. Depending on the channel's purpose, it must also inherit IChannelReceiver or IChannelSender interfaces, or both.

One question you may have is how one can, as we asserted at the outset, "create distributed applications with minimal effort and complexity," given the number of namespaces, classes, and interfaces in the preceding description. Why have the .NET designers taken something simple like COM and made it complicated? The answer is, they haven't. .NET ships with two remoting types that support HTTP and TCP/IP protocols. Each provides a client channel, a server channel, and a combined sender-receiver channel. The default formatters support binary and Simple Object Access Protocol (SOAP) serialization.

At this point, it would be beneficial to demonstrate these concepts with an example.

A Server-Activated Example

The projects are in the Sample1 folder and consist of a class library (SimpleObjectLib) and two console applications (ServerActivatedServerExe and ServerActivatedClientExe). The console projects should add a reference to System.Runtime.Remoting.dll and SimpleObjectLib.dll. This section presents the code with few remarks.

Listing 25.1: The SimpleObject Code


namespace
SimpleObjectLib
{
    public class SimpleObject : MarshalByRefObject
    {
        public SimpleObject()
            : base()
        {
            Console.WriteLine(
            "In SimpleObject constructor.");
        }

        public string ConCatString(string first, string second)
        {
            string concat = null;
            Console.WriteLine(
            "In SimpleObject.ConCatString method.");
            concat = first + " " + second;
            return (concat);
        }
    }
}


Notice in Listing 25.1 that SimpleObject derives from MarshalByRefObject. Any class that is a candidate for remoting must derive from MarshalByRefObject. The output for the example is shown below in Figure 25.3.

Figure-25.3.gif

Figure 25.3: The ServerActivatedServerExe Output

Listing 25.2: The ServerActivatedServerExe Code


using
System;
using
System.Runtime.Remoting;
using
System.Runtime.Remoting.Channels;
using
System.Runtime.Remoting.Channels.Http;
using
SimpleObjectLib;

namespace
ServerActivatedServerExe
{
    class ServerActivatedServer
    {
        static void Main(string[] args)
        {
            HttpServerChannel http = null;
            http = new HttpServerChannel(1234);
            ChannelServices.RegisterChannel(http);
            RemotingConfiguration.RegisterWellKnownServiceType(
            typeof(SimpleObject),
            "Simple",
            WellKnownObjectMode.SingleCall);
            Console.WriteLine(
            "Press <enter> to exit.");
            Console.ReadLine();
        }
    }
}


The server code in Listing 25.2 is simple and straightforward. An instance of a System.Runtime.Remoting.Channels.Http.HttpServerChannel is created, specifying the port it will listen on. Once the channel has been instantiated, it registers itself with the static method ChannelServices.RegisterChannel(). Finally, the remote object is registered with the static method RemotingConfiguration.RegisterWellKnownServiceType(). The parameters specify the remote object, the URI (Uniform Resource Identifier), and the calling mode enumeration. The Console.ReadLine() keeps the server up and running. When the process ends, all channels and registered objects are dropped by remoting services.

Figure-25.4.gif

Figure 25.4: The ServerActivatedClientExe Output

Listing 25.3: The ServerActivatedClientExe Code


using
System;
using
System.Runtime.Remoting;
using
System.Runtime.Remoting.Channels;
using
System.Runtime.Remoting.Channels.Http;
using
SimpleObjectLib;

namespace
ServerActivatedClientExe
{
    class ServerActivatedClient
    {
        static int Main(string[] args)
        {
            HttpClientChannel http = null;
            http = new HttpClientChannel();
            ChannelServices.RegisterChannel(http);
            SimpleObject simple1 = null;
            simple1 = (SimpleObject)Activator.GetObject(
            typeof(SimpleObject),
            "http://localhost:1234/Simple");
            string ret = null;
            ret = simple1.ConCatString("using",
            "Activator.GetObject");
            Console.WriteLine(ret);
            SimpleObject simple2 = null;
            RemotingConfiguration.RegisterWellKnownClientType(
            typeof(SimpleObject),
            "http://localhost:1234/Simple");
            simple2 = new SimpleObject();
            ret = simple2.ConCatString("using the \"new\"",
            "operator");
            Console.WriteLine(ret);
            return (0);
        }
    }
}


In the client example shown in Listing 25.3 and it's corresponding output in Figure 25.4, two ways of obtaining a remote reference are demonstrated: using the Activator and the new operator. Also available to developers is a System.Runtime.Remoting.RemotingServices overloaded method, Connect(). A channel is created and registered. This time, it is an HttpClientChannel. The default constructor attaches to any available port. In fact, if you look at the HttpClientChannel constructors in the .NET class library, a port number specification isn't an option. The static method Activator.GetObject() returns a transparent proxy for SimpleObject. The parameters specify the remote object type and the Uniform Resource Locator (URL). To use the new operator to activate remote objects, you must register the object by calling RemotingConfiguration.RegisterWellKnownClientType(). The parameters are the object type and the URL. Calling new on SimpleObject returns a transparent proxy. It should be noted, that calling the new operator without registering with RemoteConfiguration will work, but a local reference is returned, not a transparent proxy.

As is evident from the preceding example, .NET designers have abstracted much of the remoting complexity and hidden many of the implementation details. The preceding example and a clientactivated example, encountered later in the next article, probably offer a solution for 60 to 70 percent of problem domains. While it isn't essential to understand what is actually going on "under the covers," for those situations where portions of the framework must be adapted or extended, knowledge of the underlying architecture is required.

The server starts up, creates a channel and registers itself, then registers a "well-known type" with remoting configuration. Looking at the server's initial output, Figure 25.3, nothing has been accomplished except the Console.WriteLine() Press <enter> to exit. What has happened? The channel starts listening for client connection requests on construction. This is established by looking into the _tcpListener member, an instance of System.Net.Sockets.TcpListener, of the HTTP channel, and determining that the Active property is true. It also can be determined that the remote object has been registered with the configuration services by calling the method System.Runtime.Remoting.RemotingConfiguration.GetRegisteredWellKnownServiceTypes(), which returns a WellKnownServiceTypeEntry[] with a list of registered types. SimpleObject is in the list but not yet instantiated.

Start the client. Channels are created and registered, and the Activator.GetObject() is called. No communication has taken place yet between the client and the server. Activator.GetObject() has simply returned a local instance of the transparent proxy. Only when simple1.ConCatString() is called, does the server's output console begin to show signs of life.

Figure-25.5a.gif

Figure 25.5a: The ServerActivatedServerExe Output

Notice that SimpleObject's constructor (see Figure 25.5a) is called three times, whereas the ConCatString() is called twice. There are a number of things to do in discovering what is actually taking place, but this is a good point to digress for a moment.

If you look at the server code, the call RemotingConfiguration.RegisterWellKnown had as a parameter WellKnownObjectMode.SingleCall. It is an enumeration with two values, SingleCall and Singleton. Using SingleCall creates a new instance of the object for every method call. Singleton mode services all object requests with the same instance. If the mode in the server's code were changed, the server output would look like that shown in Figure 25.5b.

Figure-25.5b.gif

Figure 25.5b: Server Output with Mode Change

Now, back to tracing remote object activation, by doing the following:

  • Override MarshalByRefObject's CreateObjRef() in the SimpleObject to aid in tracking the sequence of events.
  • MyTrackingHandler in SimpleObjectLib is derived from System.Runtime.Remoting.Services.ITrackingHandler, a utility interface to trace remote object marshaling.
  • In the server code, register the tracking handler using System.Runtime.Remoting.Services.TrackingServices.RegisterTrackingHandler().

As the client code calls simple1.ConCatString(), the server creates an instance of SimpleObject(), followed by a call to SimpleObject.CreateObjRef(). By registering a MyTrackingHandler instance, you can see that the remoting subsystem then marshals SimpleObject to obtain a System.Runtime.Remoting.ObjRef.

This ObjRef is a representation of the remote object, including the class's strong name, hierarchy, implemented interfaces, and URI. It also contains details of all the available registered channels. The server's remoting framework stores this reference in a table to track registered objects. With server-activated remote objects, the ObjRef is created only once during the lifetime of the server. This is in contrast to client-activated objects, which we'll encounter next.

Once the server has created and stored the ObjRef, the first instance of SimpleObject is ready for the Garbage Collector (GC). At this point, the server turns to servicing the client's request with a call to SimpleObject(), and then ConCatString(). What has been established? Although this is explained in Microsoft's .NET Remoting: A Technical Overview, we have used a number of the remoting helper classes and interfaces to confirm a number of points:

  • No actual communication takes place between the client and the server until a method or property is called on the remote object.
  • Server-activated remote objects registered in SingleCall mode must remain stateless, as there is a new instance with every method call. Singleton mode remote objects could maintain state, but only one instance services all clients, and therefore they should also remain stateless, because of probable corruption.
  • An ObjRef is created only once during a server's lifetime and that is used for all subsequent method calls.

While the preceding holds true for server-activated remote objects, this is not the case for clientactivated objects. But before proceeding, it was stated earlier that a real proxy could be customized. This is also true of ObjRef, by inheriting and overriding any of its virtual properties or methods.

A Client-Activated Example - remoting


The projects are in the Sample1 folder. The two console applications are ClientActivatedServerExe and ClientActivatedClientExe. Again, add a reference to System.Runtime.Remoting.dll and SimpleObjectLib.dll. SimpleObject has remained unchanged, so we need not bother with the code. Notice that the protocol has been changed to TcpServerChannel and TcpClientChannel. The server's output is shown in Figure 25.6.

Figure-25.6.gif

Figure 25.6: The ClientActivatedServerExe Output

Listing 25.4: The ClientActivatedServerExe Code


using
System.Runtime.Remoting.Channels.Tcp;

namespace
ClientActivatedServerExe
{
    class ClientActivatedServer
    {
        static void Main(string[] args)
        {
            TcpServerChannel tcp = null;
            tcp = new TcpServerChannel(1234);
            ChannelServices.RegisterChannel(tcp);
            RemotingConfiguration.ApplicationName = "Simple";
            RemotingConfiguration.RegisterActivatedServiceType(
            typeof(SimpleObject));
            Console.WriteLine(
            "Press <enter> to exit.");
            Console.ReadLine();
        }
    }
}


This time, a System.Runtime.Remoting.Channels.Tcp.TcpServerChannel is created and registered (see Listing 25.4). The RemotingConfiguration.ApplicationName is Simple, which specifies the URI. In this scenario, server registration of a client-activated object using RemotingConfiguration employs the RegisterActivatedServiceType() method. Then the server waits for a client to connect.

Figure-25.7.gif

Figure 25.7: The ClientActivatedClientExe

Listing 25.5: The ClientActivatedClientExe Code


static
void Main(string[] args)
{
        TcpClientChannel tcp = null;
        tcp = new TcpClientChannel();
        ChannelServices.RegisterChannel( tcp);
        UrlAttribute[] urls = new UrlAttribute[1];
        UrlAttribute url = new UrlAttribute(
        "tcp://localhost:1234/Simple");
        ObjectHandle handle = null;
        urls[0] = url;
        handle = Activator.CreateInstance( "SimpleObjectLib",
        "SimpleObjectLib.SimpleObject",
        urls);
        SimpleObject simple1 = null;
        string ret = null;
        simple1 = (SimpleObject)handle.Unwrap();
        ret = simple1.ConCatString( "using", "Activator.CreateInstance");
        Console.WriteLine( ret);
        SimpleObject simple2 = null;
        RemotingConfiguration.RegisterActivatedClientType(
        typeof(SimpleObject),
        "tcp://localhost:1234/Simple");
        simple2 = new SimpleObject();
        ret = simple2.ConCatString( "using new", "operator");
        Console.WriteLine( ret);
        return( 0);
        }


The client code in Listing 25.5 uses Activator.CreateInstance() and the new operator to create instances of SimpleObject. Notice that CreateInstance() returns a System.Runtime.Remoting.ObjectHandle rather than a transparent proxy. To obtain the proxy, Unwrap() must be called before accessing SimpleObject's method. The second instance of the SimpleObject proxy is created after calling RemotingConfiguration's RegisterActivatedClientType() method. The client's output can be seen in Figure 25.7.

As in the first example, Listing 25.2, the server creates and registers its channels. When it registers the remote object with remoting configuration, no mode is specified, only the type. This is because the client controls the lifetime of each instance through leasing, discussed later in this chapter. This also means that the client-activated remote objects can have state.

Upon calling RegisterActivatedServiceType(), with the aid of ITrackingHandler, an ObjRef of ActivationListener type is marshaled. This class isn't listed in the .NET class library under the System.Runtime.Remoting.Activation namespace, but it can be found, using the MSIL Disassembler, in the mscorlib.dll in the Activation namespace. It implements the System.Runtime.Remoting.IActivator interface and contains channel and SimpleObject information.

Once the client has started up, compare the server output of Figures 25.3 and 25.6 (duplicated in Figures 25.8 and 25.9).

Figure-25.8.gif

Figure 25.8: Server Output Shown Again

Figure-25.9.gif

Figure 25.9: Server Output Shown Again

Notice on the server's console screen that the SimpleObject constructor for client-activated objects is called for each creation request. This is also true for the CreateObjRef() method. Remember that server-activated objects require a single ObjRef during the lifetime of the server. Because a client controls the lifetime of the remote object, each instance of the object requires an ObjRef. Using ITrackingHandler, we discover that an ObjRef is marshaled by the server and streamed to the client, where it is unmarshaled to create a transparent proxy. This happens each time the client activates SimpleObject. Also, unlike server-activated objects, which activate the remote objects only when method calls are made, client-activated objects are activated when Activator.CreateInstance() or new is called.

Conclusion

Hope this article would have helped you in understanding the .NET Remoting. See other articles on the website on .NET and C#.

visual C-sharp.jpg The Complete Visual C# Programmer's Guide covers most of the major components that make up C# and the .net environment. The book is geared toward the intermediate programmer, but contains enough material to satisfy the advanced developer.

Up Next
    Ebook Download
    View all
    Learn
    View all