Creating Custom Channels

Creating Custom Channels

The .NET Remoting infrastructure exposes interfaces and an architecture that allows us to plug our own custom channels into the runtime. Many reasons to create custom channels exist. For instance, you might want to make remote method calls between two computers that can communicate only over a phone line. A custom channel would be the perfect solution to this problem: you could configure the server and client channel sinks to connect and disconnect when they have messages to send. Here are some other examples of possible custom channels:

  • User Datagram Protocol (UDP) channel

  • Named-pipe channel

  • File channel

  • <insert obscure protocol> channel

You’re probably thinking, “With the built-in channels, HttpChannel and TcpChannel, why would I need these?” Consider situations in which the client or server is on a system that doesn’t support TCP or HTTP. On a system that doesn’t have the .NET common language runtime, you’d have to generate the messages yourself. As long as the messages are in the proper format, the receiving or sending channel won’t know the difference. This is incredibly powerful!

The Steps for Creating a Custom .NET Remoting Channel

This section introduces a set of steps that will guide you in creating a custom .NET Remoting channel. The steps are transport agnostic; therefore, you can apply them to the creation of any channel, regardless of the transport selected. For example, assume that our transport for the high-level steps is the hypothetical Widget transport. Armed with the Widget transport and following the naming convention of the stock .NET Remoting channels, we arrive at our channel name, WidgetChannel. Here is a list of steps that we’ll follow in creating our channel:

  1. Create the client-side channel classes. The client-side channel consists of three classes respectively derived from IChannelSender, IClient­ChannelSinkProvider, and IClientChannelSink:

    public class WidgetClientChannel : IChannelSender
    {
         
    }
    internal class 
      WidgetClientChannelSinkProvider : IClientChannelSinkProvider
    {
         
    }
    public class WidgetClientChannelSink : IClientChannelSink
    {
         
    }

  2. Create the server-side channel classes. The server side contains two classes that respectively derive from IChannelReceiver and IServerChannelSink:

    public class WidgetServerChannel : IChannelReceive
    {
         
    }
    public class WidgetServerChannelSink : IServerChannelSink
    {
         
    }

  3. Create a helper class named WidgetChannelHelper. This class houses the shared functionality between the server and client channel classes.

  4. Create the main channel class that combines the functionality of both the server and client classes:

    public WidgetChannel : IChannelSender, IChannelReceiver
    {
        private WidgetClientChannel = null;
        private WidgerServerChannel = null;
         
    }

Now that we’ve gone over the basic steps of creating a custom .NET Remoting channel, let’s create one!

Creating the Custom Channel FileChannel

FileChannel is a .NET Remoting channel that uses files and directories to move messages between the server and client. The purpose of using something as rudimentary as files for the channel transport is to show the flexibility of .NET Remoting. For example, we could remote an object between two computers with a floppy disk! Now that’s flexibility. Flexibility isn’t the only benefit of FileChannel. Because file operations are so familiar, we can focus on the details of custom channel creation without the distraction of implementing a complex transport mechanism. Finally, FileChannel request and response messages remain in the server directory after processing. This leaves a chronological history of the message interaction between the server and client for diagnostic and debugging purposes.

FileChannel Projects

The sample code for the FileChannel has the following projects:
  • FileChannel

    Contains the implementation of the FileChannel.

  • DemonstrationObjects

    Contains a class named Demo. Our example client and server will be remoting an instance of the Demo class.

  • Server

    Registers the FileChannel with channel services. This project then simply waits for the user to terminate the server by pressing Enter. In the configuration file, the server is told to watch the directory C:\File.

  • Client

    Like the server project, this project registers the FileChannel. The project then demonstrates various types of method calls.

Implementing the FileClientChannel Class

Because FileClientChannel derives from the interface IChannelSender, we must implement the method CreateSink. In addition to CreateSink, we must add the ChannelName property, the ChannelPriority property, and the Parse method. These last three members are required because IChannelSender derives from IChannel. Adding these members will give us a basic starting point for FileClientChannel.

public class FileClientChannel : IChannelSender
{
    private String m_ChannelName = "file";
    private int m_ChannelPriority = 1;

    private IClientChannelSinkProvider m_ClientProvider = null;

    IClientChannelSinkProvider m_ClientProvider = null;
    public IMessageSink CreateMessageSink( String url, 
                                           object remoteChannelData, 
                                           out String objectURI )
    {
         
    }

    public String Parse( String url, out String objectURI )
    {
        return FileChannelHelper.Parse( url, out objectURI );
    }

    public String ChannelName
    {
        get { return m_ChannelName; }
    }

    public int ChannelPriority
    {
        get { return m_ChannelPriority; }
    }
}

Notice that we added two methods and two properties to our new class. All four of these members are required for channels that send request messages. To store the values returned by ChannelName and ChannelPriority, we added three new private fields: m_ChannelName, m_ChannelPriority, and m_ClientProvider. The m_ChannelName and m_ChannelPriority fields are initialized with default values that can be overridden by one of the constructors. The m_ClientProvider variable holds a reference to the sink provider. The Parse method calls the static method Parse on the class FileChannelHelper. The reason for moving the parse functionality into a separate class is to allow FileClient­Channel and FileServerChannel to share the implementation. We’ll take a closer look at Parse later in the chapter.

