Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

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

0.00/5 (No votes)
14 Jun 2004 2  
This article explores a way of implementing Named Pipes based Inter-Process Communication between .NET applications

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();
  // 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 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) {
  // 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, 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.

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