Build a C# Named-Pipes Based Inter-Process,
Inter-Machine Object Caching Service

by Peter A. Bromberg, Ph.D.

Peter Bromberg
"If fifty million people say a foolish thing, it is still a foolish thing."   - Anatole France

This is a project I've actually had "lying around" for some time (at least in conceptual form) but time constraints prevented me from fully exploring it until now. Actually what sparked my interest was reading about the IPC Channel that's new in .NET 2.0 for Remoting, and how much faster it is than the HTTP or TCP Channels -- it's based on Named Pipes.

What is "Named Pipes"? It's an interprocess control (IPC) protocol for exchanging information between two applications, possibly running on different computers in a network. Named Pipes are supported by a number of network operating systems, including Netware and LAN Manager, and of course, Windows. If it sounds vaguely familiar, remind yourself that SQL Server uses Named Pipes as one of it's primary transport mechanisms besides TCP. Why? Because it's a low-level operating system protocol, it's extremely fast, and it's reliable.



Unfortunately, there has never been an Framework Class Library .NET implementation from Microsoft, even though there are .NET Framework unmanaged API wrappers for a lot of other things. Why, I have no idea; even the new .NET 2.0 implementation of Pipes seems to be irretrievably bound to the Remoting infrastructure (although to be fair, I haven't yet had the chance to really look into it).

At any rate, these are well - defined API's and there are several nice sample projects on how to "wrap" them with the PInvoke layer in C#. If you are "C#-Challenged" and determined to do this in VB.NET, I can't help you, but you are certainly welcome to try. The best implementation (and the one I've used here, since it required the least amount of changing and enhancing) is the one by Ivan Latunov here.

Now the first question a developer would want to ask is this:

What do I need Named Pipes for?

The answer is, "If you want to do IPC (Inter-Process) communication efficiently." IPC is when more than one process, either on the same computer or different computers on the network, need to be able to talk to each other.

There are a number of ways we can do this in the .NET Platform, each of which has its advantages and drawbacks:

1) WebServices: A Central WebService can provide easy access to almost any type of .NET Program (even ASP.NET) - however, everything needs to go over the wire as XML and it depends (usually) on HTTP.

2) Remoting: Remoting offers a way to incorporate Inter-Process Communication and has a lot of flexibility, however it also involves some complex MarshalByRefObject semantics and a lot of serialization.

3) TCP / UDP Sockets: You can implement a socket server to handle the inter-process communication. This can be reasonably fast, but it typically involves some pretty sophisticated programming. (For an example, see here).

4) Using a Database: This might be equated to the SQL Server ASP.NET Session option. Persistence is a feature, but speed is not.

The implementation I've cobbled together here is based on Named Pipes. It works, and it works well. Between processes on a single machine, you can expect to be able to store or retrieve some 10,000 managed objects in as little as 5 seconds. Between machines on a network, that could expand to as much as 40 seconds or more, depending on the size of the objects being cached and network latency. However, I've run this with a Named Pipe Pool of up to 40 Pipes with no problems. Everything is controlled by simple entries in the configuration files, and it doesn't use much memory compared to other solutions.

Moreover, I've "componentized" the pieces of the solution into separate projects in such a way that it will be very easy for developers who want to "take it up a notch" to customize the solution to the needs of their enterprise.

Finally, I've taken some effort to ensure that the usage model from a user-class standpoint is easy and does not involve a lot of code. Specifically, the semantics of the NPCache (Named Pipes Cache) class are very similar to the Indexer semantics you find in many collection classes:

USAGE:

Asembly References for a client:
ClientHandler
Payload

Required using statements:

using ClientHandler;
using PAB.IPC;

Private Cache Declaration in class that uses the Cache:
private NPCache npCache = NPCache.Instance;

CACHE ACTIONS:

STORE:
npCache[key] = object;
Example:
npCache["testDs"]=myDataset;

RETRIEVE:
Type value=(Type)npCache[key];
Example:
DataSet ds = (DataSet)npCache["testDs"];

DELETE:
npCache[key]=new Payload(key,null,Action.Delete);

How it works:

The operation of the NPCache Service is really not complex at all. When the service is started, it spins up a Hashtable of Named Pipe Connection objects that can wait for an incoming connection and become activated. One could call these "Pipe Listeners" in much the same way that a TCP or UDP Socket server might spin up "socket listeners".

On the client side, we instantiate a Singleton instance of the NPCache class, which is esentially a Cache forwarding proxy:

private static NPCache npCache = NPCache.Instance;

This is synchronized so that all operations, either read or write, are threadsafe.

