|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionIsn’t .NET development easy? I, for one, believe that our life as developers is simpler with the .NET framework comparing to prior technologies such as COM / DCOM under C++. .NET Remoting is, without a doubt, an excellent example for this simplicity. After reading a comprehensive article or two chapters of your favorite remoting book, you can immediately begin creating powerful distributed applications. .NET Remoting also offers outstanding flexibility and various customization options. However, this is where simplicity ends. In my opinion, .NET Remoting is very easy to use but not all that easy to customize. To do so, you must be familiar with the inner plumbing of the .NET Remoting infrastructure and are required to write numerous lines of code you couldn’t care less about. Don’t get me wrong. I do believe that a more aware developer, one that understands the underlying development infrastructure, is essentially a better developer. I just don’t think that it means understanding and implementing every little detail as .NET Remoting customizations sometimes require. In this article I would like to present a small library, which simplifies one the most important aspects of the .NET Remoting customization: Custom Sinks. Don’t be intimidated by the length of this article. Before you read half of it, you’ll be on your way with everything you need to create your own custom sinks within minutes. The rest is extra, more advanced features. If you already know everything about custom sinks and can’t wait to start implementing, feel free to jump to the Basic Custom Sinks section. In any case, please make sure to read the disclaimer at the very bottom of this article. What are Sinks?When you work with a remote object, you do not hold a reference to that object, but a reference to a proxy. The proxy is an object that looks and feels exactly like the remote object and can convert your stack based method calls into messages, and send them to the remote object. In order to send a message to the remote object, the proxy uses a chain of sinks. It calls the first sink in the chain and provides it with the message. The sink optionally modifies the message, and passes it to the next sink, and so on. One of the sinks along the way is a formatter sink. The task of this special sink is to serialize the message into a stream. Sinks after the formatter sink operate on the stream, because at this point the message is no longer relevant (and is provided to the sinks as information only). The last sink in the stream is the transport sink, which is in charge of sending the data to the server and waiting for a response. When response arrives, the transport sink returns it to the previous sink and the response starts finding its way back to the proxy. Along the way, the response goes through the formatter sink, which deserializes the response back into a response message. What happens on the server side? You guessed right. The server also holds a chain of sinks. This time the chain leads to the target object. The first sink is the transport sink. Along the way lies the formatter sink and finally there is the stack builder which does exactly the opposite of the client-side proxy. It converts the message into a stack based method call to the target object. When the target object’s method returns, information (return value, ref parameters, etc) is packed into a message, which is returned back through the same sink chain starting with the stack builder and ending with the transport sink.
In reality there are more sinks than the ones in the above figure, but I wanted to keep things simple and chose to show only the ones relevant to the current discussion. Otherwise I would miss the entire point of this article, wouldn’t I? Custom SinksAs shown in the above figure, it is possible to add custom sinks to the chain, both on the client side and server side. So when do we decide to develop our own custom sink? We usually do it when we want to inspect or modify the data sent from the proxy to the remote object and / or the data returned from the remote object back to the proxy. Let’s say that you want to encrypt the information sent over the wire between the client and the server. To do so, you can create a client-side custom sink that encrypts outgoing request data and decrypts incoming response data. You should also create a server-side custom sink that decrypts request data arriving from the client and encrypts response data sent back to the client. Custom sinks can be placed either before or after the formatter sink, depending on whether they are designed to manipulate the message or the serialized stream. The encryption custom sink would want to work on the stream (it doesn’t care about the logical meaning of the message; it just needs to scramble it). Therefore the client-side custom sink should be situated after the formatter sink (after the message is serialized to a stream) whilst the server-side custom sink should be situated before the formatter sink (before the stream is desterilized back to a message). As another example, let’s say you want the client to send username and password information and the server, which should examine them before allowing access to the target object. You could add the username and password as parameters to every method of the target object. This would effectively add the required information to the message, but would be rather cumbersome. As an alternative, you can create a client-side custom sink that adds the username and password to every outgoing message and a server-side custom sink that retrieves this information and throws an exception (which will be propagated back to the client) if the username and / or password are invalid. Since these custom sinks work on the message, rather than on the serialized stream, the client-side custom sink should be placed before the formatter sink (before the message is serialized to a stream) and the server-side custom sink should be placed after the formatter sink (after the stream is desterilized back to a message). Cool! How do I do it?Here is the catch. In order to implement your own custom sink, you will work hard! You must define a class that implements at least one of the These tasks are surely doable, but are quite tedious, time-consuming and error-prone. When I first realized all I had to do here, I asked myself – couldn’t they provide a base class that takes care of all of these details? Can’t I handle only my own business logic by implementing only the relevant parts of the custom sink? Well, I didn’t find such a class in the class library, so I decided to write one on my own. Admittedly, this class does not cover every custom sink scenario. By simplifying things, you sometimes loose some flexibility. However, I believe that the class is valid for most real-world custom sink scenarios. The class FeaturesThe Main features:
These features will be further described and demonstrated in the following sections. Basic Custom SinksIn order to demonstrate the use of the Before implementing the sinks, let’s create a helper class public Stream Encrypt(Stream source)
{
byte tempByteData;
int tempIntData;
MemoryStream encrypted = new MemoryStream();
while ((tempIntData = source.ReadByte()) != -1)
{
tempByteData = (byte)tempIntData;
tempByteData += this.delta;
encrypted.WriteByte(tempByteData);
}
encrypted.Position = 0;
return encrypted;
}
The The protected virtual void ProcessRequest(
IMessage message,
ITransportHeaders headers,
ref Stream stream,
ref object state)
protected virtual void ProcessResponse(
IMessage message,
ITransportHeaders headers,
ref Stream stream,
object state)
As you can see, the parameter list of both methods is almost identical.
Examine the implementation of these methods for the protected override void ProcessRequest(
IMessage message,
ITransportHeaders headers,
ref Stream stream,
ref object state)
{
stream = this.encryptionHelper.Encrypt(stream);
headers["LamelyEncrypted"] = "Yes";
}
protected override void ProcessResponse(
IMessage message,
ITransportHeaders headers,
ref Stream stream,
object state)
{
if (headers["LamelyEncrypted"] != null)
{
stream = this.encryptionHelper.Decrypt(stream);
}
}
Both methods use a member field of the type protected override void ProcessRequest(
IMessage message,
ITransportHeaders headers,
ref Stream stream,
ref object state)
{
if (headers["LamelyEncrypted"] != null)
{
stream = this.encryptionHelper.Decrypt(stream);
state = true;
}
}
protected override void ProcessResponse(
IMessage message,
ITransportHeaders headers,
ref Stream stream,
object state)
{
if (state != null)
{
stream = this.encryptionHelper.Encrypt(stream);
headers["LamelyEncrypted"] = "Yes";
}
}
The code for the server sink is designed to be capable of working with any client, regardless of whether they use the That’s it! Our custom sinks are now ready to be used. I will demonstrate how the client and server utilize the sinks using configuration files. Before I do that, I must say a word about providers. The .NET Remoting infrastructure does not directly create custom sinks. Instead it creates a sink provider class, which is able to create the custom sinks upon demand. The filename: Basic_SampleClient.exe.config <configuration>
<system.runtime.remoting>
<application>
<channels>
<channel ref="http">
<clientProviders>
<formatter ref="soap" />
<provider
type="CustomSinks.CustomClientSinkProvider, CustomSinks"
customSinkType="LameEncryption.LameEncryptionClientSink, SampleSinks" />
</clientProviders>
</channel>
</channels>
</application>
</system.runtime.remoting>
</configuration>
filename: Basic_SampleServer.exe.config <configuration>
<system.runtime.remoting>
<application>
<channels>
<channel ref="http" port="7878">
<serverProviders>
<provider
type="CustomSinks.CustomServerSinkProvider, CustomSinks"
customSinkType="LameEncryption.LameEncryptionServerSink, SampleSinks" />
<formatter ref="soap" />
</serverProviders>
</channel>
</channels>
</application>
</system.runtime.remoting>
</configuration>
As shown above, the provider is given the custom sink type using the customSinkType attribute. The type is specified in the format “namespace.class, assembly”. In my sample, the custom sink classes reside in a namespace named NOTE: I used an HTTP channel in my example, but you can easily switch to TCP. You may now run the provided sample client and server application to see the custom sinks in action. Actually you won’t see much, as the encryption will be done unnoticeably (after all, that’s the whole point). However, I added some console outputs, which will prove that the sinks actually work... In this section, I demonstrated the relative ease of creating simple custom sinks using the In the following sections I will further develop the Lame Encryption sinks, to demonstrate additional features of the CustomSinks library. Since I want to leave the simplest sample intact, any further developments will be incorporated into Accessing Configuration DataTo design more general custom sinks, we may sometimes need to access data from a configuration file. For example our Lame Encryption sinks may want to read the delta value (the value added / subtracted to / from every byte in the stream). This can be easily accomplished when deriving a sink from Enhanced_SampleClient.exe.config <clientProviders> <formatter ref="soap" /> <provider type="CustomSinks.CustomClientSinkProvider, CustomSinks" customSinkType="LameEncryption.EnhancedLameEncryptionClientSink, SampleSinks"> <customData delta = "15" /> </provider> </clientProviders> Enhanced_SampleServer.exe.config <serverProviders>
<provider type="CustomSinks.CustomServerSinkProvider, CustomSinks"
customSinkType="LameEncryption.EnhancedLameEncryptionServerSink, SampleSinks">
<customData delta = "15" />
</provider>
<formatter ref="soap" />
</serverProviders>
In order to retrieve this data, the sink should have a constructor that accepts one parameter of the type Let’s review the constructor of the modified custom client sink ( public EnhancedLameEncryptionClientSink(SinkCreationData creationData)
{
byte delta = 1;
if (creationData.ConfigurationData.Properties["delta"] != null)
{
delta = byte.Parse(
creationData.ConfigurationData.Properties["delta"].ToString());
}
this.encryptionHelper = new LameEncryptionHelper(delta);
}
Self Exclusion from Sink ChainOne of the main differences between client and server sinks is the timing of their creation. Server sinks are created once, when the channel is configured. Client sinks are created every time a new proxy is created (every proxy may have a different chain of sinks). Thus, client sinks may be created multiple times. Your custom sink (either client-side or server-side) can prevent its addition to the sink chain in runtime. If for some reason (after inspecting the Notes:
Obtaining Additional Sink Creation ParametersYour custom sinks may obtain additional information upon their creation. This is done by having a constructor that accepts one parameter of type The The Back to our Lame Encryption example, let’s say that we don’t need encryption when the target object resides on “localhost”. Review the following constructor (which supports previously developed functionality as well). The new constructor for public EnhancedLameEncryptionClientSink(ClientSinkCreationData creationData)
: this((SinkCreationData)creationData)
{
Uri uri = new Uri(creationData.Url);
if (uri.IsLoopback)
{
throw new ExcludeMeException();
}
}
Notes:
Static InitializationSince client-side custom sinks may be created numerous times, you may sometimes want your class to initialize as much static information as possible. This way, you refrain from processing the same information for every newly created instance. Normally, when you need static initialization, you add a static constructor to your class. You may certainly adopt this approach for your custom sink. However it has one major drawback. If your static constructor throws an exception, this exception cannot normally be caught and handled. Here is my solution. Define the following method in your custom client-side sink: public static void Init(SinkProviderData data, ref object perProviderState) { } This method is guaranteed to be called before any instance of your custom sink is created. You can now place the call to Important: There is a major distinction between this method and a static constructor. If your custom sink appears more than once in the configuration file, the The The Init method is not demonstrated in the Lame Encryption sample. However the SampleSinks project contains another example, The Credentials Sinks, which uses this feature. The Credentials Sinks are described below. A Deeper Look at ProcessRequest and ProcessResponseThe first three parameters of ProcessRequest Client-side, before the formatter sink:
Client-side, after the formatter sink:
Server-side, before the formatter sink:
Server-side, after the formatter sink:
ProcessReponse Client-side, before the formatter sink:
Client-side, after the formatter sink:
Server-side, before the formatter sink:
Server-side, after the formatter sink:
The Credentials SinksThe Lame Encryption sinks presented throughout this article manipulate the stream of the request and the response. For completeness, I also included the Credential Sinks, which demonstrate message-based processing. The task of the client-side sink is to add the username and password to every outgoing communication. The server-side sink verifies these credentials and throws an exception if they are found to be invalid. The server-side retrieves the credentials from the configuration file. The client-side sink also retrieves the credentials from the configuration file. However, different credentials can be assigned to different servers and even ports. You are invited to review the Final NoteI made every effort to make this article as mistakes-free and the code as bug-free as possible. If however, I missed anything, I would really like to know. Also, suggestion for further improvements of the CustomSinks library and general comments are more than welcome. DisclaimerThis article and the accompanying code are provided as-is. You may use it as you please (I’m becoming a poet...). You may NOT hold me liable for any damage caused to you, your company, your neighbors or anyone else as a result of reading this article or using the code. Whatever you do with this article and the accompanying code is at your own risk. Enjoy. | ||||||||||||||||||||