Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Extensible .NET remoting framework

0.00/5 (No votes)
30 Apr 2003 1  
This article is about installable transport connections

Abstract

The extensibility of the .NET remoting framework ends with the transports. There is much better reason to make the remoting framework communicate over transports other than TCP or HTTP. Because the transports are streams oriented, other ones like named pipes or RS232 could also be used in the .NET remoting framework. This article presents a generic channel framework that allows for the easy additions of any transport schemes.

Introduction

The .NET remoting framework was designed to be extensible. Client applications can communicate with remote servers by sending and receiving messages through channels. These consist of message sinks that are chained together. A message passes from one to another before transmittal across the network to a server. The remoting framework allows for the addition of custom message sinks as well as for the replacement of existing ones. The message, is in that way intercepted and eventually processed.

A channel normally consists of two message sinks, the formatter and the transport sinks. We could easily add additional message sinks by chaining them either before or after the formatter sinks. In fact we could replace the existing formatter sinks with one of our own.

One thing we cannot do is to replace the transport sink. The remoting framework presently offers only TCP and HTTP for transport. But we can, for example, combine a SOAP formatter with a TCP transport and a binary formatter with an HTTP transport. For any other type of transport, such as secure sockets, named pipes, or RS232 to name a few, a whole new channel type must be constructed from scratch. But this is not a too difficult task. Developers everywhere have created message channels based on alternative transports.

In this article I want to present a generic channel framework for the easy addition of alternative trans

The transport problem

On the client side, the transport channel sink is the last sink in the chain of sinks and on the server side it is the first sink starting a chain of sinks. By the time the message has reached the client transport channel sink, it must already be formatted or serialized into a stream, ready to be transmitted to a server. The server transport channel sink, receiving the serialized message, passes it up the chain of sink channels for deserializing and method invocation. The channel architecture is illustrated below. The client application communicates with the remote object by proxy. For every method invocation, the proxy forms a message object and passes it down the channel to the next sink. There it is formatted or serialized into a byte stream. That byte stream is passed to the transport sink from where it is transmitted via a connection to the server. Received by the transport sink on the server side the byte stream is forwarded to the formatter, where the message object is deserialized and then translated into the appropriate method call on the actual object.

Because alternative transports are about alternative connections, I have designed the generic channel framework to accept an installable transport connection. The connection object is not a channel sink but some object that is used by the channel transport sink.

The connection layer consists of a number of objects that implement a number of interfaces. A connection is assumed to be streams based and that is reflected in a basic ITransportStream interface. The client and server channels' transport sinks use it to send and receive the message stream.

public interface ITransportStream {
    void Read(out ITransportHeaders headers, out Stream stream);
    void Write(IMessage msg, ITransportHeaders headers, Stream stream);
    void Flush();
    void Close();
}

A transport stream is created and returned by the client and server�s transport connection objects. Calling a remote method generates a message that must eventually be processed by the client transport channel sink. So, a connection must be established before any message can be sent off to the server. A successful connection returns a transport stream.

public interface IClientTransportConnection {
    ITransportStream Connect(String uri);
}

The following code snippet illustrates how the client transport sink makes use of it.

// open the stream

ITransportStream transportStream = _transportConnection.Connect(_channelURL);

// send the request

transportStream.Write(msg, requestHeaders, requestStream);
transportStream.Flush();

// read the response

transportStream.Read(out responseHeaders, out responseStream);

// close the stream

transportStream.Close();

The server transport sink listens continuously for client connections.

public interface IServerTransportConnection {
    ITransportStream Accept();
}

Once it accepts a client connection it also returns a transport stream object. It then sets out to receive the message data and forwards it up the sink chain. The follwing code snippet illustrates that.

// read message data

transportStream.Read(out requestHeaders, out requestStream);
...
// invoke the object

ServerProcessing processing = NextChannelSink.ProcessMessage(sinkStack, 
    null, requestHeaders, requestStream, out responseMessage, 
    out responseHeaders, out responseStream);
...
// handle response

transportStream.Write("",responseHeaders, responseStream);
transportStream.Flush();

Implementation

