Inter-Process Communication in .NET Using Named Pipes, Part 1

1. Introduction

Have you ever needed to exchange data between two .NET applications running on the same machine? For example, a Web site talking to a Windows service? The .NET Framework provides several good options for inter-process communication (IPC) like Web services and Remoting, the fastest being Remoting with a TCP channel and binary formatter.

The problem, however, is that Remoting is relatively slow, which most of the time is irrelevant, but if you need to make frequent "chatty" calls from one application to another and if your primary concern is performance, Remoting might become an obstacle. What makes Remoting slow is not so much the communication protocol but the serialization.

Generally speaking, Remoting is great but in the case when the IPC is confined to the local machine, it adds unnecessary overhead. That is why I started looking at alternatives, namely Named Pipes that will not involve binary serialization and will provide fast and lightweight IPC.

The first part of this article explores a way of implementing Named Pipes based IPC between .NET applications. In Part 2, we look at building a pipe server and a client communicating with it.

Remember that the situations where this solution would be most beneficial is when one application is exchanging frequent short text messages with another, located on the same machine or within the same LAN. For structured data exchange, those text messages can also be XML documents or serialized .NET objects. No security layer is implemented because Named Pipes are accessible within the LAN only and it is assumed that security will be handled by the existing infrastructure.

For more information on Named Pipes, visit the MSDN Library. The NamedPipeNative class, part of this solution, is based on the Named Pipes Remoting Channel by Jonathan Hawkins.

2. Classes

Let's now look at some of the classes and methods that are part of the Named Pipes solution. Diagram 1 below shows those classes and the relationships between them.

There are several interfaces, part of the solution, such as IClientChannel and IInterProcessConnection, all compiled in the AppModule.InterProcessComm assembly. Those interfaces are introduced to abstract the Named Pipes implementation from clients involved in the IPC. Following the fundamental object-oriented principle of "loose coupling," our client application will use the interfaces when exchanging messages with the server, which allows the specific IPC protocol to vary if necessary.

Outlined below are the main responsibilities of the classes, part of the .NET Named Pipes solution.

NamedPipeNative: This utility class exposes kernel32.dll methods for Named Pipes communication. It also defines constants for some of the error codes and method parameter values.

NamedPipeWrapper: This class is a wrapper around NamedPipesNative. It uses the exposed kernel32.dll methods to provide controlled Named Pipes functionality.

APipeConnection: An abstract class, which defines the methods for creating Named Pipes connections, reading, and writing data. This class is inherited by the ClientPipeConnection and ServerPipeConnection classes, used by client and server applications respectively.

ClientPipeConnection: Used by client applications to communicate with server ones by using Named Pipes.

ServerPipeConnection: Allows a Named Pipes server to create connections and exchange data with clients.

PipeHandle: Holds the operating system native handle and the current state of the pipe connection.

Diagram 1: Named Pipes UML static diagram

3. Creating a Named Pipe

As part of the different Named Pipes operations, first we are going to see how a server Named Pipe is created.

Each pipe has a name as "Named Pipe" implies. The exact syntax of server pipe names is \\.\pipe\PipeName. The "PipeName" part is actually the specific name of the pipe. To connect to the pipe, a client application needs to create a client Named Pipe with the same name. If the client is located on a different machine, the name should also include the server e.g. \\SERVER\pipe\PipeName.

The following static method from NamedPipeWrapper is used to instantiate a server Named Pipe.

By calling NamedPipeNative.CreateNamedPipe, the above method creates a duplex Named Pipe of type message and sets it in blocking mode. It is also specified that unlimited instances of the pipe will be allowed.

If the pipe is created successfully, CreateNamedPipe returns the native pipe handle, which we assign to our PipeHandle object. The native handle is an operating system pointer to the Named Pipe and is further used in all pipe-related operations. The PipeHandle class is introduced to hold the native handle and also to track the current state of the pipe. The Named Pipes states are defined in the InterProcessConnectionState enumeration and they correspond to the different operations—reading, writing, waiting for clients, and so forth.

Assuming that the server Named Pipe was created successfully, it can now start listening to client connections.

4. Connecting Client Pipes

The Named Pipe server needs to be set in a listening mode in order for client pipes to connect to it. This is done by calling the NamedPipeNative.ConnectNamedPipe method. Because our pipe was created in a blocking mode, calling this method will put the current thread in waiting mode until a client pipe attempts to make a connection.

A Named Pipe client is created and connected to a listening server pipe by calling the NamedPipeNative.CreateFile method, which in turn calls the corresponding Kernel32 method. The code below, part of NamedPipeWrapper.ConnectToPipe, illustrates that.

After we create a PipeHandle object and build the pipe name, we call the NamedPipeNative.CreateFile method to create a client Named Pipe and connect it to the specified server pipe. In our example, the client pipe is configured to cater to both reading and writing.

If the client pipe is created successfully, the CreateFile method returns the native handle corresponding to the client Named Pipe, which we are going to use in subsequent operations. If for some reason the client pipe creation failed, the method would return -1, which is set to be the value of the INVALID_HANDLE_VALUE constant.

