Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

.NET Remoting Relay Server

4.60/5 (2 votes)
5 Jul 2006CPOL4 min read 2   399  
An article about re-deploying remote services with the help of a relay server.

Introduction

Imagine that you have developed a .NET remoting server that exposes multiple services. And a number of clients out there rely upon your services to be available at a certain endpoint. Say, your clients are accustomed to address your services like so:

C#
string url1 = "tcp://phaestos:1234/Service1.rem";
string url2 = "tcp://phaestos:1234/Service2.rem";

Your server channel listens on machine PHAESTOS at port 1234. Two (or more) well known services are available there. For some good reason, you decide to re-deploy your services onto two other machines, named KNOSSOS and ZAKROS, so that one remote service can be deployed on KNOSSOS and the other service can be deployed on ZAKROS. Thus, the new endpoints of your services become:

C#
string url1 = "tcp://knossos:1234/Service1.rem";
string url2 = "tcp://zakros:1234/Service2.rem";

Seems easy enough to do. But there is just one problem. Your clients have no idea that the services have been re-deployed on different hosts. They continue to address the services on the former host, PHAESTOS. That creates the need to re-direct every request from PHAESTOS to KNOSSOS and ZAKROS, respectively. The former service host must now act as a relay server.

I will explain in this article how to create such a relay server.

A .NET Remoting Relay Server

The relay server requires a special ServerChannelSink that we can build and insert into the server's inbound communication channel. No other complementary ClientChannelSink is required. The ServerChannelSink provides us with an opportunity to intercept a client's communication call.

When a client communicates, a message is transmitted through a channel. A channel consists of multiple channel sinks, the transport sink, the formatter sink, and any possible additional custom sinks. Our ServerChannelSink is that critical custom sink which will help us relay the inbound message to the final destination server.

We can intercept the transmitted message inside the channel when it becomes available as a request stream, or after deserialization by the formatter into a Message object. Once intercepted, the message must be transferred into an outbound channel, for delivery to the final destination.

The following diagram depicts two different configurations. The first configuration places a custom channel sink C right after the formatter sink F on the inbound side of the relay server. The second configuration places the custom channel C between the transport sink T and the formatter sink F. Either configuration is essentially a bypass system.

Sample Image

To make this bypass system work, we identify the inbound call and select a corresponding outbound channel. Each inbound call can be identified by inspecting the request transport header for its __RequestUri property value. The relay server maintains a mapping of the request URI to a special ChannelObject that encapsulates a reference to an outbound MessageSink and a destination URI.

The following code snippet briefly illustrates how it works:

C#
public ServerProcessing ProcessMessage(
    IServerChannelSinkStack sinkStack,
    IMessage requestMsg,
    ITransportHeaders requestHeaders,
    Stream requestStream,
    out IMessage responseMsg,
    out ITransportHeaders responseHeaders,
    out Stream responseStream)
{
    // inspect the __RequestUri
    string requestUri = (string)requestHeaders["__RequestUri"];
    
    // use the __RequestUri to lookup a ChannelObject
    ChannelObject channelObject = 
            ChannelManager.ChannelObjects[requestUri];

    // assign the destination uri to the requestMesg
    requestMsg.Properties["__Uri"] = channelObject.Uri;
    
    // call the outbound MessageSink to dispatch the requestMsg
    return channelObject.MessageSink.SyncProcessMessage(requestMsg);
}

The ChannelObject is something the relay server provides and constructs from the URLs of the remote services. As we have seen before:

C#
string url1 = "tcp://knossos:1234/Service1.rem";
string url2 = "tcp://zakros:1234/Service2.rem";

it is not important how the relay server initially obtains these URLs. The sample projects included with this article simply read it from the App.config file.

C#
// simply read the application services endpoints from the config file
string services = ConfigurationManager.AppSettings["services"];

// create the connection objects and add them to the list of connections
foreach(string service in services.Split(new char[] {','}))
{
    Uri uri = new Uri(service.Trim());
    ChannelManager.ChannelObjects.Add(uri.LocalPath, 
                            new ChannelObject(uri));
}

The interesting point is how a ChannelObject obtains a reference to an outbound MessageSink. Here is the complete class definition of the ChannelObject:

C#
class ChannelObject : 
    MarshalByRefObject,
    IDisposable
{
    
    string absoluteUri;
    IMessageSink messageSink;

    public ChannelObject(Uri uri)
    {
        // save the absolute uri for later
        // assignment to the request message
        this.absoluteUri = uri.AbsoluteUri;

        // loop through all sender channels
        // to find one that fits the scheme, e.g. 'tcp'
        foreach (IChannel channel in ChannelServices.RegisteredChannels)
        {
            if (channel is IChannelSender)
            {
                // create and save a MessageSink if possible
                IChannelSender sender = (IChannelSender)channel;
                if (string.Compare(sender.ChannelName, uri.Scheme) == 0)
                {
                    string objectUri;
                    this.messageSink = sender.CreateMessageSink(
                          this.absoluteUri, null, out objectUri);
                    if (this.messageSink != null)
                        break;
                }
            }
        }

        if (this.messageSink == null)
            throw new ApplicationException("No channel found for " + 
                                           uri.Scheme);

        // strip the '/' character from the objectUri
        RemotingServices.Marshal(this, uri.LocalPath.Substring(1));
    }

    public string Uri
    {
        get { return this.absoluteUri; }
    }

    public IMessageSink MessageSink
    {
        get { return this.messageSink; }
    }

    public void Dispose()
    {
        RemotingServices.Disconnect(this);
    }
}

Inside the constructor, three important things happen. First, we retain the absolute URI of the remote service. Second, we create an outbound MessageSink. Third, we marshal the ChannelObject in the domain of the relay server as a stand-in for the remote service. The clients call as if the relay server is the host of a remote service, which in fact it is not. Having to marshal the ChannelObject appears rather redundant. We do it to satisfy the conditions of the .NET remoting infrastructure. Without it, there would be an exception indicating that the remote service was not found.

The second bypass configuration works in a similar way. The difference is that the formatter sinks are bypassed. The ChannelObject references an outbound TransportSink instead of an outbound MessageSink. Another important difference is bypassing the formatter sinks we no longer need, to provide to the relay server the shared assembly containing the common interfaces and data structures of the remote service. The first configuration requires a shared assembly. Otherwise, the formatter sinks would not know how to deserialize/serialize the messages. By not using the formatters, we transfer the request stream from the inbound channel to the outbound channel.

Conclusion

While writing this article, I wanted to be as brief as possible. I hope that you will download the source code to obtain a complete understanding of these techniques. As for the benefits of using these techniques, I admit that one has to be involved in the proper engineering of a distributed computing environment. A good distributed computing environment must be dynamic. You can never insist that an application or service will always reside on the same host.

If you are trying to solve similar problems, let me know about it. I am always interested to learn from you about alternative or better ways.

Using the sample project

The sample project is a Visual Studio 2005 solution. Download it, compile it, and start the various components. There is an ApplicationServer, an ApplicationClient, and two relay servers, RelayServer1 and RelayServer2. First, start the ApplicationServer, then start one of the relay servers, say RelayServer1, and finally, start the ApplicationClient.

License

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