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:
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:
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.
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:
public ServerProcessing ProcessMessage(
IServerChannelSinkStack sinkStack,
IMessage requestMsg,
ITransportHeaders requestHeaders,
Stream requestStream,
out IMessage responseMsg,
out ITransportHeaders responseHeaders,
out Stream responseStream)
{
string requestUri = (string)requestHeaders["__RequestUri"];
ChannelObject channelObject =
ChannelManager.ChannelObjects[requestUri];
requestMsg.Properties["__Uri"] = channelObject.Uri;
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:
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.
string services = ConfigurationManager.AppSettings["services"];
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
:
class ChannelObject :
MarshalByRefObject,
IDisposable
{
string absoluteUri;
IMessageSink messageSink;
public ChannelObject(Uri uri)
{
this.absoluteUri = uri.AbsoluteUri;
foreach (IChannel channel in ChannelServices.RegisteredChannels)
{
if (channel is IChannelSender)
{
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);
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
.