Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

IPC with Named Pipes

4.85/5 (15 votes)
28 Sep 2017CPOL4 min read 47.5K   2K  
This article documents a simple messaging layer implemented with named pipes.

Introduction

This article describes a messaging library which can be used to send a message between two .NET applications running on the same network. The library allows a simple string to be sent between a Client and Server application.

The library uses Named Pipes in its internal implementation. For more information on Named Pipes, visit the MSDN page.

I also used the following CodeProject articles as a starting point for developing the library:

Background

I recently came across a scenario where I needed to notify an application that an upgrade was waiting to start. I needed a simple mechanism that would allow me to signal the application. Once signalled, the application would safely shutdown, allowing the upgrade to start.

I investigated a number of IPC solution before developing this library. I settled for Named Pipes as it was the most light weight method of IPC I could find.

Using the Code

The library is simple to use, there are two main entry points, the PipeServer and the PipeClient. The following code snippet is copied from the sample application included in the source code:

C#
var pipeServer = new PipeServer("demo", PipeDirection.InOut);
pipeServer.MessageReceived += (s, o) => pipeServer.Send(o.Message); 
pipeServer.Start();

var pipeClient = new PipeClient("demo", PipeDirection.InOut);
pipeClient.MessageReceived += (s, o) => Console.WriteLine("Client Received: value: {0}", o.Message);
pipeClient.Connect();

The sample application demonstrates using the library to build a simple echo server. The MessageReceived event handler simply echoes any messages received from the client.

PipeServer

Start

The start method calls BeginWaitingForConnection passing a state object. The Start method is overloaded allowing the client to provide a cancellation token.

C#
public void Start(CancellationToken token)
{
    if (this.disposed)
    {
       throw new ObjectDisposedException(typeof(PipeServer).Name);
    }

    var state = new PipeServerState(this.ServerStream, token);
    this.ServerStream.BeginWaitForConnection(this.ConnectionCallback, state);
}

Stop

The stop method simply calls the Cancel method of the internal CancelllationTokenSource. Calling Cancel sets the IsCancelationRequested property of the token which gracefully terminates the Server.

C#
public void Stop()
{
    if (this.disposed)
    {
        throw new ObjectDisposedException(typeof(PipeServer).Name);
    }

    this.cancellationTokenSource.Cancel();
}

Send

The send method first converts the provided string to a byte array. The bytes are then written to the stream using the BeginWrite method of the PipeStream.

C#
public void Send(string value)
{
    if (this.disposed)
    {
        throw new ObjectDisposedException(typeof(PipeClient).Name);
    }

    byte[] buffer = Encoding.UTF8.GetBytes(value);
    this.ServerStream.BeginWrite(buffer, 0, buffer.Length, this.SendCallback, this.ServerStream);
}

ReadCallback

The ReadCallback method is called when data is received on the incoming stream. The received bytes are first read from the stream, decoded back to a string and stored in the state object.

If the IsMessageComplete property is set, this indicates that the Client has finished sending the current message. The MessageReceived event in invoked and the Message buffer is cleared.

If the server has not stopped - indicated by the cancellation token - and the client is still connected, the server continues reading data, otherwise the server begins waiting for the next connection.

C#
private void ReadCallback(IAsyncResult ar)
{
    var pipeState = (PipeServerState)ar.AsyncState;

    int received = pipeState.PipeServer.EndRead(ar);
    string stringData = Encoding.UTF8.GetString(pipeState.Buffer, 0, received);
    pipeState.Message.Append(stringData);
    if (pipeState.PipeServer.IsMessageComplete)
    {
        this.OnMessageReceived(new MessageReceivedEventArgs(stringData));
        pipeState.Message.Clear();
    }

    if (!(this.cancellationToken.IsCancellationRequested || 
             pipeState.ExternalCancellationToken.IsCancellationRequested))
    {
        if (pipeState.PipeServer.IsConnected)
        {
            pipeState.PipeServer.BeginRead(pipeState.Buffer, 0, 255, this.ReadCallback, pipeState);
        }
        else
        {
            pipeState.PipeServer.BeginWaitForConnection(this.ConnectionCallback, pipeState);
        }
    }
}