Next we need to add two constructors to FileClientChannel. The first constructor will set up a basic channel that uses SOAP as its formatter:

public FileClientChannel()
{
     
}

The second constructor allows you to customize the channel with an IDictionary object and to create the channel via a configuration file. To meet the requirements for configuration file support, the constructor must have a parameter that takes an instance of an IClientChannelSinkProvider object. The following constructor meets our requirements:

public FileClientChannel( IDictionary properties, 
                          IClientChannelSinkProvider sinkProvider )
{
    if( properties != null )
    {
        foreach (DictionaryEntry entry in properties)
        {
            switch ((String)entry.Key)
            {
                case "name": 
                    m_ChannelName = ( String ) entry.Value; 
                    break; 
                case "priority":
                    m_ChannelPriority = ( int ) entry.Value;
                    break;
            }
        }
    }

    m_ClientProvider = sinkProvider;

     
}

This constructor extracts the information from the IDictionary object. The only two customizable parts of the FileClientChannel are the ChannelName and ChannelPriority. Because ChannelName and ChannelPriority are read-only properties, this constructor is the only place where you can change the member fields.

It’s the responsibility of the FileClientChannel class to create the channel provider chain. Later, this provider chain will create the channel sink chain. FileClientChannel must also provide a default formatter for the instances in which the user of the class doesn’t specify one. The following function will handle this for the FileClientChannel:

private void SetupClientChannel()
{
    if( m_ClientProvider == null )
    {
        m_ClientProvider = new SoapClientFormatterSinkProvider();
        m_ClientProvider.Next = new FileClientChannelSinkProvider();
    }
    else
    {
        AddClientProviderToChain( 
                        m_ClientProvider, 
                        new FileClientChannelSinkProvider( ));
    }
}

SetupClientChannel first checks to see whether a provider already exists by testing m_ClientProvider for null. If m_ClientProvider is null, we must build an IClientChannelSinkProvider chain. Part of building the provider chain is selecting a formatter provider for the channel. FileChannel will use System.Runtime.Remoting.Channels.SoapClientFormatterSinkProvider. Next we add our provider to the end of the chain. If m_ClientProvider isn’t null, we must add our provider to the end of the chain. We do this by calling AddClientProviderToChain. This method is a replica of an internal remoting method of the same name found in the CoreChannel class.

private static void AddClientProviderToChain( 
                          IClientChannelSinkProvider clientChain,
                          IClientChannelSinkProvider clientProvider )
{
    while( clientChain.Next != null )
    {
        clientChain = clientChain.Next;
    }

    clientChain.Next = clientProvider;
}

Because our provider must be last in the chain, AddClientProviderToChain must first move to the end of the chain. This is accomplished by calling the property Next until it returns null. At this point, we insert the new provider into the proper position in the chain.

Our final method to implement is CreateMessageSink. CreateMessageSink returns a reference to an IMessageSink object. At a minimum, this object will contain a formatter sink and our FileClientChannelSink. The remote object proxy then uses the chain to dispatch method calls. CreateMessageSink must be able to handle both client-activated and server-activated objects. When making a method call on a server-activated object, the url parameter will contain the URL that the channel was configured with. However, when making a call on a client-activated object, the remoteChannelData parameter will contain the URL.

public IMessageSink CreateMessageSink( String url, 
                                       object remoteChannelData, 
                                       out String objectURI )
{
    objectURI = null;
    String ChannelURI = null;

    if( url != null )
    {
        ChannelURI = Parse( url, out objectURI );
    }
    else
    {
        if(( remoteChannelData != null ) && 
           ( remoteChannelData is IChannelDataStore ))
        {
            IChannelDataStore DataStore = 
                ( IChannelDataStore )remoteChannelData;
        
            ChannelURI = Parse( DataStore.ChannelUris[0], 
                                out objectURI );
            if( ChannelURI != null )
            {
                url = DataStore.ChannelUris[0];
            }
        }
    }

    if( ChannelURI != null )
    {
        return ( IMessageSink ) m_ClientProvider.CreateSink( this, 
                                            url, remoteChannelData );
    }

    objectURI = "";
    return null;
}

The majority of the code in CreateMessageSink is determining the correct server-activated or client-activated URL to pass to CreateSink. We first check the url parameter that’s passed in to see whether it’s null. If it’s not null, we simply use the Parse function to extract the channel URI and the object URI. Calling Parse is also a sanity check to make sure that the URL is valid. If both the return value and the objectURI value are null, we don’t have a proper URL for this channel. If the url parameter is null, we’ll have to extract the information from the remoteChannelData parameter. The .NET Remoting infrastructure retrieves remoteChannelData from the server side of the channel. This object must be of the type System.Runtime.Remoting.Channels.IChannelDataStore. Before using this object, we must check its validity by using the is keyword. If the object is the correct type, we can cast it to a local variable of type IChannelDataStore. Recall from our HttpChannel discussion that an IChannelDataStore object contains an array of URIs that the server can process for this channel. As with the well-known object case, we’ll check the channel URI that’s returned from Parse to ensure we have a valid URL. Once we’ve reached this point, it’s safe to call the CreateSink method on our FileClientChannelSinkProvider.

Implementing the FileClientChannelSinkProvider Class

The main purpose of FileClientChannelSinkProvider is to create our transport sink, FileClientChannelSink. Because FileClientChannelSinkProvider derives from the interface IClientChannelSinkProvider, we must implement the members CreateSink and Next. Because this is the last provider in the chain, Next will return only null and CreateSink will return only a reference to a new FileClient­ChannelSink.

internal class 
    FileClientChannelSinkProvider : IClientChannelSinkProvider
{
    public IClientChannelSink CreateSink( IChannelSender channel, 
                                          String url,
                                          Object remoteChannelData )
    {
        return new FileClientChannelSink( url );
    }

    public IClientChannelSinkProvider Next
    {
        get
        {
            return null;
        }
        set
        {
            throw new NotSupportedException();
        }
    }
}

As you can see, CreateSink simply creates a new FileClientChannelSink by passing in the URL.

Implementing the FileClientChannelSink Class

FileClientChannelSink handles the dispatching of method calls. It has the following responsibilities:

  • Handling synchronous method calls

  • Handling asynchronous method calls

  • Packaging and unpackaging the ITransportHeaders and Stream data into a file

  • Handling return messages from the server

FileClientChannelSink derives from the interface IClientChannelSink. Because we’re the last sink in the chain, we’ll add functionality to our implementation of IClientChannelSink.ProcessMessage and IClientChannelSink.Async­ProcessRequest only. ProcessMessage and AsyncProcessRequest are responsible for handling the first two items in the list. This is the basic layout for our new class:

internal class FileClientChannelSink : IClientChannelSink
{ 
    private String m_PathToServer = null;

    public delegate void AsyncDelegate( String fileName, 
                                 IClientChannelSinkStack sinkStack );

    public FileClientChannelSink( String url )
    {
        String ObjectURI;
        m_PathToServer = FileChannelHelper.Parse( url, 
                                                  out ObjectURI );
    }

    public void AsyncProcessRequest( 
        IClientChannelSinkStack sinkStack,
        IMessage msg,
        ITransportHeaders requestHeaders,
        Stream requestStream )
    {
         
    }

    public void AsyncProcessResponse( 
        IClientResponseChannelSinkStack sinkStack,
        object state,
        ITransportHeaders headers,
        Stream stream )
    {
        throw new NotSupportedException();
    }

    public Stream GetRequestStream( IMessage msg, 
                                    ITransportHeaders headers )
    {
        return null;
    }

    public void ProcessMessage( IMessage msg,
                               ITransportHeaders requestHeaders,
                               Stream requestStream,
                               out ITransportHeaders responseHeaders,
                               out Stream responseStream )
    {
         
    }

    public IClientChannelSink NextChannelSink
    {
        get
        {
            return null;
        }
    }

    public IDictionary Properties
    {
        get
        {
            return null;
        }
    }
}

ProcessMessage handles all synchronous methods calls. To do this, ProcessMessage must first bundle up the data necessary for the server to perform the method call. ProcessMessage then sends the request message to the server and waits for the return message. Upon receipt of the server’s return message, Process­Message reconstitutes the response message into the appropriate variables.

public void ProcessMessage( IMessage msg,
                            ITransportHeaders requestHeaders,
                            Stream requestStream,
                            out ITransportHeaders responseHeaders,
                            out Stream responseStream )
{
    String uri = ExtractURI( msg );

    ChannelFileData data = new ChannelFileData( uri, 
                                                requestHeaders, 
                                                requestStream );
    String FileName = ChannelFileTransportWriter.Write( 
                                       data, 
                                       m_PathToServer, 
                                       null );
    FileChannelHelper.WriteSOAPStream( requestStream, 
                                       FileName + "_SOAP" );

    FileName = ChangeFileExtension.ChangeFileNameToClientExtension( 
                                            FileName );
    WaitForFile.Wait( FileName ); 

    ChannelFileData ReturnData = 
                         ChannelFileTransportReader.Read( FileName );

    responseHeaders = ReturnData.header;
    responseStream = ReturnData.stream;
}

ProcessMessage is the first method we have written that’s specific to our transport. To remain transport agnostic as long as possible, we’ll discuss the transport specifics of this method in more detail later.

AsyncProcessRequest handles asynchronous method calls against the remote object. AsyncProcessRequest will handle two types of request. Both requests package up and send the message in a similar manner as ProcessMessage. The difference is their actions after the message is delivered. The first request type is a OneWay asynchronous request. A OneWay method is marked with the OneWayAttribute and signals that we expect no return of data or confirmation of success. To check whether a method call is OneWay, we must inspect the IMessage object. The second request type is an ordinary asynchronous request. For this type of request, we’ll invoke a delegate that waits for the return message.

