Click here to Skip to main content
Click here to Skip to main content

The case for a channel-schema concept

, 17 May 2003
Rate this:
Please Sign up or sign in to vote.
An article about .NET remoting channel schemas
<!-- Add the rest of your HTML here -->

Abstract

A .NET remoting channel can be extended in many different ways, providing encryption, transformation, logging, etc. For a client that wants to communicate with many remote servers there is no direct way to choose the appropriate channel for the appropriate remote server. A channel is a chain of sinks where each sink serves a purpose. Thinking of channel schemas helps to distinguish one channel from the other.

Introduction

The .NET remoting framework provides that clients and servers use channels for the exchange of messages. Two principal parts must always be considered when selecting a channel, the formatter sink and the transport sink, together constituting a channel schema. The .NET remoting framework readily provides two channel schemas, one that employs a binary formatter with a tcp transport, and another that employs a soap formatter with an http transport.

The concept of a .NET remoting channel schema is regrettably not well accepted in the .NET remoting framework. But combining the out-of-the-box transports and formatters four channel schemas can be established.

  1. Tcp transport and binary formatter
  2. Http transport and soap formatter
  3. Tcp transport and soap formatter
  4. Http transport and binary formatter

The third and fourth channels are obtained by setting the configuration entries like so:

<channels>
  <channel name="Two way http-binary server channel" port="9090" ref="http">
    <clientproviders>
      <formatter ref="binary" />
    </clientproviders>
    <serverproviders>
      <formatter ref="binary" />
    </serverproviders>
  </channel>
            
  <channel name="Two way tcp-soap server channel" port="9000" ref="tcp">
    <clientproviders>
      <formatter ref="soap" />
    </clientproviders>
    <serverproviders>
      <formatter ref="soap" />
    </serverproviders>
  </channel>
</channels>

But why use channel schemas other then the out-of-the-box ones? The Tcp channels are touted for better performance because they employ the binary formatter that serializes the messages into much smaller streams. The Http channels are touted for their ability to get through firewalls. It makes good sense to combine an Http channel with a binary formatter. Indeed, the performance of an http-binary channel is as good as a tcp-binary channel. Either channel transfers the message data by sending or receiving a series of transport headers together with the serialized message data. The only difference is that the http channel arranges and decorates the transport headers in compliance with the http protocol whereas the tcp channels decorate the transport headers with a signature, version number, and other magic numbers. Finally, it is all TCP/IP or sockets that transfers the messages across the network.

In one of my previous articles I have described the dilemma of picking a channel when multiple channels all of the same transport but with different formatters are registered. Read my previous article here. In this article I will revisit that problem but in a somewhat more thorough way.

The bi-directional channel dilemma

A client application typically registers bi-directional channels, e.g. TcpChannel or HttpChannel , if it wants to pass a callback object. Here is some sample client code.

//implemented on the server
public RemoteObject : MarshalByRefObject {
    public void CallMe(Callback cb) {
        String ret = cb.Callback(“Hello”);
    }
}

// implemented on the client
public CallbackObject : MarshalByRefObject {
    public String Callback(String msg) {
        return "Message received: " + msg;
    }
}

CallbackObject callbackObject = new CallbackObject();

String url = "http://localhost:8000/KnossosObject.rem";
RemoteObjec remoteObj = 
  (RemoteObject)RemotingServices.Connect(typeof(RemoteObject), url);

// call the remote server
RemoteObject.CallMe(callbackObject);

To make this work, the client and server must each register at least one bi-directional http channel. The client and server could than be calling each in turn using that same channel. But what if the client and the server have each registered two or more bi-directional channels? This is a very realistic scenario where a client needs to talk to two different servers and where the servers may have to serve their clients over different channel. The interesting question is: Would a client that calls a server via the http channel also be called back via that same http channel? The answer is: If client and server each have registered a TcpChannel and an HttpChannel there is a fair chance that the server may use a different channel to execute the callback on, possibly presenting a dilemma where a client initiates the communication via the faster TcpChannel yet the server calls back the client via the slower HttpChannel. To see why this may happen let me explain how the server actually learns about the client’s callback address.

A callback object is always a subclass of MarshalByRefObject. When it is passed as a parameter to a method call it is marshalled into a serializable reference object. Once the client side formatter realizes that a method parameter is an object of type MarshalByRefObject it converts it to an ObjRef before serializing it to the message stream. This conversion is performed by the RemotingServices. One effect of the conversion is that the ObjRef type object has been provided with channel data that has embedded in it the addresses of every registered server channel.

// create the callback object
CallbackObject callback = new CallbackObject();

// convert to ObjRef
ObjRef objRef = RemotingServices.Marshal(callback);

