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, then 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 in order 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. In order 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.
public static PipeHandle Create(string name,
uint outBuffer,
uint inBuffer) {
name = @"\\.\pipe\" + name;
PipeHandle handle = new PipeHandle();
for (int i = 1; i<=ATTEMPTS; i++) {
handle.State = InterProcessConnectionState.Creating;
handle.Handle = NamedPipeNative.CreateNamedPipe(
name,
NamedPipeNative.PIPE_ACCESS_DUPLEX,
NamedPipeNative.PIPE_TYPE_MESSAGE |
NamedPipeNative.PIPE_READMODE_MESSAGE |
NamedPipeNative.PIPE_WAIT,
NamedPipeNative.PIPE_UNLIMITED_INSTANCES,
outBuffer,
inBuffer,
NamedPipeNative.NMPWAIT_WAIT_FOREVER,
IntPtr.Zero);
if (handle.Handle.ToInt32() !=
NamedPipeNative.INVALID_HANDLE_VALUE) {
handle.State = InterProcessConnectionState.Created;
break;
}
if (i >= ATTEMPTS) {
handle.State = InterProcessConnectionState.Error;
throw new NamedPipeIOException("Error creating named pipe "
+ name + " . Internal error: " +
NamedPipeNative.GetLastError().ToString(),
NamedPipeNative.GetLastError());
}
}
return handle;
}
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, etc.
Assuming that the server Named Pipe was created successfully, it can now start listening to client connections.
4. Connecting Client Pipes
The server Named Pipe 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 client Named Pipe 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.
public static PipeHandle ConnectToPipe(string pipeName,
string serverName) {
PipeHandle handle = new PipeHandle();
string name = @"\\" + serverName + @"\pipe\" + pipeName;
for (int i = 1; i<=ATTEMPTS; i++) {
handle.State = InterProcessConnectionState.ConnectingToServer;
handle.Handle = NamedPipeNative.CreateFile(name,
NamedPipeNative.GENERIC_READ | NamedPipeNative.GENERIC_WRITE,
0, null, NamedPipeNative.OPEN_EXISTING, 0, 0);
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 for 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
:
if (handle.Handle.ToInt32() != NamedPipeNative.INVALID_HANDLE_VALUE) {
handle.State = InterProcessConnectionState.ConnectedToServer;
uint mode = NamedPipeNative.PIPE_READMODE_MESSAGE;
if (NamedPipeNative.SetNamedPipeHandleState(handle.Handle,
ref mode, IntPtr.Zero, IntPtr.Zero)) {
break;
}
Each client pipe communicates with an instance of the server pipe. If the server pipe has reached its maximum number of instances, then creating a client pipe will return an error. In such case, 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:
if (NamedPipeNative.GetLastError() ==
NamedPipeNative.ERROR_PIPE_BUSY)
NamedPipeNative.WaitNamedPipe(name, WAIT_TIME);
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 for very large messages so we are going to use a System.Int32
variable to specify the message length. In order 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.
public static void WriteBytes(PipeHandle handle, byte[] bytes) {
byte[] numReadWritten = new byte[4];
uint len;
if (bytes == null) {
bytes = new byte[0];
}
if (bytes.Length == 0) {
bytes = new byte[1];
bytes = System.Text.Encoding.UTF8.GetBytes(" ");
}
Get the length of the message:
len = (uint)bytes.Length;
handle.State = InterProcessConnectionState.Writing;
Get the bytes representation of the message length and write those four bytes first:
if (NamedPipeNative.WriteFile(handle.Handle,
BitConverter.GetBytes(len), 4, numReadWritten, 0)) {
Write the rest of the message:
if (!NamedPipeNative.WriteFile(handle.Handle, bytes,
len, numReadWritten, 0)) {
handle.State = InterProcessConnectionState.Error;
throw new NamedPipeIOException("Error writing to pipe.
Internal error: " + NamedPipeNative.GetLastError().ToString(),
NamedPipeNative.GetLastError());
}
}
else {
handle.State = InterProcessConnectionState.Error;
throw new NamedPipeIOException("Error writing to pipe.
Internal error: " + NamedPipeNative.GetLastError().ToString(),
NamedPipeNative.GetLastError());
}
handle.State = InterProcessConnectionState.Flushing;
Finally flush the pipe. Flushing the Named Pipe ensures that any buffered data is written to the pipe and will not get lost.
Flush(handle);
handle.State = InterProcessConnectionState.FlushedData;
}
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.
public static byte[] ReadBytes(PipeHandle handle, int maxBytes) {
byte[] numReadWritten = new byte[4];
byte[] intBytes = new byte[4];
byte[] msgBytes = null;
int len;
handle.State = InterProcessConnectionState.Reading;
handle.State = InterProcessConnectionState.Flushing;
Read the first four bytes and convert them to an integer:
if (NamedPipeNative.ReadFile(handle.Handle, intBytes,
4, numReadWritten, 0)) {
len = BitConverter.ToInt32(intBytes, 0);
msgBytes = new byte[len];
handle.State = InterProcessConnectionState.Flushing;
Read the rest of the data or throw an exception:
if (!NamedPipeNative.ReadFile(handle.Handle, msgBytes, (uint)len,
numReadWritten, 0)) {
handle.State = InterProcessConnectionState.Error;
throw new NamedPipeIOException("Error reading from pipe.
Internal error: " + NamedPipeNative.GetLastError().ToString(),
NamedPipeNative.GetLastError());
}
}
else {
handle.State = InterProcessConnectionState.Error;
throw new NamedPipeIOException("Error reading from pipe.
Internal error: " + NamedPipeNative.GetLastError().ToString(),
NamedPipeNative.GetLastError());
}
handle.State = InterProcessConnectionState.ReadData;
if (len > maxBytes) {
return null;
}
return msgBytes;
}
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 in order 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.
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below. A list of licenses authors might use can be found here.