public void AsyncProcessRequest( IClientChannelSinkStack sinkStack,
                                 IMessage msg,
                                 ITransportHeaders requestHeaders,
                                 Stream requestStream )
{
    String uri = ExtractURI( msg );

    ChannelFileData data = new ChannelFileData( uri, 
                                                requestHeaders, 
                                                requestStream );
    String FileName = ChannelFileTransportWriter.Write( data, 
                                              m_PathToServer, null );
    FileChannelHelper.WriteSOAPStream( requestStream, 
                                       FileName + "_SOAP" );

    if( !IsOneWayMethod( (IMethodCallMessage)msg ))
    {
        FileName = 
            ChangeFileExtension.ChangeFileNameToClientExtension( 
                                                        FileName );
        AsyncDelegate Del = new AsyncDelegate( this.AsyncHandler );
        Del.BeginInvoke( FileName, sinkStack, null, null );
    }
}

Our implementation of AsyncProcessRequest packages up the message and sends it to the server. We then use the private method FileClientChannel­Sink.IsOneWayMethod to check whether we need to wait for a return message:

private bool IsOneWayMethod( IMethodCallMessage methodCallMessage )
{
    MethodBase methodBase = methodCallMessage.MethodBase;
    return RemotingServices.IsOneWay(methodBase);
}

The static method System.Runtime.RemotingServices.IsOneWay returns a Boolean value indicating whether the method described by the supplied MethodBase parameter is marked with the OneWayAttribute.

When the method isn’t a OneWay method, we invoke an AsyncDelegate. At construction, we pass AsyncDelegate the method FileClientChannelSink.Async­Handler. The method alone isn’t enough to complete the asynchronous call. We must also pass a sink stack object to BeginInvoke. The sink stack is used to chain together all the sinks that want to work on the returning message.

The job of AsyncHandler is to wait for the response message from the server. AsyncHandler will reconstitute the data into an ITransportHeader and Stream object before passing it up the sink chain:

private void AsyncHandler( String FileName, 
                           IClientChannelSinkStack sinkStack )
{
    try
    {
        if( WaitForFile.Wait( FileName ))
        {
            ChannelFileData ReturnData = 
                         ChannelFileTransportReader.Read( FileName );

            sinkStack.AsyncProcessResponse( ReturnData.header, 
                                            ReturnData.stream );
        }
    }
    catch (Exception e)
    {
        if (sinkStack != null)
        {
            sinkStack.DispatchException(e);
        }
    }

}

After the invocation of the delegate, the thread of execution will continue. This allows the application to continue without waiting for the server response.

In this next step, we’ll construct the server-side classes for FileChannel. We’ll create two classes, FileServerChannel and FileServerChannelSink. These two classes will correspond with two server-side classes, HttpServerChannel and HttpServerTransportSink, which we discussed in the “How Channels Are Constructed” section of the chapter.

Implementing the FileServerChannel Class

FileServerChannel is the main class for our server-side processing. It has the following responsibilities:

  • Listening for incoming messages

  • Creating a formatter sink

  • Creating a FileServerChannelSink

  • Building the sink chain

As we discuss the construction of FileServerChannel, we’ll take extra care in addressing these items.

FileServerChannel will be receiving messages; therefore, it must implement IChannelReceiver. Because IChannelReceiver implements IChannel, we must add three new members to FileServerChannel: ChannelName, ChannelPriority, and Parse. To support IChannelReceiver functionality, we must add the members ChannelData, GetUrlsForUri, StartListening, and StopListening. Let’s start by examining the public interface of FileServerChannel:

public class FileServerChannel : IChannelReceiver
{
    public FileServerChannel( String serverPath ){ ... }

    public FileServerChannel( IDictionary properties, 
                              IServerChannelSinkProvider provider )
        { ... }
    
    // IChannel members
    public String Parse( String url, out String objectURI ){ ... }

    public String ChannelName{ ... }

    public int ChannelPriority{ ... }

    // IChannelReceiver members
    public void StartListening( Object data ){ ... }

    public void StopListening( Object data ){ ... }

    public String[] GetUrlsForUri( String objectURI ){ ... }

    public Object ChannelData{ ... }
}

As with FileClientChannel, FileServerChannel has two constructors. The first is a simple constructor that sets the private member variable m_ServerPath to the value passed into the constructor and calls the private method Init. The m_ServerPath variable designates which directory the server will watch for files.

The second constructor will allow for more granularity in the FileServerChannel settings. Like the first constructor, it must also call Init.

public FileServerChannel( IDictionary properties, 
                          IServerChannelSinkProvider provider )
{
    m_SinkProvider = provider;

    if( properties != null )
    {
        foreach (DictionaryEntry entry in properties)
        {
            switch ((String)entry.Key)
            {
                case "name": 
                    m_ChannelName = ( String ) entry.Value;
                    break; 
                case "priority":
                    m_ChannelPriority = ( int ) entry.Value;
                    break;
                case "serverpath":
                    m_ServerPath = ( String ) entry.Value;
                    break;
            }
        }
    }

    // Since the FileChannel constructor that calls this constructor
    // creates both the FileClientChannel and FileServerChannel
    // objects, we must check the m_ServerPath to see if we should
    // listen for incoming messages.
    if( m_ServerPath != null )
    {
        Init();             
    }
} 

Using this constructor is the only way to change the values returned by the get properties ChannelName and ChannelPriority. Both constructors call the Init method, which performs the following actions:

  1. Creates a formatter

  2. Initializes a ChannelDataStore object

  3. Populates the ChannelDataStore object data from the provider chain

  4. Creates the sink chain

  5. Calls the method StartListening

When we built FileClientChannel, we chose SoapClientFormatterSinkProvider for our formatter provider; therefore, the server must use the corresponding provider—SoapServerFormatterSinkProvider.

private void Init()
{
    // If a formatter provider was not specified, we must create one.
    if( m_SinkProvider == null )
    {
        // FileChannel uses the SOAP formatter if no provider
        // is specified.
        m_SinkProvider = new SoapServerFormatterSinkProvider();
    }

    // Initialize the ChannelDataStore object with our channel URI.
    m_DataStore = new ChannelDataStore( null );
    m_DataStore.ChannelUris = new String[1];
    m_DataStore.ChannelUris[0] = "file://" + m_ServerPath;
    
    PopulateChannelData( m_DataStore, m_SinkProvider );

    IServerChannelSink sink = 
        ChannelServices.CreateServerChannelSinkChain( m_SinkProvider,
                                                      this );

    // Add our transport sink to the chain.
    m_Sink = new FileServerChannelSink( sink, m_ServerPath );
    
    StartListening( null );         
}

In the previous snippet, m_DataStore is populated with our channel URI. We use the private method PopulateChannelData to iterate the provider chain and collect data:

private void PopulateChannelData( ChannelDataStore channelData,
                                 IServerChannelSinkProvider provider)
{
    while (provider != null)
    {
        provider.GetChannelData(channelData);
    
        provider = provider.Next;
    }
}

The GetChannelData method extracts information from the provider’s IChannelDataStore member. Because GetChannelData is a member of the IServerChannelSinkProvider interfaces, this member is present in all channel sink providers.

The next interesting part of Init is the call to ChannelServices.CreateServerChannelSinkChain. This static method builds the server-side sink chain. After we have the sink chain, we must add our sink, FileServerChannelSink, to the chain. The final responsibility for Init is to call the method StartListening.

StartListening is the first of the four members of the IChannelReceiver interface that we’ll create. StartListening must create and start a thread that will watch a directory for incoming messages. After creating the thread, the function must not block on the main thread of execution but instead return from StartListening. This allows the server to continue to work while waiting to receive messages.

public void StartListening( Object data )
{
    ThreadStart ListeningThreadStart = new ThreadStart(
                              m_Sink.ListenAndProcessMessage );
    m_ListeningThread = new Thread( ListeningThreadStart );
    m_ListeningThread.IsBackground = true;
    m_ListeningThread.Start();
}

The ThreadStart delegate must be assigned a method that will be executed when our thread calls start. The method we’ll be executing, ListenAndProcessMessage, is implemented as a public method on our sink. Once the thread is running, the server will accept request messages. The StopListening method simply ends the listening thread by calling m_ListeningThread.Abort.

public void StopListening( Object data )
{
    if( m_ListeningThread != null )
    {
        m_ListeningThread.Abort();
        m_ListeningThread = null;
    }
}

The final two IChannelReceiver members we need to implement are ChannelData and GetUrlsForUri. ChannelData is simply a read-only property that returns our IChannelDataStore member. GetUrlsForUri will take an object URI and return an array of URLs. For example, if the object URI Demo.uri is passed into FileServerChannel.GetUrlsForUri, the object URI should return file://<m_ServerPath>/Demo.uri.

public String[] GetUrlsForUri( String objectURI )
{
    String[] URL = new String[1];

    if (!objectURI.StartsWith("/"))
    {
        objectURI = "/" + objectURI;
    }

    URL[0] = "file://" + m_ServerPath + objectURI;

    return URL;
}

The implementation for the IChannel members is straightforward. ChannelName and ChannelPriority are read-only properties that return their respective member variables, m_ChannelName and m_ChannelPriority. Parse calls the shared implementation of Parse in the FileChannelHelper class.

Implementing the FileServerChannelSink Class

The job of FileServerChannelSink is to dispatch request messages from the client to the .NET Remoting infrastructure. FileServerChannelSink will implement the interface IServerChannelSink, so we must implement the members NextChannelSink, AsyncProcessResponse, IServerChannelSink, and ProcessMessage. Because we don’t need to do any processing to the request message, we won’t add any functionality to ProcessMessage. IServerChannelSink throws a NotSupported­Exception because we don’t need to build a stream. The constructor for FileServerChannelSink must take a reference to the next sink in the chain. The read-only property NextChannelSink allows access to the reference. This is the basic layout of FileServerChannelSink:

internal class FileServerChannelSink : IServerChannelSink
{
    private IServerChannelSink m_NextSink = null;
    private string m_DirectoryToWatch;

    public FileServerChannelSink( IServerChannelSink nextSink,
                                  String directoryToWatch )
    {
        m_NextSink = nextSink;
        m_DirectoryToWatch = directoryToWatch;
    }

    public ServerProcessing ProcessMessage( 
                               IServerChannelSinkStack sinkStack,
                               IMessage requestMsg,
                               ITransportHeaders requestHeaders,
                               Stream requestStream,
                               out IMessage responseMsg,
                               out ITransportHeaders responseHeaders,
                               out Stream responseStream )
    {
        throw new NotSupportedException();
    }

    public void AsyncProcessResponse( 
                           IServerResponseChannelSinkStack sinkStack,
                           Object state,
                           IMessage msg,
                           ITransportHeaders headers,
                           Stream stream )
    {
    }

    public Stream GetResponseStream( 
                           IServerResponseChannelSinkStack sinkStack,
                           Object state,
                           IMessage msg,
                           ITransportHeaders headers )
    {
        throw new NotSupportedException();
        return null;
    }

    public IServerChannelSink NextChannelSink
    {
        get
        {
            return m_NextSink;
        }
    }

    public IDictionary Properties
    {
        get
        {
            return null;
        }
    }

    internal void ListenAndProcessMessage()
    {
         
    }
}

ListenAndProcessMessage is where all action takes place in FileServerChannelSink. ListenAndProcessMessage has the following responsibilities:

  • Wait for messages

  • Extract the ITransportHeader and Stream data from the message

  • Pass the request to the sink chain

  • Respond to the client

    internal void ListenAndProcessMessage()
    {
        while( true )
        {
            // Wait for client messages.
            String FileName = WaitForFile.InfiniteWait();
    
            // Server received a message; extract the data from
            // the message.
            ChannelFileData Data = ChannelFileTransportReader.Read( 
                                                              FileName );
    
            ITransportHeaders MessageHeader = Data.header;
            MessageHeader[ CommonTransportKeys.RequestUri ] = Data.URI;
            Stream MessageStream = Data.stream;
    
            // Add ourselves to the sink stack.
            ServerChannelSinkStack Stack = new ServerChannelSinkStack();
            Stack.Push(this, null);
                
            IMessage ResponseMsg;
            ITransportHeaders ResponseHeaders;
            Stream ResponseStream;
    
            // Start the request in the sink chain.
            ServerProcessing Operation = m_NextSink.ProcessMessage( 
                                                    Stack,
                                                    null,
                                                    MessageHeader,
                                                    MessageStream,
                                                    out ResponseMsg,
                                                    out ResponseHeaders,
                                                    out ResponseStream );
    
            // Respond to the client.
            switch( Operation )
            {
                case ServerProcessing.Complete:
                    Stack.Pop( this );
                    ChannelFileData data = new ChannelFileData( null, 
                                                     ResponseHeaders,
                                                     ResponseStream );
    
                    String ClientFileName = ChangeFileExtension.
                             ChangeFileNameToClientExtension( FileName );
                    ChannelFileTransportWriter.Write( data,
                                                      null,
                                                      ClientFileName );
                    FileChannelHelper.WriteSOAPMessageToFile( 
                                              ResponseStream,
                                              ClientFileName + "_SOAP" );
                    break;
                case ServerProcessing.Async:
                    Stack.StoreAndDispatch(m_NextSink, null);
                    break;
                case ServerProcessing.OneWay:
                    break;
    
            }
        }
    }

We’ll discuss the transport toward the end of this section, so for now we’ll gloss over those details. The first thing ListenAndProcessMessage does is infinitely wait on a message. Upon receipt of a request message, we extract the data. Before passing the data to the sink chain, we create a sink stack. To create a sink stack, we use the class System.Runtime.Remoting.Channel.ServerChainSinkStack. Once we have a new ServerChainSinkStack, we call its Push method. The first parameter takes an IServerChannelSink object, and the second parameter takes an object. The object parameter allows you to associate some state with your sink. This state comes into play only for channels that will be processing in AsyncProcessResponse, ProcessMessage, and IServerChannelSink, which our channel doesn’t. ProcessMessage starts the processing of the request by the client. ProcessMessage returns a ServerProcessing object. With this object, we can determine the next action we must take. In the case of ServerProcessing.Complete, we remove our sink from the sink stack by using the ServerChainSinkStack.Pop method. Pop not only removes our sink, it removes any sink added after it.

So far, we have five classes that implement the majority of the custom channel functionality. The next class we must implement will tie together the server-side and the client-side classes.

Implementing the FileChannel Class

FileChannel is very simple. Its sole purpose is to provide a unified interface for both client and server, so it must implement both IChannelSender and IChannelReceiver. FileChannel has three constructors:

public FileChannel();
public FileChannel( String serverPath );
public FileChannel( IDictionary properties, 
                    IClientChannelSinkProvider clientProviderChain,
                    IServerChannelSinkProvider serverProviderChain );

The first constructor creates a FileClientChannel object and assigns it to the private member m_ClientChannel. When using this constructor, FileChannel can send request messages only. The second constructor initializes the private member m_ServerChannel with a newly created FileServerChannel. This constructor requires a directory that we’ll pass along to the FileServerChannel. The third constructor creates both a FileServerChannel and a FileClientChannel.

public FileChannel( IDictionary properties, 
                    IClientChannelSinkProvider clientProviderChain,
                    IServerChannelSinkProvider serverProviderChain )
{
    m_ClientChannel = new FileClientChannel( properties, 
                                             clientProviderChain );
    m_ServerChannel = new FileServerChannel( properties, 
                                             serverProviderChain );
}

When configuring FileChannel via a configuration file, the .NET Remoting infrastructure will use this constructor. The remaining public members for FileChannel simply call their counterparts that have been implemented in either FileServerChannel or FileClientChannel.

public class FileChannel : IChannelSender, IChannelReceiver
{
    private FileClientChannel m_ClientChannel = null;
    private FileServerChannel m_ServerChannel = null;

    // Constructors have been removed from this snippet.

    public String Parse( String url, out String objectURI )
    {
        return FileChannelHelper.Parse( url, out objectURI );
    }

    public String ChannelName
    {
        get
        {
            if( m_ServerChannel != null )
            {
                return m_ServerChannel.ChannelName;
            }
            else
            {
                return m_ClientChannel.ChannelName;
            }
        }
    }

    public int ChannelPriority
    {
        get
        {
            if( m_ServerChannel != null )
            {
                return m_ServerChannel.ChannelPriority;
            }
            else
            {
                return m_ClientChannel.ChannelPriority;
            }
        }
    }

    public Object ChannelData
    {
        get
        {
            if( m_ServerChannel != null )
            {
                return m_ServerChannel.ChannelData;
            }

            return null;
        }
    }

    public String[] GetUrlsForUri( String objectURI )
    {
        return m_ServerChannel.GetUrlsForUri( objectURI );
    }

    public void StartListening( Object data )
    {
        m_ServerChannel.StartListening( data );
    }

    public void StopListening( Object data )
    {
        m_ServerChannel.StopListening( data );
    }

    public IMessageSink CreateMessageSink( String url, 
                                           object remoteChannelData, 
                                           out String objectURI )
    {
        return m_ClientChannel.CreateMessageSink( url, 
                                                  remoteChannelData, 
                                                  out objectURI );
    }
}

Implementing the FileChannelHelper Class

The next step involves creating a helper class that will contain methods that share functionality between FileClientChannel and FileServerChannel. Because all these methods will be static, we made the constructor private. In the previous code snippets, we used the call to FileChannelHelper.Parse several times. Now we need to create the Parse method. As we’ve discussed, Parse is a member of the IChannel interface; therefore, the .NET Remoting infrastructure defines the method signature. Parse takes a URL as a parameter and returns a channel URI. In addition, Parse returns the object URI through an out parameter.

public static String Parse( String url, out String objectURI )
{
    objectURI = null;

    if( !url.StartsWith( "file://" ) )
    {
        return null;
    }

    int BeginChannelURI = url.IndexOf( "://", 0, url.Length ) + 3;
    int EndOfChannelURI = url.LastIndexOf( "/" );

    String ChannelURI;
    if( BeginChannelURI < EndOfChannelURI )
    {
        ChannelURI = url.Substring( BeginChannelURI, 
                                 EndOfChannelURI - BeginChannelURI );
        objectURI = url.Substring( EndOfChannelURI + 1 );
    }
    else
    {
        ChannelURI = url.Substring( BeginChannelURI );
    }

    return ChannelURI;
}

The key item to note in the snippet is the check for file:// in the URI. This action allows Parse to identify whether FileChannel should be processing this URL. For example, if the URL starts with http://, we’d return null in both parameters.

The next method in FileChannelHelper is WriteSOAPMessageToFile. WriteSOAPMessageToFile writes a SOAP message stream to a file, thus providing a tool for diagnostic and educational purposes:

public static void WriteSOAPMessageToFile( Stream stream, 
                                           String FileName )
{
    StreamWriter Writer = new StreamWriter( FileName );
    StreamReader sr = new StreamReader(stream);
    String line;
    while ((line = sr.ReadLine()) != null)
    {
        Writer.WriteLine(line);
    }
    Writer.Flush();
    Writer.Close();

    stream.Position = 0;
}

Creating the FileChannel Transport Class

Up until this point, we’ve avoided transport-specific discussions because we could show a clear separation between the mechanics of creating a channel and the transport. With the exception of configuration information, we see transport-specific code only in the following methods:

  • FileClientChannelSink.ProcessMessage

  • FileClientChannelSink.AsyncProcessRequest

  • FileClientChannelSink.AsyncHandler

  • FileServerChannelSink.ProcessMessage

Our transport will require a two-step process for both reading and writing messages. The first step is to load the class ChannelFileData with the data we’ll need to send to the server. Once the data is contained in ChannelFileData, we’ll use a FileStream to serialize it to the specified path. When reading data, we perform the steps in reverse.

The data contained in ChannelFileData consists of the request URI, ITransportHeaders, and the Stream:

[Serializable]
public class ChannelFileData
{
    private String m_URI = null;
    private ITransportHeaders m_Header = null;
    private byte[] m_StreamBytes = null;

    public ChannelFileData( String URI, 
                            ITransportHeaders headers, 
                            Stream stream )
    {
        String objectURI;
        String ChannelURI = FileChannelHelper.Parse( URL, 
                                                     out objectURI );
        if( ChannelURI == null )
        {
            objectURI = URL;
        }
        m_URI = objectURI;
        m_Header = headers;
        m_StreamBytes = new Byte[ (int)stream.Length ];
        stream.Read( m_StreamBytes, 0, (int) stream.Length );
        stream.Position = 0;
    }

    public String URI
    {
        get
        {
            return m_URI;
        }
    }

    public ITransportHeaders header
    {
        get
        {
            return m_Header;
        }
    }

    public Stream stream
    {
        get
        {
            return new MemoryStream( m_StreamBytes, false );
        }
    }
}

First, notice that ChannelFileData has the Serializable attribute. This is integral for the next step. The only way to set data in ChannelFileData is through the constructor. To retrieve the data from the ChannelFileData object, we provide read-only properties.

Now that we have our data class to serialize, we need to create a class that writes the file to disk. This class, ChannelFileTransportWriter, will have a single method, named Write. For parameters, Write will take a ChannelFileData object, a path to write the file, and a name for the file.

public class ChannelFileTransportWriter
{
    private ChannelFileTransportWriter()
    {
    }

    public static String Write( ChannelFileData data, 
                                String ServerPath,
                                String FileName )
    {
        // If FileName is null, generate a filename with Guid.NewGuid
        if( FileName == null )
        {
            FileName = Path.Combine( ServerPath, 
                                     Guid.NewGuid().ToString() + 
                                     ".chn.server" );
        }

        // Append _Temp to the file name so the file is not accessed
        // by the server or client before we are finished writing the
        // file. After the data is written, we will rename the file.
        String TempFileName = FileName + "_Temp";
        IFormatter DataFormatter = new SoapFormatter();
        Stream DataStream = new FileStream( TempFileName, 
                                            FileMode.Create, 
                                            FileAccess.Write, 
                                            FileShare.None );
        DataFormatter.Serialize( DataStream, data);
        DataStream.Close();

        File.Move( TempFileName, FileName );
        return FileName;
    }
}

ChannelFileTransportWriter has a few key items of note. First, ChannelFile­TransportWriter maintains no state, so we don’t need instances of this class. This allows us to make the constructor private and the Write method static. Notice that we use Guid.NewGuid to generate unique names. This allows us to avoid naming conflicts when we use multiple channels to connect to the server.

Now that we can write request and response message files, we must be able to read them. To do this, we’ll create a class named ChannelFileTransportReader that defines a single method named Read that will populate a ChannelFileData object:

public class ChannelFileTransportReader
{
    private ChannelFileTransportReader()
    {
    }

    public static ChannelFileData Read( String FileName )
    {
        IFormatter DataFormatter = new SoapFormatter();
        Stream DataStream = new FileStream( FileName, 
                                            FileMode.Open, 
                                            FileAccess.Read, 
                                            FileShare.Read);
        ChannelFileData data = 
            (ChannelFileData) DataFormatter.Deserialize(
                                                    DataStream );
        DataStream.Close();
        File.Move( FileName, FileName + "_processed" );

        return data;
    }
}

Notice in the Read method that, after we’re finished with a message, we append the string _processed to the end of the filename. This allows us to see a history of the messages that were sent between the server and client.

The final transport class we need to create is WaitForFile. WaitForFile will have two methods, Wait and InfiniteWait. Wait’s responsibility is to wait for some period for a file to appear.

public static bool Wait( String filename )
{
    int RetryCount = 120;
    
    while( RetryCount > 0 )
    {
        Thread.Sleep( 500 );

        if( File.Exists( filename ))
        {
            return true;
        }

        RetryCount--;
    }           

    return false;
}

Wait checks for the file every half second for 1 minute. A more advanced version of FileChannel would allow you to set the retry count and the sleep time in the configuration file. InfiniteWait will wake up every half a second to see whether a file with an extension of .server is in the specified directory.

public static String InfiniteWait( String DirectoryToWatch )
{               
    // Loop forever or until a file is found.
    while( true )
    {
        String[] File = Directory.GetFiles( DirectoryToWatch, 
                                            "*.server" );

        if( File.Length > 0 )
        {
            return File[0];
        }

        Thread.Sleep( 500 );
    }           
}