// get at the channel data
Object[] channelData = objRef.ChannelInfo.ChannelData;

foreach(Object data in channelData) {

    // look for a ChannelDataStore
    if(data is ChannelDataStore) {
        // get at the channel URIs
        String[] channelUris = ((ChannelDataStore)data).ChannelUris;

        // print each uri, the client’s server channel address
        foreach(String uri in channelUris)
            Console.WriteLine(uri); // 
    }
}

If two http channels have been registered than this is the output showing the local endpoints with identical network addresses but different listening ports. This is part of the channel data that the server receives on the other side. The server must choose one of the URLs when calling the client back.

http://192.168.1.100:8000
http://192.168.1.100:8080

The RemoringServices.CreateEnvoyAndChannelSinks method makes this choice on behalf of the server. Before connecting to the server, the RemotingServices attempts to create a message sink. It does so by presenting the embedded channel data to the ChannelServices.CreateMessageSink method. The code snippet below illustrates this and it is actual source code of the .NET remoting framework.

// RemotingServices.CreateEnvoyAndChannelSinks 

// Extract the channel from the channel data and name embedded
// inside the objectRef
Object[] channelData = objectRef.ChannelInfo.ChannelData;
if (channelData != null) {
   for (int i = 0; i < channelData.Length; i++)
   {
      // Get the first availabe sink           
      chnlSink = ChannelServices.CreateMessageSink(channelData[i]);
      if(null != chnlSink)
      {
         break;
      }
   }
}

Eventually, the ChannelServices tries every registered sender channel. Here is the actual source code of the .NET remoting infrastructure.

// ChannelServices.CreateMessageSink
internal static IMessageSink CreateMessageSink(Object data)
{
   String objectUri;
   return CreateMessageSink(null, data, out objectUri);
} // CreateMessageSink
               
internal static IMessageSink CreateMessageSink(String url, 
                                          Object data, out String objectURI) 
{
   IMessageSink msgSink = null;

    RegisteredChannelList regChnlList = s_registeredChannels;
    int count = regChnlList.Count;
            
    for(int i = 0; i < count; i++)
    {
        if(regChnlList.IsSender(i))
        {
            IChannelSender chnl = (IChannelSender)regChnlList.GetChannel(i);
             msgSink = chnl.CreateMessageSink(url, data, out objectURI);
                    
             if(msgSink != null)
                break;
        }
    }
       
    return msgSink;
} // CreateMessageSink

A message sink may be created providing that the URL is one of the known types, one that starts with either "tcp://" or "http://". If, for example, two http sender channels are registered than the first one that matches the "http://" at the starts of the URL will create the message sink. The problem here is that the first best sender channel may not be provided with the desired formatter. That is one crux of my channel-schema proposition. Register any two channel of the same type, one provided with a soap formatter the other provided with a binary formatter, and the system will become confused.

The other crux of my channel-schema proposition lies in the fact that the bi-directional channels are not very bi-directional. In fact, the TcpChannel or the HttpChannel are only loose associations of the client and server channels, or just a convenient short cut to registering the client and server channels explicitly. For example, the following configuration file entries are equivalent.

<channel ref="tcp" />

// or alternatively

<channel ref="tcp client" />
<channel ref="tcp server" />

Let us assume that the client and the server have registered one tcp channel and one http channel each.

// the client’s config entries
<channel ref="tcp" />
<channel ref="http" />

// the servers’s config entries
<channel ref="http" />
<channel ref="tcp" />

And considering the client code like so.

String url = "tcp://remoteserver:8000/MyObject.rem";
MyObject obj = (MyObject)RemotingServices.Connect(typeof(MyObject), url);

// create a callback object, one that derives from MarshalByRefObject
CallbackObject callback = new CallbackObject();

// and make a call
obj.CallMe(callback);

The URL will cause the client to select a tcp channel with its default binary formatter. The callback object will eventually transfer embedded in its channel data the URLs for the tcp and http server channels, which are two URLs. When the server calls back we would naturally want it to call back on the same channel that the request was initiated first, that was the tcp channel. But a message sink must be created on the server’s side before any callback can be made. And, as we have seen before, the remoting services will create the first best message sink that matches the first best URL. In other words, the server’s callback may be executing on the other channel, the http one. Add to it additional channels with alternate formatters and the whole thing becomes even more confusing. The concept of a channel schema, in my opinion, is thoroughly justified.

A channel schema implementation

As I have already pointed out, the existing framework does not offer the opportunity to specify a particular communication channel. So, I have tried to provide one. Please download the source code to inspect the implementation.

The implementation involves a client channel sink provider that is hooked as an interceptor into the client provider chain. After some experimenting and thinking, I decided to modify the channel information in the URL like this.

