This article has been excerpted from book "The Complete Visual C# Programmer's Guide" from the Authors of C# Corner.
The examples for this section are in the Sample5 folder. Two projects-CustomSinkLib and SimpleObjectForSinkLib-are libraries. The client and server console projects are SinkClientExe and SinkServerExe respectively. Each of the console projects requires references to System.Runtime.Remoting.dll, SimpleObjectForSinkLib.dll, and CustomSinkLib.dll.
Channel sinks are the means by which messages are passed back and forth between the client and server. Earlier in the chapter, call contexts were explored as a device to transparently pass data across the wire. A channel sink can be an alternative way to achieve the same end. But sinks are not limited to that single function. The HTTP and TCP channels, provided by .NET, have two default sinks in the sink chain-a formatter and a transport sink. The formatter converts an IMessage into a stream, while the transporter streams the data across the wire. Each does a discrete unit of work and hands the result to the next sink in the chain.
The .NET Framework does not restrict the number of links in a sink chain. The sink chain can be viewed as a functional linked list, rather than a linked list of data. Depending on the problem domain, one to many sinks are plugged into a channel's chain, with each sink addressing a single task. This may include logging, security functions, encryption, or any other task that is required on the route from the transparent proxy to the remote object. To become a member of a sink chain, certain criteria must be met.
All channels are divided into senders and receivers. Receivers are generally servers, while senders are clients. Channel sinks can be seen in the same light. They are connected to a channel by a sink provider, either a System.Runtime.Remoting.Channels.IClientChannelSinkProvider or an IServerChannelSinkProvider.
Listing 25.28: ClientSinkProvider.cs
public class ClientSinkProvider : IClientChannelSinkProvider
{
private IClientChannelSinkProvider next = null;
public ClientSinkProvider()
{
}
public ClientSinkProvider(IDictionary props, ICollection provider)
{
}
public IClientChannelSink CreateSink(IChannelSender sender, string url, object channelData)
{
IClientChannelSink sink = null;
if (next != null)
{
sink = next.CreateSink(sender, url, channelData);
}
if (sink != null)
{
sink = new ClientSink(sink);
}
return (sink);
}
public IClientChannelSinkProvider Next
{
get
{
return (next);
}
set
{
next = value;
}
}
}
Deriving from the IClientChannelSinkProvider interface, described in Listing 25.28, requires implementing the CreateSink method and the Next property. The server provider code, not shown, in ServerSinkProvider.cs is the same, except for an additional method, GetChannelData(), which has a System.Runtime.Remoting.Channels.IChannelDataStore parameter.
Notice in CreateSink() that if there is a next provider, its CreateSink() is called before the current provider instantiates its sink. In other words, the last provider is the first to create an actual sink and starts the process where the sinks are chained together. It is also evident that as soon as the provider creates its sink, it could be a candidate for disposal, as it has no knowledge of the sink once instantiation is complete.
The order in which providers are attached to a channel determines the order of the sink chain. On the server, the first sink must be the transport sink, and the last must the formatter sink. On the client the order is reversed. If the order is incorrect, results are unpredictable! Sink providers are attached to a channel via a configuration file or programmatically. The order of creation will be clearer with some example code. (See Listing 25.29.)
Listing 25.29: SinkClient.cs
Hashtable channelData = new Hashtable();
Hashtable properties = new Hashtable();
ArrayList providerData = new ArrayList();
ClientSinkProvider clientProvider = null;
ServerSinkProvider serverProvider = null;
SoapClientFormatterSinkProvider clientFormatter = null;
SoapServerFormatterSinkProvider serverFormatter = null;
MyHttpChannel http = null;
clientFormatter = new SoapClientFormatterSinkProvider( properties, providerData);
clientProvider = new ClientSinkProvider( properties, providerData);
clientFormatter.Next = clientProvider;
serverProvider = new ServerSinkProvider( properties, providerData);
serverFormatter = new SoapServerFormatterSinkProvider( properties,providerData);
serverProvider.Next = serverFormatter;
http = new MyHttpChannel( channelData, clientFormatter, serverProvider);
The client creates the SOAP formatter and then the customized provider, which is assigned to the clientFormatter.Next property. The server's order of creation and assignment is reversed. Because MyHttpChannel inherits from HttpChannel, both sender and receiver sinks are required. During construction of the channel, the transport sinks are created and placed in the appropriate position in their respective chains.
Listing 25.30: Shared.config
<?xml version="1.0" ?>
<configuration>
<system.runtime.remoting>
<channels>
<channel id="http"
type="CustomSinkLib.MyHttpChannel, CustomSinkLib">
</channel>
</channels>
<channelSinkProviders>
<clientProviders>
<formatter id="soap"
type="System.Runtime.Remoting.Channels.SoapClientFormatterSinkProvider,System.Runtime.Remoting"/>
<provider id="custom" type="CustomSinkLib.ClientSinkProvider, CustomSinkLib"/>
</clientProviders>
<serverProviders>
<formatter id="soap"
type="System.Runtime.Remoting.Channels.SoapServerFormatterSinkProvider,System.Runtime.Remoting"/>
<provider id="custom" type="CustomSinkLib.ServerSinkProvider, CustomSinkLib"/>
</serverProviders>
</channelSinkProviders>
</system.runtime.remoting>
</configuration>
The server and client configuration files are shown in Listings 25.31 and 25.32, while a common configuration file is shown in Figure 25.30. The server's provider is listed ahead of the formatter. Not shown, but present nonetheless, is the transporter. The Http and Tcp namespaces supply a client transport sink provider and transport sink as private classes in System.Runtime.Remoting.dll. It is worth noting that the Http and Tcp servers don't have a transport sink provider, simply a transport sink, which has the variable name transportSink.
Listing 25.31: SinkServerExe.exe.config
<?xml version="1.0" ?>
<configuration>
<system.runtime.remoting>
<application>
<service>
<wellknown type="SimpleObjectForSinkLib.SimpleObject, SimpleObjectForSinkLib" mode="Singleton" objectUri="Simple"/>
</service>
<channels>
<channel ref="http" port="1234">
<serverProviders>
<provider ref="custom"/>
<formatter ref="soap"/>
</serverProviders>
</channel>
</channels>
</application>
</system.runtime.remoting>
</configuration>
If it were a requirement that the server send as well as receive, it would be a simple matter to place the following code within the <channel> element, as shown in Listing 25.32:
<clientProviders>
<formatter ref="soap"/>
<provider ref="custom"/>
</clientProviders>
Listing 25.32: SinkClientExe.exe.config
<?xml version="1.0" ?>
<configuration>
<system.runtime.remoting>
<application>
<client>
<wellknown type="SimpleObjectForSinkLib.SimpleObject,SimpleObjectForSinkLib" url="http://localhost:1234/Simple"/>
</client>
<channels>
<channel ref="http">
<clientProviders>
<formatter ref="soap"/>
<provider ref="custom"/>
</clientProviders>
</channel>
</channels>
</application>
</system.runtime.remoting>
</configuration>
In the client's configuration file the formatter is first, followed by the provider. Now that sink ordering is clearer, it is a good point to examine when objects are created. The server is considered first (see Listing 25.33).
Listing 25.33: SinkServer.cs
RemotingConfiguration.Configure("shared.config");
RemotingConfiguration.Configure("SinkClientExe.exe.config");
Earlier in the article, we noted that the server's channel listens for requests upon creation, so it should come as no surprise that sink providers and sinks are created at the outset. The call RemotingConfiguration.Configure() with the shared.config file name, which is only a template, triggers nothing. The call using SinkServerExe.exe.config begins the process of instantiation. First, the provider constructors are called, followed by the channel constructor. In the channel's constructor, the provider CreateSink() methods are called, which create their sinks. When the channel constructor returns, the channel is ready to accept requests. The client, on the other hand, is not quite as straightforward. The second call to Configure() in Listing 25.34 creates the providers and the channel. Only with the new SimpleObject() instantiation are the providers' CreateSink() methods called.
Listing 25.34: SinkClient.cs
static int Main(string[] args)
{
RemotingConfiguration.Configure("shared.config");
RemotingConfiguration.Configure("SinkClientExe.exe.config");
SimpleObject simple = null;
simple = new SimpleObject();
string ret = null;
ret = simple.ConCatString("Custom", "sink");
Console.WriteLine(ret);
return (0);
}
Now that the sink chain has been created, what function do the sinks actually perform? Sinks derive from either System.Runtime.Remoting.Channels.IServerChannelSink or IClientChannelSink interfaces, depending on whether they are senders or receivers. Each implements ProcessMessage() and AsyncProcessResponse() methods and the get NextChannelSink property, as well as the System.Runtime.Remoting.Channels.IChannelSinkBase.Properties property. The server also implements GetResponseStream(), while the client implements GetRequestStream() and AsyncProcessRequest(). The first client sink must also implement System.Runtime.Remoting.Messaging.IMessageSink. The interface defines SyncProcessMessage() and AsyncProcessMessage(). One of these methods is called by RealProxy to initiate a remote call to the server.
Listings 25.35 and 25.36 are only a partial source code listing. The ProcessMessage() is the method of primary interest.
Listing 25.35: ServerSink.cs
internal class ServerSink : IServerChannelSink
{
private IServerChannelSink next = null;
public ServerSink(IServerChannelSink next)
: base()
{
this.next = next;
}
public ServerProcessing ProcessMessage(IServerChannelSinkStack stack, IMessage reqMsg, ITransportHeaders reqHdrs, Stream reqStrm, out IMessage msg,
out ITransportHeaders respHdrs,
out Stream
respStrm)
{
Console.WriteLine("In ServerSink.ProcessMessage()");
SinkHelper helper = null;
helper = new SinkHelper();
helper.WriteTransportHeaders(reqHdrs,
"Transport Request Headers");
ServerProcessing process;
stack.Push(this, null);
process = next.ProcessMessage(stack, reqMsg, reqHdrs,
reqStrm, out msg, out respHdrs, out respStrm);
helper.WriteTransportHeaders(respHdrs,
"Transport Response Headers");
respHdrs["ServerSide"] = "In ServerSink.ProcessMessage()";
switch (process)
{
case ServerProcessing.Complete:
{
stack.Pop(this);
break;
}
case ServerProcessing.OneWay:
{
stack.Pop(this);
break;
}
case ServerProcessing.Async:
{
stack.Store(this, null);
break;
}
}
return (process);
}
}
Listing 25.36: ClientSink.cs
internal class ClientSink : IClientChannelSink
{
private IClientChannelSink next = null;
public ClientSink(IClientChannelSink next
: base()
{
this.next = next;
}
public void ProcessMessage(IMessage msg, ITransportHeaders reqHdrs,Stream reqStrm,
out ITransportHeaders respHdrs, out Stream respStrm)
{
Console.WriteLine("In ClientSink.ProcessMessage");
SinkHelper helper = null;
helper = new SinkHelper();
helper.WriteTransportHeaders(reqHdrs,
"Transport Request Headers");
reqHdrs["ClientSide"] = "In ClientSink.ProcessMessage";
next.ProcessMessage(msg, reqHdrs,
reqStrm, out respHdrs, out respStrm);
helper.WriteTransportHeaders(respHdrs,
"Transport Response Headers");
}
}
The client's ProcessMessage() has as one of its parameters an IMessage object. At this stage it is informational only, because the formatter has already converted the message to a stream, reqStrm. In this example, the custom sink adds a key/value pair-reqHdrs["ClientSide"] = "In ClientSink.ProcessMessage" to the transport headers and passes it on to the next sink in the chain. The server responds with two out parameters, which are the response headers and streamed return value.
The server code is much the same, forwarding the message to the next sink. The points to highlight are the stack parameter and the return enumeration from ProcessMessage(). The System.Runtime.Remoting.Channels.ServerProcessing enumeration describes how the message was processed. As with the client, the IMessage is unused. The stack is a means of keeping track of the sink order during asynchronous remoting. Finally, the server adds a key/value to the response headers.
Figure 25.17: SinkClientExe.exe Output
Figure 25.18: SinkServerExe.exe Output
This simple example, whose output is shown in Figures 25.17 and 25.18, explains the basics of constructing a sink. Using essentially the same code, a sink could hang onto the original stream and create its own stream. Ship that stream to the server. Once it receives a response, send the original stream to the server. As should be evident, sinks are a powerful mechanism to extend object remoting.
Conclusion
Hope this article would have helped you in understanding Channel Sinks. See other articles on the website on .NET and C#.
|
The Complete Visual C# Programmer's Guide covers most of the major components that make up C# and the .net environment. The book is geared toward the intermediate programmer, but contains enough material to satisfy the advanced developer. |