[Remoting]

About Remoting

Thursday, May 29, 2003

.NET Remoting is a general purpose solution for cross-AppDomain communication. It applies to exchanging data between different application domains within a process, different processes, and even processes running on different machines. A big pro is that Remoting allows the use of .NET data types, something that is described as "rich type fidelity". Remotable objects can be passed across application domains by value (then, they must be seriablizable), or by reference (then, they must derived from System.MarshalByRefObject).

This article shows an example of using a remote object that marshals by reference, and is dynamically published.

Some sample code

Here is how WebEdit.NET uses .NET Remoting for inter-process communication between program instances (the code has been simplified somewhat). WebEdit.NET functions as both a .NET Remoting service and as a .NET Remoting client. First, it tries to act as a client, looking for a previous instance of the application, and connecting to a remote object. If that fails, it starts a Remoting services, and run the application:

using System;
using System.Diagnostics;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using System.Threading;
using System.Windows.Forms;
using Gregor.Core;

public class WebEditStart {

    private static IChannel s_Channel;
    private static CWebEditConnector s_Connector;

    public static void Main(string[] args){
        Thread.CurrentThread.ApartmentState = ApartmentState.STA;
        RunSingleInstance(args);
    }

    private static void RunSingleInstance(string[] args){
        if(false == RunWithPreviousInstance(args)){
            try{
                StartServer();
                WebEditApp.Enter(args);
            }finally{
                StopServer();
            }
        }
    }

    // the code for RunWithPreviousInstance,
    // StartServer, and StopServer is shown below

} // class WebEditStart

The client side

On the client side, WebEdit first determines if there is a previous application instance. If so, a TcpChannel is created (a channel is a line of communication in Remoting; its main characteristic is a network protocol). Then, the connector type is registered with the remoting configuration (this can also be done in a config file), along with a URL (which points to a symbolic object name via a port on the local host) that identifies the object.

The object creation expression does not actually create an instance in the current AppDomain. Instead, a proxy is created, and instantiation, which happens in the remote AppDomain, is delayed until a method is called (the connector class must have a parameterless constructor).

private static bool RunWithPreviousInstance(string[] args){
    bool fRet = false;
    IChannel channel = null;
    try{
        // determine if there is a previous process
        string sProcName = Process.GetCurrentProcess().ProcessName;
        Process[] processes = Process.GetProcessesByName(sProcName);
        if(processes.Length > 1){
            // register client channel
            channel = new TcpChannel(0);
            ChannelServices.RegisterChannel(channel);
            // register well-known client type
            Type tp = typeof(CWebEditConnector);
            string sURL = CWebEditConnector.URL;
            RemotingConfiguration.RegisterWellKnownClientType(tp, sUrl);
            // establish reference to remote object
            CWebEditConnector connector = new CWebEditConnector();
            // see if app is alive (the first access to an instance
            // member access will instantiate the remote object)
            if(connector.IsAlive){
                // that's all we want to know
                fRet = true;
                // process command line (ignore further exceptions)
                connector.ProcessCommandLine(args);
                // pop up the app
                connector.ActivateApp();
            }	
        }
    }catch(Exception ex){
        Dev.ProcessException(ex);
    }finally{
        // unregister channel
        if(channel != null){
            try{
                ChannelServices.UnregisterChannel(channel);
            }catch(Exception ex){
                Dev.ProcessException(ex);
            }
        }
    }
    return fRet;
}

The server side

For starting the Remoting service, a TcpChannel is created (this time, from the server side). Next, the connector type is registered - this time as a service type, using a symbolic name for the singleton object; the last parameter specifies that the object is a singleton (an alternative would be WellKnownObjectMode.SingleCall, which means that every call creates a new instance; but note that .NET Remoting offers more options for object lifetime management). The singleton is published using RemotingServices.Marshal(), again using a symbolic name.

public static void StartServer(){
    try{
        // register channel
        s_Channel = new TcpChannel(CWebEditConnector.PORT);
        ChannelServices.RegisterChannel(s_Channel);
        // register service type
        Type tp = typeof(CWebEditConnector);
        string sUri = CWebEditConnector.URI;
        WellKnownObjectMode mode = WellKnownObjectMode.Singleton;
        RemotingConfiguration.RegisterWellKnownServiceType(tp, sUri, mode);
        // publish singleton
        s_Connector = new CWebEditConnector();
        RemotingServices.Marshal(s_Connector, sUri);
    }catch(Exception ex){
        Dev.ProcessException(ex);
    }
}

public static void StopServer(){
    try{
        // revoke publication
        if(s_Connector != null){
            RemotingServices.Disconnect(s_Connector);
        }
        // remove channel
        if(s_Channel != null){
    	       ChannelServices.UnregisterChannel(s_Channel);
        }
    }catch(Exception ex){
        Dev.ProcessException(ex);
    }
}

The connector object

The connector object resides with the server, and is a singleton. It's charged with sending command to the application. Any instance method that the client calls (through the proxy discussed above), is executed on the server. Note that this happens on a diffent thread, so we use the Control.Invoke() method as a synchronization mechanism.

using System;
using System.Windows.Forms;
using Gregor.Core;

public class CWebEditConnector : MarshalByRefObject {

    private delegate bool CommandRunner(string sCmd);
    
    public static int PORT = 8080;
    public static string URI = "WebEditConnector";
    public static string URL = "tcp://localhost:" + PORT + "/" + URI;
    
    public CWebEditConnector() {
        // do nothing (must not have parameters)
    }
    
    public bool IsAlive{
        get{
            // the application is alive if the client can successfully
            // call this property, and WebEdit.NET confirms it's alive
            return WebEditApp.IsAlive;
        }
    }

    public void ProcessCommandLine(string[] args){
        try{
            // delegate for executing command on remote main thread
            CommandRunner runner = new CommandRunner(WebEditApp.RunCommand);
            FMain frm = WebEditApp.MainForm;
            foreach(string sCmd in args){
                if(false == sCmd.StartsWith("/")){
                    frm.Invoke(runner, new object[]{sCmd});
                }
            }
        }catch(Exception ex){
            Dev.ProcessException(ex, true);
        }
    }

    public void ActivateApp(){
        try{
            FMain frm = WebEditApp.MainForm;
            // invoke Form.Show() on remote main thread
            MethodInvoker invoker = new MethodInvoker(frm.Show);
            frm.Invoke(invoker, new object[0]);
            // invoke Form.Activate() on remote main thread
            invoker = new MethodInvoker(frm.Activate);
            frm.Invoke(invoker, new object[0]);    	
        }catch(Exception ex){
            Dev.ProcessException(ex, true);
        }
    }

} // class CWebEditConnector