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