The transport connection and stream can be implemented for any streams oriented transfer technology, sockets, named pipes, RS232 ports, etc. The enclosed project contains two implementations, one for named pipes and another one for sockets. A few considerations are in order here. The same client and server transport sinks are used for every remote method call and acceptance, to any server from any client. That should beg for some efficient reuse of existing connections. The client transport sink owns only one reference to a client transport connection. Therefore it is up to you to provide the transport connection object internally with a possible pool of open sockets, pipe handles, etc. The server transport sink loops continuously, assuming that one message after another is sent over the same connection. Once the response data has been flushed, it will proceed to read another request. It is therefore up to you to ensure that the repeated read request will time out in case no other data becomes available. When time out expires, you should throw an exception. Here is an implementation of the transportStream.Read() method.

public override void Read(out ITransportHeaders headers, out Stream stream) {
    headers = null;  stream = null;
    _socket.Receive(buffer, 0, 0, SocketFlags.Peek);
  
    if(_socket.Available > 0)
        base.Read(out headers, out stream);
    else
        throw new Exception();

Every new request is served by first reading the transport headers and message data stream. On a time out of a read attempt, no data is assumed to be available and an exception is thrown. This exception is caught right inside the request method which than terminates the loop of repeated requests.

try {
    // start a loop of repeated requests

    while(true) {
        // get the request, may throw an exception here

        transportStream.Read(out requestHeaders, out requestStream);
    
        // invoke the object

        ServerProcessing processing = NextChannelSink.ProcessMessage(sinkStack, 
            null, requestHeaders, requestStream, out responseMessage, 
            out responseHeaders, out responseStream);

        // handle response

        if(processing == ServerProcessing.Complete) {
            transportStream.Write(null, responseHeaders, responseStream);
            transportStream.Flush();
        }
    }
} 
catch(Exception exception) {
    if(exception is RemotingException)
        throw exception;
    // this just breaks the loop of repeated requests

    exception = exception; 
} 
finally {
    transportStream.Dispose();
}

This code is a shortened version, just to show how a request is ended.

Channel transport sinks

A channel transport connection plugs into a generic channel transport sink. You may find a good reason for using your own version of a transport sink. The generic channel framework allows you to do it. You can write your own channel transport sinks and plug them in just like you can plug in the transport connections, providing that the server channel transport sink implements the following Interface.

public interface IServerTransportSink : IServerChannelSink {
    void StartListening(Object data);
    void StopListening(Object data);
}

The methods StartListening and StopListening are also found as part of the IChannelReceiver interface. Most custom server channel implementation would put the listening code right into the ChannelReceiver. But because the server transport sink is first in line to receive the requests, it makes better sense to put the listening code there. You may look at the generic server channel, which implements the IServerTransportSink interface for guidance.

Usage and configuration files

To set up the channel section in the configuration file, you need to specify the channel type, and a transport connection or transport sink. Here is an example of two channels.

<channel
    type = "dotNET.Remoting.Channels.GenericChannel, GenericChannelsLibrary"
    name = "transport connection channel"
    server-transport-connection = "dotNET.Remoting.Transports.
        PipeServerTransportConnection, TransportStreams.dll"
    client-transport-connection = "dotNET.Remoting.Transports.
        PipeClientTransportConnection, TransportStreams.dll" />
</channel>
<channel
    type = "dotNET.Remoting.Channels.GenericChannel, GenericChannelsLibrary"
    name = "transport sink channel"
    server-transport-sink = "dotNET.Remoting.Transports.
        SoccketServerTransportSink, TransportSinks.dll"
    client-transport-sink = "dotNET.Remoting.Transports.
        SocketClientTransportSink, TransportSinks.dll" />
</channel>

The generic channel framework

There are a number of additional details that need to be considered. No default formatter sinks are supported. You need to specify the one you want. Here is a sample channel section.

<channel
    type = "dotNET.Remoting.Channels.GenericChannel, GenericChannelsLibrary"
    name = "transport connection channel"
    server-transport-connection = "dotNET.Remoting.Transports.
        PipeServerTransportConnection, TransportStreams.dll"
    client-transport-connection = "dotNET.Remoting.Transports.
        PipeClientTransportConnection, TransportStreams.dll" />

    <clientProviders>
    <formatter ref="binary" />
    </clientProviders>
    <serverProviders>
    <formatter ref="binary" />
    </serverProviders>
</channel>

Server channels and/or bi-directional channels must be provided with a localhost URL. This would be the port number in case of a sockets based channel, and a pipe name in case of a named pipe based channel. The generic channel framework expects the localhost URL to be of the following form: <protocol>://<hostname>:<port-name>. A localhost URL for a TCP based sockets channel might be this: tcp://localhost:8000. And for a named pipe based channel this: pipe://localhost:mypipe. The localhost URL must be added to the channel section.

<channel
    type = "dotNET.Remoting.Channels.GenericChannel, GenericChannelsLibrary"
    name = "transport connection channel"
    localhost-url = "pipe://localhost:mypipe"
    server-transport-connection = "dotNET.Remoting.Transports.
        PipeServerTransportConnection, TransportStreams.dll"
    client-transport-connection = "dotNET.Remoting.Transports.
        PipeClientTransportConnection, TransportStreams.dll">

    <clientProviders>
    <formatter ref = "binary" />
    </clientProviders>
    <serverProviders>
    <formatter ref=";binary" />
    </serverProviders>
</channel>

There is one more thing. A client application may register any number of channels, even multiple channels supporting the same transport protocol. And this is a reasonable thing to do, where two channel of the same transport protocol but differing formatters, binary and SOAP, are desired. The channels section in the client�s configuration file might look like this.

<channel
    type = "dotNET.Remoting.Channels.GenericChannel, GenericChannelsLibrary"
    name = "transport connection channel"
    localhost-url = "pipe://localhost:mypipe"
    server-transport-connection = "dotNET.Remoting.Transports.
        PipeServerTransportConnection, TransportStreams.dll"
    client-transport-connection = "dotNET.Remoting.Transports.
        PipeClientTransportConnection, TransportStreams.dll">

    <clientProviders>
    <formatter ref = "binary" />
    </clientProviders>
    <serverProviders>
    <formatter ref="binary" />
    </serverProviders>
</channel>
<channel
    type = "dotNET.Remoting.Channels.GenericChannel, GenericChannelsLibrary"
    name = "transport connection channel"
    localhost-url = "pipe://localhost:mypipe"
    server-transport-connection = "dotNET.Remoting.Transports.
        PipeServerTransportConnection, TransportStreams.dll"
    client-transport-connection = "dotNET.Remoting.Transports.
        PipeClientTransportConnection, TransportStreams.dll">

    <clientProviders>
    <formatter ref = "soap" />
    </clientProviders>
    <serverProviders>
    <formatter ref="soap" />
    </serverProviders>
</channel>

A reference to a remote object may now be obtained in the following way.

String url1 = "tcp://server1:9000/MyObject1";
IMyObject1 obj1 = (IMyObject)RemotingServices.Connect(typeof(IMyObject1), url1);

String url2 = "tcp://server2:9090/MyObject2";
IMyObject2 obj2 = (IMyObject)RemotingServices.Connect(typeof(IMyObject2), url2);

You might as well note this. The .NET remoting framework does not provide a means to specify the channel to be used for connection to remote objects. This is a typical problem for client applications. On the other end, the server end, this problem does not exist. The reason it does not exist is that, the connections are made with the server channels� listeners at very specific end points. The client application specifies the end points, e.g. tcp://server1:9000/MyObject1.

The generic channel framework was designed with this consideration in mind. Therefore one more piece of information need to be provided in the channels section.

<channel 
    type = "dotNET.Remoting.Channels.GenericChannel, GenericChannelsLibrary"
    name = "transport connection channel"
    localhost-url = "pipe://localhost:mypipe" 
    protocol = "soap-tcp"
    server-transport-connection = "dotNET.Remoting.Transports.
        TcpSocketServerTransportConnection, TransportStreams.dll"
    client-transport-connection = "dotNET.Remoting.Transports.
        TcpSocketClientTransportConnection, TransportStreams.dll">
    <clientProviders>
    <formatter ref = "soap" />
    </clientProviders>
    <serverProviders>
    <formatter ref = "soap" />
    </serverProviders>
</channel>

Using the generic channel framework seems like a very complex affair because of the many necessary pieces of information. This may be simplified with custom wrapper channel. A library of such is provided with the source code of this project. The channel section can be simplified. Here is an example.

<channel 
  type = "dotNET.Remoting.Channels.PipeChannel, CustomChannelsLibrary"
  localhost-url = "tcp://localhost:8080" >
</channel>

All other bits of information may be coded into the constructors of the custom wrapper channels. You may look at the source code for yourself.

Conclusion

There is quiet a bit of code in this project yet to be explained. I expect the reader to be familiar with .NET Remoting and its extensibility architecture in general. My desire is to contribute to the .NET Remoting, extensibility in a meaningful way. A critical examination of my code would certainly bring someone to suggest better implementations, especially with respect to scalability and performance. But I have been more focused on the architecture than on the other important things. I do hope that someone with a serious interest in .NET remoting and its extensibility will take a good look at my work and suggest some better ways. Send your comments to: wytek@szymanski.com

P.S.

Microsoft has done the world a great deal of good by making the .NET remoting framework to be so extensible. But this should not obviate any needs for Microsoft to improve the .NET remoting framework. I would like to mention a few of the problems I have come across.

As already mentioned in this article, there should be a way for client apps to specify the communication channel, as for example:

<client>
    <wellknown 
    type = "InterfaceLibrary.IFirstObject, InterfaceLibrary"
    url = "tcp://firstserver:9090/FirstObject"
    channel = "first tcp channel" />
    <wellknown 
    type = "InterfaceLibrary.ISecondObject, InterfaceLibrary"
    url = "tcp://firstserver:9000/SecondObject"
    channel = "second tcp channel" />
<client/>

Also, as I have pointed out earlier, for a connection from the client to the server, the server need not worry about the channel, as the end point will automatically select the right channel. But another similar problem appears in the case of a callback from the server to the client.

A callback is a remote reference to an object that is passed as an argument to a remote method call. For example:

void CallMe(SomeObject callback) {
    callback.Call(�Hello�);
}

This code executes on the server and it must be marshaled through some channel. Which channel will it be marshaled through, if the server has more than one registered?

When the .NET remoting framework creates a remote object reference for the callback object it attaches to it some channel data. Inside the channel data there is a list of URLs, one for each channel registered on the client. During callback then, the framework iterates through the list of channels and picks the first best available on the server. This leads to the case where the client calls into the server through a HTTP-SOAP channel, but the server calls back through a TCP-binary channel. That is not a desirable effect and can make the development of distributed application very complicated.

There is also the basic principal of distributed computing, which the .NET framework does not strictly adhere to. Why do we need to deploy the remote assemblies on the client�s machine as well? In my view, this is an inelegant matter and it does not make it elegant by having to go through the hoops of building interface libraries, additional class factories, and other workarounds. This is unlike what we have come to expect from COM/DCOM, CORBA, RPC, and Java RMI.

Otherwise, I find the .NET remoting framework to be much better than any other distributed computing framework. There is however one thing that has impressed me immensely about Java RMI, namely the dynamic class loader. There is nothing like that in the .NET remoting framework. This feature can open up a whole new class of applications. Let me briefly describe what can be done with Java RMI.

Assuming there is a remote java server defined like so.

public interface Computable {
    public Object Compute();
}

public interface ComputServer {
    public Object Compute(Computable obj);
}
  
public class ComputeServerImpl extends 
    UnicastRemoteObject implements ComputeServer { 

    public Object Compute(Computable obj) {
        return obj.Compute();
    }
}

And on the client end there is an object implementing the interface Computable.

public class ComputableObject extends 
    Serializable implements Computable { 
    
    public Object Compute() { 
    // all the complex computation goes here

    ...
    return this;
}

The object is marshaled to the server by value, not by reference. So, with the class definition of the computable object only available to the client but not to the server, the Java RMI framework will automatically and dynamically download the class bytes from the client and than execute it on the server, really cool stuff.

Exploiting such a feature could lead to a whole new world in distributed computing. I would coin it, distributed agent computing. Imagine a network of distributed compute servers. A client might send an agent to one server for information, that server might forward the request to another server and so on. Eventually the response would be sent back to the client but with the cooperation of all servers in the network. This new way of computing would be indeed revolutionary, the much touted web services industry would begin to make sense then. If you are seriously interested here, let me know at wytek@szymanski.com.

List of assemblies

GenericChannels Library

This assembly contains the core client and server channels and also the interface definitions for the transport connections and the transport sinks. It references nothing besides the system assemblies.

TransportStreams

This assembly contains the transport connections for a named pipe and a TCP socket. It references the GenericChannels library.

CustomChannelsLibrary

This assembly contains a few wrappers around generic channels. It references the GenericChannels library and the TransportStreams library.

InterfaceLibrary

This is an interfaces only library. It couples the client to the server.

TestLibrary

This assembly contains one object to test the framework. It references the Interface library.

Client

This is a client application for testing. It references the GenericChannels  library, the TransportStreams  library, and the Interface library.

Server

This is a server application for testing. It references the GenericChannels library, the TransportStreams library, and the TestLibrary library.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here