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.

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, 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.

public static PipeHandle ConnectToPipe(string pipeName,
       string serverName) {
  PipeHandle handle = new PipeHandle();
  // Build the name of the pipe.
  string name = @"\\" + serverName + @"\pipe\" + pipeName;

  for (int i = 1; i<=ATTEMPTS; i++) {
    handle.State = InterProcessConnectionState.ConnectingToServer;
    // Try to connect to the server
    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 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:

if (handle.Handle.ToInt32() != NamedPipeNative.INVALID_HANDLE_VALUE) {
   // The client managed to connect to the server pipe
   handle.State = InterProcessConnectionState.ConnectedToServer;
   // Set the read mode of the pipe channel
   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, 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:

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 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.

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;

Finallyn 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 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.

------------------------------

Discuss this article at ivanweb.com.



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.

Downloads

Comments

  • wheloltabotly PumeSonee Phobereurce 6479521

    Posted by TizefaTaNaday on 07/06/2013 12:46pm

    hallepFreew myhee.com/zbxes/louis-vuitton-outlet-in-deutschland.html audiplePela www.bestlavori.it/file/burberrybabydress.html odomouryisy

    Reply
  • UML Notations

    Posted by JohnsonFW on 01/11/2009 11:56pm

    Hi Ivan, It seems tha UML notations here are different from what I learned at the University. Can you provide me a link of UML notations? Thanks. Johnson

    Reply
  • 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

    Reply
  • 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

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • The hard facts on SaaS adoption in over 80,000 enterprises: Public vs. private companies Mid-market vs. large enterprise GoogleApps, Office365, Salesforce & more Why security is a growing concern Fill out the form to download the full cloud adoption report.

  • On-demand Event Event Date: December 18, 2014 The Internet of Things (IoT) incorporates physical devices into business processes using predictive analytics. While it relies heavily on existing Internet technologies, it differs by including physical devices, specialized protocols, physical analytics, and a unique partner network. To capture the real business value of IoT, the industry must move beyond customized projects to general patterns and platforms. Check out this webcast and join industry experts as …

Most Popular Programming Stories

More for Developers

RSS Feeds