There is one more thing that needs to be done before the client Named Pipe can be used for reading and writing. We need to set its handle mode to PIPE_READMODE_MESSAGE, which will allow us to read and write messages. This is done by calling NamedPipeNative.SetNamedPipeHandleState:

Each client pipe communicates with an instance of the server pipe. If the server pipe has reached its maximum number of instances, creating a client pipe will return an error. In such cases, it is useful to check for the error type, wait for some time, and then make another attempt to create the client Named Pipe. Checking for the error type is done by the NamedPipeNative.GetLastError method:

5. Writing and Reading Data

Named Pipes do not support stream seeking, which means that when reading from a named pipe we cannot determine in advance the size of the message. As a workaround, a simple message format is introduced, which allows us to first specify the length of the message and then read or write the message itself.

Our solution will not need to cater to very large messages so we are going to use a System.Int32 variable to specify the message length. To represent an Int32 we need four bytes, so the first four bytes of our messages will always contain the message length.

5.1. Writing Data to a Named Pipe

The NamedPipeWrapper.WriteBytes method below writes a message to a Named Pipe, represented by the handle provided as an input parameter. The message itself has been converted to bytes using UTF8 encoding and is passed as a bytes array.

5.2. Reading from a Named Pipe

In order to read a message from a Named Pipe, we first need to find its length by converting the first four bytes to an integer. Then, we can read the rest of the data. The NamedPipeWrapper.ReadBytes method below illustrates that.

6. Other Named Pipes Operations

Some other operations, part of the IPC, include disconnecting, flushing, and closing Named Pipes.

DisconnectNamedPipe disconnects one end of the pipe from the other. Disconnecting a server pipe allows the latter to be reused by releasing it from the client pipe. This technique is shown in Part 2 of the article, where we build a multithreaded Named Pipes server.

FlushFileBuffers writes to the pipe any buffered data. It is often used in conjunction with writing operations before closing a Named Pipe.

CloseHandle is used to close a Named Pipe and release its native handle. It is important to always close a pipe after finishing working with it to release any related resources; therefore, any Named Pipes operations should be wrapped in a try-catch-finally block and the CloseHandle method should be placed in the finally part.

About the Author

Ivan Latunov

Ivan Latunov is a Software Architect with long term commercial experience in the areas of software architecture and engineering, design and development of web and distributed applications and business and technical analysis. Website: ivanweb.com.

UML Notations

Great article, but a couple of issues

Posted by robertdunlop
on 12/30/2005 05:40am

This article helped me to quickly get my windows service up and running with a named pipe for communication with my client apps. Until I came across this article I was failing to find anything available regarding named pipes and .NET.
However, I've got a couple of issues with the implementation. You state that:
"Named Pipes do not support stream seeking, which means that when reading from a named pipe we cannot determine in advance the size of the message. As a workaround, a simple message format is introduced, which allows us to first specify the length of the message and then read or write the message itself."
While you cannot seek a named pipe, you can use the message pipe type (which your application already uses) to synchronize blocks of data. When a message type server pipe is specified, when the server reads the pipe it will return the contents of one corresponding write operation from the client. THe same applies when writing to the client. You can use a fixed size array (maybe use that unuzed maxBytes parameter you pass to ReadBytes?) to read from the named pipe, which will return the actual number of bytes read. You can then check the status to determine if there is an error indicating that more data is available, in which case the buffer was not large enough to read the whole message, in which case you can read the pipe again to get the remainder of the data.
Thus the workaround is not really needed, and you can transmit and receive data without needing to send a message length. But the real concern that I have with the sample code is that it assumes a valid length will be received, and allocates a block to that size. In my case my unmanaged app was not sending a length, so the first 4 bytes of the data were used as an allocation size, and I kept puzzling over the out of memory exceptions I kept receiving despite having half a gig of RAM free. Using unvalidated values like that can lead to program nstabilities, not to mention the type of holes that hackers love to exploit. I would consider retouching that code, as others may utilize this code and inherit any pitfalls that come with it.
Once again though, great article, it was a definite help to get me past a sticking point, thanks!
Robert Dunlop
Microsoft DirectX MVP

why are Named Pipes confined to the same PC IPC?

Posted by vgv8
on 08/21/2005 07:25pm

Your link to MSDN
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/ipc/base/named_pipes.asp
tells:
"Named pipes can be used to provide communication between processes on the same computer or between processes on different computers across a network. If the server service is running, all named pipes are accessible remotely."
Guennadi Vanine

Top White Papers and Webcasts

U.S. companies are desperately trying to recruit and hire skilled software engineers and developers, but there is simply not enough quality talent to go around. Tiempo Development is a nearshore software development company. Our headquarters are in AZ, but we are a pioneer and leader in outsourcing to Mexico, based on our three software development centers there. We have a proven process and we are experts at providing our customers with powerful solutions. We transform ideas into reality.

When individual departments procure cloud service for their own use, they usually don't consider the hazardous organization-wide implications. Read this paper to learn best practices for setting up an internal, IT-based cloud brokerage function that service the entire organization. Find out how this approach enables you to retain top-down visibility and control of network security and manage the impact of cloud traffic on your WAN.