http-binary://server:port/objectUri
http-soap://server:port/objectUri

tcp-binary://server:port/objectUri
tcp-soap://server:port/objectUri

The installed and intercepting sink providers could easily recognize the desired schema and create the appropriate message sinks. But the sender channels reject anything but “tcp://” and http://. The solution to this is a custom implementation of the bi-directional tcp and http channels. The modified URL can in this way be intercepted and rewritten to the expectation of the standard client channels.

// IChannelSender interface
public IMessageSink CreateMessageSink(
    String url, 
    Object remoteChannelData,
    out String objectUri) {

    if(url != null) {
    
        // client sending a request to server
        // remove the formatter part 'binary'
        // as in 'tcp-binary://server:port/object.rem'
        int index = url.IndexOf("://");
        String schema = url.Substring(0, index);
        int n = schema.IndexOf('-');
        if(n != -1) {
             url = schema.Substring(0, n) + url.Substring(index);
             remoteChannelData = schema.Substring(n);
        }
    }
    else
    if(remoteChannelData is ChannelDataStore) {
        
        // server calling the client back
        ChannelDataStore channelDataStore = 
            (ChannelDataStore)remoteChannelData;
        url = channelDataStore.ChannelUris[0];
        
        // remove the formatter part 'binary'
        // as in tcp-binary://server:port/object.rem
        int index = url.IndexOf("://");
        String schema = url.Substring(0, index);
        int n = schema.IndexOf('-');
        if(n != -1) {
            url = schema.Substring(0, n) + url.Substring(index);
            String[] channelURIs = new String[] { url, schema.Substring(n) };
            remoteChannelData = new ChannelDataStore(channelURIs);
        }
     }

     // pass it on to the standard TcpClientChannel/HttpClientChannel
     return _clientChannel.CreateMessageSink(url, remoteChannelData, 
                                             out objectUri);
} // CreateMessageSink

The trick here is to pass the formatter specification as a remoteCchannelData parameter to the CreateSink method, as I had already described in one of my previous articles.

public IClientChannelSink CreateSink(
    IChannelSender channel,
    String url,
    Object remoteChannelData) {

    int index = url.IndexOf(':');
    if(index == -1)
        throw new RemotingException("No channel url specified.");

    String channelSchema = url.Substring(0,index);
    String formatter = remoteChannelData as String;
    if(formatter == null) {
        // this is the server's callback 
        // take the hint from the ChannelDataStore
        ChannelDataStore channelDataStore =
            remoteChannelData as ChannelDataStore;
        if(channelDataStore == null)
            throw new RemotingException("Cannot identify channel schema.");
            
        formatter = channelDataStore.ChannelUris[1];
    }

    channelSchema += formatter;
    if(String.Compare(channelSchema, _channelSchema, true) != 0)
        return null; // channel schema does not match  

    IClientChannelSink nextSink = null;
    if(_nextSinkProvider != null) {
        nextSink = _nextSinkProvider.CreateSink(channel, url, 
                                                remoteChannelData);
        if(nextSink == null)
            return null;
    }

    return new ClientChannelSink(nextSink, _serverUrl);
}

All the trickery is about passing the formatter information around.

To be certain that the server will make the callback to the same channel that the initial request was posted, the client channel must examine the method arguments before the request is posted. For every method argument that is an object derived from MarshalByRefObject the channel data is rewritten so that only the desired channel address is made available. In this way we can be certain that the server will post the callback on the same channel that the initial request was made. Here is the relevant code of the client channel sink.

public IMessage SyncProcessMessage(IMessage msg)
{
    msg = ModifyChannelData(msg);
    return NextSink.SyncProcessMessage(msg);
}

IMessage ModifyChannelData(IMessage msg) {

    IMethodCallMessage mcm = msg as IMethodCallMessage;
    if(mcm == null)
        return msg;

    Object[] args = mcm.InArgs;
    for(int i=0; i < args.Length; i++)
        if(args[i] is MarshalByRefObject) {

            // Any method parameter that is a MarshalByRefObject 
            // is a callback facility
            // so we want to be sure that the desired channel schema 
            // is used when the 
            // callback is made.
            ObjRef objRef = 
                RemotingServices.Marshal((MarshalByRefObject)args[i]);

            Object[] data = objRef.ChannelInfo.ChannelData;

            for(int j=0; j < data.Length; j++) {

                if(data[j] is ChannelDataStore) {
                    data[j] = new ChannelDataStore(_channelURIs);
                    break;
                }
            }
        }
    


     String url = msg.Properties["__Uri"] as String;

     int index = url.IndexOf("://");
     if(index != -1) {
         // the client makes a request to the server
         // rewrite the url to remove the formatter spec
         // e.g. remove 'binary' from http-binary://server:port/MyObject'

         String formatter = url.Substring(0, index);
         int n = formatter.IndexOf('-');
         if(n != -1) {
             String newUrl = formatter.Substring(0, n) + 
                 url.Substring(index);

             MethodCallMessageWrapper newMsg = 
                 new MethodCallMessageWrapper(mcm); 
             newMsg.Properties["__Uri"] = newUrl;
             msg = newMsg;
        }
     }

     return msg;
}

The configuration file entries might be as follows.

<channels>
    <channel id="http" 
         type="Custom.Remoting.Channels.HttpChannel, TwoWayChannel" />
    <channel id="tcp" 
         type="Custom.Remoting.Channels.TcpChannel, TwoWayChannel" />
</channels>
 
<application>
    <channels>
        <channel ref="http" port="8000" channel-schema="http-binary" 
            name="Two way http-binary channel" />
        <channel ref="http" port="9000" channel-schema="http-soap" 
            name="Two way http-soap channel" />
        <channel ref="tcp" port="8080" channel-schema="tcp-binary" 
            name="Two way tcp-binary channel" />
        <channel ref="tcp" port="9090" channel-schema="tcp-soap" 
            name= "Two way tcp-soap channel" />
        
        <channel ref="http" port="9876" channel-schema= "http-secure" 
            name="Two way http-secure channel" >
            <clientProviders>
                <formatter ref="binary" />
                <provider
                    type= 
            "MsdnMag.Remoting.SecureClientChannelSinkProvider, SecureChannel"
                    algorithm="DES" oaep="false" maxRetries="1"
                />
            </clientProviders>
            <serverProviders>
                <provider
                    type=
            "MsdnMag.Remoting.SecureServerChannelSinkProvider, SecureChannel"
                    algorithm="DES" oaep= "false requireSecurity="true" 
                    securityExemptionList="127.0.0.1; 207.46.230.220" 
                    connectionAgeLimit="120" sweepFrequency="60" 
                 />
                <formatter ref="binary" />
            </serverProviders> 
        </channel>
    </channels>
</application

Conclusion

The soap formatter outputs much larger data streams than binary formatters do. On the other hand, the http channels are preferred where a connection must pass through a firewall. Assigning a binary formatter to an http channel is therefore one way to improve the performance. But the channel schema concept is not just about performance. There are also other ways to improve the performance of an http channel. One other way is to compress/decompress the soap formatted text stream.

The channel schema concept is really about clients that want to communicate with many different servers. Be it a tcp channel or an http channel, the extensibility of the .NET remoting framework invites many different types of extensions serving many purposes, e.g. encryption, logging, transformation. There is presently no official way to identify the channel that a client should use to connect to a remote object. I do hope that the .NET remoting team at Microsoft will do something about it in a future release of the framework.

Project

TwoWayChannel
This assembly is the core. It implements the channel schema.

SecureChannel
Microsoft developed this assembly. It contains the client and server channel sinks for secure communications. This project is part of an article in the June MSDN magazine.

TestLibray
This assembly contains an object for testing

RemotingClient
This is a client application for demo purposes.

RemotingServer
This is the server application for demo purposes.

P.S.

The object of my work is to identify the limits of what I otherwise believe is a fine framework for distributed computing. You may review the code I have written and most like find it not to be good enough to be deployed in a production environment. But I will always appreciate your comments, suggestions, and tips on how to make things better. Please, be free to provide your feed-back.

License

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

Share

About the Author

Wytek Szymanski
Web Developer
United States United States
I am a consultant, trainer, software archtect/engineer, since the early 1980s, working in the greater area of Boston, MA, USA.
 
My work comprises the entire spectrum of software, shrink-wrapped applications, IT client-server, systems and protocol related work, compilers and operating systems, and more ....
 
I am currently focused on platform development for distributed computing in service oriented data centers.

Comments and Discussions

 
QuestionHow can one contact Wytek Szymanski, the author of this arcticle? PinmemberYossiMimon1-May-08 23:59 
Generalsingle client n multiple servers Pinmembersamtam28-Jul-06 21:56 
GeneralThanks PinmemberDaniel Vaughan16-Aug-05 14:39 
GeneralSome questions PinmemberDiablo_m5-Aug-05 4:05 
QuestionCan it be used for this? Pinmemberstuartporter10-Nov-04 6:27 
GeneralGreat article PinmemberAdrianAlexan16-Apr-04 2:53 
GeneralRemoting PinsussAnonymous22-Jul-03 9:39 

Hello Mentor,
I want dynamic registering in one application, first on 1234
port, next 1250 port, how to, please help !
 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140821.2 | Last Updated 18 May 2003
Article Copyright 2003 by Wytek Szymanski
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid