Sinks and Sink Chains

Channels send each message along a chain of channel sink objects prior to sending or after receiving a message. This sink chain contains sinks required for basic channel functionality, such as formatter, transport, or stackbuilder sinks, but you can customize the channel sink chain to perform special tasks with a message or a stream.

Each channel sink implements either IClientChannelSink or IServerChannelSink. The first channel sink on the client side must also implement IMessageSink. It typically implements IClientFormatterSink (which inherits from both IMessageSink, IChannelSinkBase, and IClientChannelSink) and is called a formatter sink because it transforms the incoming message into a stream (an IMessage object).

The channel sink chain processes any message that is sent to or from an application domain. At this point, all you have is the message, but you are able to do anything you want with that message, and subsequent processing will use the message that you return to the system after processing. This is a natural place to implement a logging service, any sort of filter, or perhaps encryption or other security measures on the client or the server. The following illustration shows the structure of a basic channel sink chain.

Basic Channel Sink Chain

Note that each channel sink processes the stream and then passes the stream to the next channel sink, which means that objects before or after your sink should know what to do with the stream you have passed to them.

Note   Message sinks must not throw exceptions. One way a message sink can control this is by wrapping method code in try-catch blocks.

Channel sink providers (objects that implement the IClientChannelSinkProvider, IClientFormatterSinkProvider, or IServerChannelSinkProvider interface) are responsible for creating the channel sinks through which remoting messages flow. When a remote type is activated, the channel sink provider is retrieved from the channel; and the CreateSink method is called on the sink provider to retrieve the first channel in the sink from the chain.

Channel sinks are responsible for transporting messages between the client and the server. Channel sinks are also linked together in a chain. When the CreateSink method is called on a sink provider, it should do the following:

  • Create its own channel sink.
  • Call CreateSink on the next sink provider in the chain.
  • Ensure that the next sink and the current one are linked together.
  • Return its sink to the caller.

Channel sinks are responsible for forwarding all calls made on them to the next sink in the chain and should provide a mechanism for storing a reference to the next sink.

Channel sinks have great flexibility in what they send down the sink chain. For example, security sinks that want to negotiate authentication before sending the actual serialized original message can hold onto the complete channel message, replace the content stream with their own content, and send it down the sink chain and on to the remote application domain. On the return journey, the security sink can intercept the reply message, creating a conversation with the corresponding security sinks in the remote application domain. Once an agreement is reached, the originating security sink can send the original content stream on to the remote application domain.

Message Processing in the Channel Sink Chain

Once the .NET remoting system locates a channel that can process the IMethodCallMessage implementation, the channel passes the message to the formatter channel sink by calling IMessageSink.SyncProcessMessage (or IMessageSink.AsyncProcessMessage). The formatter sink creates the transport header array and calls IClientChannelSink.GetRequestStream on the next sink. This call is forwarded down the sink chain, and any sink can create a request stream that will be passed back to the formatter sink. If GetRequestStream returns a null reference (Nothing in Visual Basic), the formatter sink creates its own sink to use for serialization. Once this call returns, the message is serialized and the appropriate message processing method is called on the first channel sink in the sink chain.

Sinks cannot write data into the stream but can read from the stream or pass a new stream along where required. Sinks can also add headers to the header array (if they have not previously called GetRequestStream on the next sink) and add themselves to the sink stack before forwarding the call to the next sink. When the call reaches the transport sink at the end of the chain, the transport sink sends the headers and serialized message over the channel to the server where the entire process is reversed. The transport sink (on the server side) retrieves the headers and serialized message from the server side of the stream and forwards these through the sink chain until the formatter sink is reached. The formatter sink deserializes the message and forwards it to the remoting system where the message is turned back into a method call and is invoked on the server object.

Creating Channel Sink Chains

To create a new channel sink, you must implement and configure the remoting system to recognize an IServerChannelSinkProvider or IClientChannelSinkProvider implementation, which can create your custom IClientChannelSink or IServerChannelSink implementation or retrieve the next sink in the chain. You can use the BaseChannelSinkWithProperties abstract class to help implement your custom channel sinks.

Building a Channel Sink Provider

Applications can provide server or client channel sink providers as parameters when constructing a channel. Channel sink providers should be stored in a chain and it is the responsibility of the user to chain all channel sink providers together before passing the outer one to the channel constructor. The channel sink provider implements a Next property for this purpose. The following code example illustrates how to build a client-side channel sink provider. A complete example is available at Remoting Example: Channel Sink Provider.

private Function CreateDefaultClientProviderChain() As IClientChannelSinkProvider
   Dim chain As New FirstClientFormatterSinkProvider            
   Dim sink As IClientChannelSinkProvider
   sink = chain
   sink.Next = New SecondClientFormatterSinkProvider
   sink = sink.Next
   return chain
End Function 
[C#]private IClientChannelSinkProvider CreateDefaultClientProviderChain(){
   IClientChannelSinkProvider chain = new FirstClientFormatterSinkProvider();            
   IClientChannelSinkProvider sink = chain;
   sink.Next = new SecondClientFormatterSinkProvider();
   sink = sink.Next;
   return chain;
} 

Note   When multiple channel sink providers are provided in a configuration file, the remoting system chains them together in the order in which they are found in the configuration file. The channel sink providers are created when the channel is created during the RemotingConfiguration.Configure call.

Formatter Sinks

Formatter sinks serialize the channel message into the message stream as an object that implements IMessage. Some formatter sink implementations use the system-provided formatter types (BinaryFormatter and SoapFormatter). Other implementations can use their own means to transform the channel message into the stream.

The function of the formatter sink is to generate the necessary headers and serialize the message to the stream. After the formatter sink, the message is forwarded to all sinks in the sink chain through the IMessageSink.ProcessMessage or Imessagesink.AsyncProcessMessage calls. At this stage the message has already been serialized and is provided as information only.

**Note   **Sinks that need to create or modify the message itself must be placed in the sink chain prior to the formatter. This is easily achieved by implementing IClientFormatterSink, thereby fooling the system into believing that it has a reference to the formatter sink. The real formatter sink can then be placed later in the sink chain.

On the return journey, the formatter sink transforms the message stream back into the channel message elements (return message). The first sink on the client side must implement the IClientFormatterSink interface. When CreateSink returns to the channel, the reference returned is cast to an IClientFormatterSink type so the SyncProcessMessage of the IMessage interface can be called. If the cast fails, the system raises an exception.

Custom Channel Sinks

On the client side, custom channel sinks are inserted into the chain of objects between the formatter sink and the last transport sink. Inserting a custom channel sink in the client or server channel enables you to process the IMessage at one of two points:

  • During the process by which a call represented as a message is converted into a stream and sent over the wire.
  • During the process by which a stream is taken off the wire and sent to the StackBuilderSink object (the last message sink before the remote object on the server) or the proxy object (on the client).

Custom sinks can read or write data (depending if the call is outgoing or incoming) to the stream and add additional information to the headers where desired. At this stage, the message has already been serialized by the formatter and cannot be modified. When the message call is forwarded to the transport sink at the end of the chain, the transport sink writes the headers to the stream and forwards the stream to the transport sink on the server using the transport protocol dictated by the channel.

Transport Sinks

The transport sink is the last sink in the chain on the client side, and the first sink in the chain on the server side. Besides transporting the serialized message, the transport sink is also responsible for sending the headers to the server and retrieving the headers and the stream when the call returns from the server. These sinks are built into the channel and cannot be extended.

Replacing the Default Formatter

Because a channel is an abstract networking mechanism, you can configure the .NET remoting system to combine a system-implemented channel with any formatter you choose. You can do this using the channel constructor that takes an IDictionary implementation of channel properties, a server-side formatter, and a client-side formatter. You can also specify the formatter in a configuration file. The following example instructs the .NET remoting configuration system to create an HttpChannel **** but use the BinaryClientFormatterSink on the client side.

<configuration>
   <system.runtime.remoting>
      <application>
         <channels>
            <channel ref="http">
               <clientProviders>
                  <formatter ref="binary"/>
               </clientProviders>
         <channels>
      </application>
   </system.runtime.remoting>
</configuration> 

The following code does the same thing programmatically, assuming a remote interface type IService that implements GetServerString and GetServerTime.

Imports System
Imports System.Collections
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Http

Public Class ClientProcess
  <MTAThread()> _
  Public Shared Sub Main()
      
    ' Note that any name/value pairs of configuration attributes can be 
    ' placed in this dictionary (the configuration system calls this same 
    ' constructor).
    Dim properties As New Hashtable()
    properties("name") = "HttpBinary"
     
    ChannelServices.RegisterChannel(New HttpChannel(properties, New BinaryClientFormatterSinkProvider(), Nothing))
    ' The last parameter above (Nothing) is the server sink provider chain 
    ' to obtain the default behavior (which includes SOAP and 
    ' binary formatters on the server side).
    Dim service As IService = CType(Activator.GetObject(GetType(IService), "https://computer:8080/SAService"), IService)
      
    Console.WriteLine("Server string is: " + service.GetServerString())
    Console.WriteLine("Server time is: " + service.GetServerTime())
  End Sub
   
End Class 
[C#]using System;
using System.Collections;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;

public class ClientProcess{

  public static void Main(string[] Args){
        
    // Note that any name/value pairs of configuration attributes can be 
    // placed in this dictionary (the configuration system calls this 
    // same HttpChannel constructor).
    IDictionary properties = new Hashtable();
    properties["name"] = "HttpBinary";

    // The last parameter below is the server sink provider chain 
    // to obtain the default behavior (which includes SOAP and binary 
    // formatters) on the server side.
    ChannelServices.RegisterChannel(new HttpChannel(null, new BinaryClientFormatterSinkProvider(), null));

    IService service = (IService)Activator.GetObject(typeof(IService),"https://computer:8080/SAService");
        
    Console.WriteLine("Server string is: " + service.GetServerString());
    Console.WriteLine("Server time is: " + service.GetServerTime());      
  }
}

For a complete example of this channel and formatter combination hosted in Internet Information Services (IIS), see Remoting Example: Hosting in Internet Information Services (IIS).

To change this client to use a TcpChannel object with the SoapClientFormatterSink object, you need to change only the namespaces and the RegisterChannel call, as shown in the following code example.

ChannelServices.RegisterChannel(New TcpChannel(properties, NewSoapClientFormatterSinkProvider(), Nothing))
[C#]ChannelServices.RegisterChannel(new TcpChannel(null, new SoapClientFormatterSinkProvider(), null));

See Also

Advanced Remoting | Hosting Remote Objects in Internet Information Services (IIS) | Remoting Example: Hosting in Internet Information Services (IIS)