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.
ITransportStream transportStream = _transportConnection.Connect(_channelURL);
transportStream.Write(msg, requestHeaders, requestStream);
transportStream.Flush();
transportStream.Read(out responseHeaders, out responseStream);
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.
transportStream.Read(out requestHeaders, out requestStream);
...
ServerProcessing processing = NextChannelSink.ProcessMessage(sinkStack,
null, requestHeaders, requestStream, out responseMessage,
out responseHeaders, out responseStream);
...
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 {
while(true) {
transportStream.Read(out requestHeaders, out requestStream);
ServerProcessing processing = NextChannelSink.ProcessMessage(sinkStack,
null, requestHeaders, requestStream, out responseMessage,
out responseHeaders, out responseStream);
if(processing == ServerProcessing.Complete) {
transportStream.Write(null, responseHeaders, responseStream);
transportStream.Flush();
}
}
}
catch(Exception exception) {
if(exception is RemotingException)
throw exception;
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() {
...
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.