PipeClient

Connect

The connect method simply establishes a connection to the PipeServer, and begins readings the first message received from the server. An interesting caveat is that you can only set the ReadMode of the ClientStream once a connection has been established.

C#
public void Connect(int timeout = 1000)
{
    if (this.disposed)
    {
        throw new ObjectDisposedException(typeof(PipeClient).Name);
    }

    this.ClientStream.Connect(timeout);
    this.ClientStream.ReadMode = PipeTransmissionMode.Message;

    var clientState = new PipeClientState(this.ClientStream);
    this.ClientStream.BeginRead(
        clientState.Buffer,
        0,
        clientState.Buffer.Length, 
        this.ReadCallback, 
        clientState);
}

Send

The send method for the PipeClient is very similar to the PipeServer. The provided string is converted to a byte array and then written to the stream.

C#
public void Send(string value)
{
    if (this.disposed)
    {
        throw new ObjectDisposedException(typeof(PipeClient).Name);
    }

    byte[] buffer = Encoding.UTF8.GetBytes(value);
    this.ClientStream.BeginWrite(buffer, 0, buffer.Length, this.SendCallback, this.ClientStream);
}

ReadCallback

The ReadCallback method is again similar to the PipeServer.ReadCallback method without the added complication of handling cancellation.

C#
private void ReadCallback(IAsyncResult ar)
{
    var pipeState = (PipeClientState)ar.AsyncState;
    int received = pipeState.PipeClient.EndRead(ar);
    string stringData = Encoding.UTF8.GetString(pipeState.Buffer, 0, received);
    pipeState.Message.Append(stringData);
    if (pipeState.PipeClient.IsMessageComplete)
    {
        this.OnMessageReceived(new MessageReceivedEventArgs(pipeState.Message.ToString()));
        pipeState.Message.Clear();
    }

    if (pipeState.PipeClient.IsConnected)
    {
        pipeState.PipeClient.BeginRead(pipeState.Buffer, 0, 255, this.ReadCallback, pipeState);
    }
}

Points of Interest

NamedPipes vs AnonymousPipes

The System.IO.Pipes namespace contains a managed API for both AnonymousPipes and NamedPipes. The MSDN page states the following:

Anonymous pipes

Quote:

Anonymous pipes are one-way and cannot be used over a network. They support only a single server instance. Anonymous pipes are useful for communication between threads, or between parent and child processes where the pipe handles can be easily passed to the child process when it is created

Named pipes

Quote:

Named pipes provide interprocess communication between a pipe server and one or more pipe clients. Named pipes can be one-way or duplex. They support message-based communication and allow multiple clients to connect simultaneously to the server process using the same pipe name.

I based the library on Named pipes because I wanted the library to support duplex communication. Also, the parent child model of Anonymous pipes didn't fit with my scenario.

PipeTansmissionMode

Named pipes offer two transmission modes, Byte mode and Message mode.

  • In Byte mode - messages travel as a continuous stream of bytes between the client and server.
  • In Message mode - the client and the server send and receive data in discrete units. The end of a message is indicated by setting the IsMessageComplete property.

In both modes, a write on one side will not always result in a same-size read on the other. This means that a client application and a server application do not know how many bytes are being read from or written to a pipe at any given moment.

In Byte mode, the end of a complete message could be identified by searching for and End-Of-Message marker. This would require defining an application level protocol which would add needless complexity to the library.

In message mode, the end of a message can be identified by reading the IsMessageComplete property. The IsMessageComplete property is set to true by calling Read or EndRead.

Source Code

If you would like to view the libraries source code and demo applications, the code can be found on my Bit Bucket site.

The git repository address is as follows:

History

Date Changes
07/01/2015 Initial release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)