In the middle, we have a Payload class that is used as a serializable container for whatever object we want to remote over Pipes to the server's Hashtable "cache" repository:

using System;

namespace  PAB.IPC

{

    public enum Action

    {

        Store, // insert to Hashtable

        Retrieve, // Retrieve from Hashtable

        Status, // This is a Status (returned) Payload

        Delete  // Delete from Hashtabe

    }

    /// <summary>

    /// Payload is a serializable transport vehicle class

    /// </summary>

    [Serializable]

    public class Payload

    {

        public string Key;

        public Object Body;

        public Action Action;

        public Payload(string key, object body, Action action)

        {

            this.Key=key;

            this.Body =body;

            this.Action =action;

        }

    }

}

As can be seen, this class "holds" both the object, its Hashtable key, and an Action enum that tells the sending or the receiving end what the purpose of the container is.

To Store an object into the Cache via Pipes, we can use familiar Indexer semantics like so:

npCache[key] = ObjectInstanceToStore;

The NPCache class has two methods for storing and retrieving; here is the store method:

private void SetPayload(string key, Payload value)

        {

            try

            {

                ClientHandler.Client c = new ClientHandler.Client();

                Payload outP = new Payload(key,value.Body , Action.Store);

                Payload recvP = c.Send(pipeName,serverName,outP);

                if(recvP.Body.ToString()!="OK")

                    recvP.Body="ERROR: Key Not Found";

            }

            catch

            {

                throw;

            }

 

        }

The ClientHandler class serializes the Payload, instantiates a new Pipe connection, sends (and then receives back) the Payload:

public Payload Send(string pipeName, string serverName, Payload message)

        {

            IInterProcessConnection clientConnection = null;

            Payload retP=null;

 

            try

            {

                clientConnection = new ClientPipeConnection(pipeName, serverName);

                clientConnection.Connect();

                byte[] bytesToSend = PAB.IPC.Utilities.PayloadToBytes(message);

                clientConnection.WriteBytes(bytesToSend);

                byte[] returnBytes = clientConnection.ReadBytes();

                retP =PAB.IPC.Utilities.BytesToPayload(returnBytes);

                clientConnection.Close();

            }

            catch (Exception ex)

            {               

                retP=new Payload(message.Key,ex.Message,Action.Status);

            }

 

            finally

            {

                clientConnection.Dispose();

            }

 

            return retP;

        }

    }

A return instance of the Payload class, which can be a container for either a result object or just the "status" of the operation, is sent back in exactly the reverse of the Send operation. Note that if an exception is thrown, it's message property is populated into the return Payload body so it can be inspected by the caller.

There's more to it, of course, but that's how it all works, in a nutshell. In your particular operations, it may turn out that you really don't want a Hashtable. Perhaps you have an operation where various clients need to get the "Most recent X" items, similar to the SQL Statment "Select TOP 10 X from Y". In that case, you would simply replace the Hashtable with a Queue class, and your Payload object, instead of having a "Key", might have a "HowMany" field. You would simply read this information on the server side, Dequeue the correct number of objects from the Queue, package them up into an array of "X" -es, and Payload them back over the pipe to the requestor. So, it's a very flexible arrangement.

When you download the Visual Studio 2003 Solution below, you will see that in debug mode, my Windows Service actually runs not as a service, but as a "plain old" executable, which makes debugging incredibly easy. You simply start the Service project, and then start up one of two client test apps, either a Windows Forms Client class that let's you put in a key and a value, or the Speed Test app that lets you do some timings for heavy load testing. Be sure to look at the two config files for the Service and for each Client; they should be self-explanatory. You can use "." (a dot) for the server element for work on the same machine, or a named machine if working between machines. The service would normally always listen on "." (itself). In release build mode, the Windows Service must be installed via the install.bat or uninstall.bat files included, which are also in the MSI Installer project.

There is complete CHM format documentation and a built installer for the Windows Service, as well as copies of Ivan's original articles on his original implementation of the Named Pipes IPC process that I used here. If you have ideas, recommendations, or requests, feel free to post them on the discussion forum at the bottom of this article and I'll try to address these and possibly enhance this article and the download as time goes on. This can be converted "in place" to Visual Studio 2005. If you do this, you will want to change the Hashtable to a Generic List object of type Payload to take advantage of the performance enhancements available with Generics. To implement this one could use a Generic Dictionary, which is the Generic class analogous to the Hashtable. This would require only the changing of a single line of code:

public static Dictionary<string, Payload > htCache= new Dictionary<string, Payload>();

Download the Visual Studio 2003 Solution that accompanies this article


Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.
Article Discussion: