Introduction
I know this is the nth article about using Windows Pipes in .NET. The only reason I am writing this is because I wanted a very simple implementation of a managed wrapper for Windows Pipes. Ivan's article on The Code Project is like an example of military-grade implementation! I wanted something simpler and easier to use.
How much easier? Sending a message should be as easy as _pipe.Send("Some command string")
and receiving a message should be as easy as string message = _pipe.Receive()
My implementation uses only one class, ManagedPipe
, that can create a server pipe connection:
_pipe = new ManagedPipe("Test1", ManagedPipeType.Server,...
or a client pipe connection:
_pipe = new ManagedPipe("Test1", ManagedPipeType.Client,...
The ManagedPipe
wrapper allows full duplex communication between the server and the client. This means the server and client can send
and receive
messages at the same time!
Background
People looking for something more flexible and robust should read the articles by Ivan and John Korres.
You will also be interested in reading about Windows Pipes in the MSDN library.
Server Connection
Since this is supposed to be a very easy-to-use wrapper, let's see how quickly we can create a connection and start sending messages.
A server connection is created like this:
_pipe = new ManagedPipe("Test1", ManagedPipeType.Server,
BlockingMode.Wait, 1024, 1024);
_pipe.Open();
The first parameter is the name of the pipe. The second parameter specifies if this is a server application or a client application. The third parameter is to specify whether the send
and receive
operations should block or return immediately. The fourth and fifth parameters are input and output buffer sizes. You can set them to 1K or more based on your average message size.
I have not added any try
-catch
block to keep the implementation simple. Your production code must have try
-catch
when calling Open()
. Before the server application can send
or receive
on the pipe you have to call:
_pipe.WaitForClientToConnect();
The above function is a blocking call and will not return until a client connects to the server. Once this function returns, you can call the send receive
functions like this:
_pipe.Send("Hello");
......
string message = _pipe.Receive();
......
If a blocking _pipe.Receive()
returns null
, that means the client has closed the connection. Any more calls to send
or receive
are not valid. Keep in mind: you must have all these functions inside a try
-catch
block. Here's the code from my sample:
private void ReceiveThreadFunction()
{
_pipe.WaitForClientToConnect();
btnSend.Invoke((MethodInvoker)delegate()
{
btnSend.Enabled = true;
});
while (true)
{
string message = _pipe.Receive();
if (message == null)
return;
txtReceiveMessage.Invoke((MethodInvoker)delegate()
{
txtReceiveMessage.Text += message + "\r\n";
});
}
}
Client Connection
The client connection code is as follows:
_pipe = new ManagedPipe("Test1", ManagedPipeType.Client,
BlockingMode.Wait, 1024, 1024);
_pipe.Open();
This is exactly similar to the server connection call except for the second parameter. All the other parameters should be identical to the ones specified on the server connection. To find out why, please read about Windows Pipes on MSDN.
Sending and receiving messages is similar to what we did in the server connection. The difference is that from the client you do not call WaitForClientToConnect()
. The client can send
and receive
messages as soon as it connects to the server. When the server goes down, a blocking _pipe.Receive()
will return null
. Future calls to send
or receive
are not valid. Here's the code from my sample:
private void ReceiveThreadFunction()
{
while (true)
{
string message = _pipe.Receive();
if (message == null)
return;
txtReceiveMessage.Invoke((MethodInvoker)delegate()
{
txtReceiveMessage.Text += message + "\r\n";
});
}
}
Dispose!
Once you are finished using the ManagedPipe
wrapper, do not forget to call:
_pipe.Dispose();
I know .NET has garbage collection and all that. The GC might eventually call dispose
, depending on what kind of code you write. Holding on to resources after you are done using them is a waste. As a good programming practice, you should call dispose
on an object once you are done using it. This makes sure all managed and unmanaged resources used by that object are released for other processes to use.
The ManagedPipe Wrapper
You would have noticed that I have not mentioned anything about the wrapper code. This was deliberate. To use the wrapper, one does not need to know its internals. Remember, it's supposed to be very easy to use! Now that we have covered the "how to use", lets' take a look at the wrapper itself.
The wrapper internally creates two pipe connections. The first connection is a dedicated pipe to send
messages and the second connection is a dedicated pipe to receive
messages. This allows full duplex communication between the server and the client.
Design
Code
The server pipe connection is created like this:
protected void CreateServer()
{
UInt32 pipeMode;
if (_blockingMode == BlockingMode.Wait)
pipeMode = PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT;
else
pipeMode = PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_NOWAIT;
_hSendPipe = CreateNamedPipe(@"\\.\pipe\Send_" + _name,
PIPE_ACCESS_OUTBOUND,
pipeMode,
1,
_outputBufferSize,
_inputBufferSize,
0,
IntPtr.Zero);
if (_hSendPipe.IsInvalid)
throw new Win32Exception(Marshal.GetLastWin32Error());
_hReceivePipe = CreateNamedPipe(@"\\.\pipe\Receive_" + _name,
PIPE_ACCESS_INBOUND,
pipeMode,
1,
_outputBufferSize,
_inputBufferSize,
0,
IntPtr.Zero);
if (_hReceivePipe.IsInvalid)
throw new Win32Exception(Marshal.GetLastWin32Error());
}
As mentioned earlier, you will notice that I create two pipe connections to facilitate parallel send
and receive
. To keep the implementation simple, I have kept the maximum number of pipe instances to one and I have not used overlapped input and output. Remember, this is not the military grade implementation! If you need something more powerful, then you should read the other articles mentioned in the "Background" section.
The client pipe connection is created like this:
protected void CreateClient()
{
_hSendPipe = CreateFile(@"\\.\pipe\Receive_" + _name,
GENERIC_WRITE,
0,
IntPtr.Zero,
OPEN_EXISTING,
0,
IntPtr.Zero);
if (_hSendPipe.IsInvalid)
throw new Win32Exception(Marshal.GetLastWin32Error());
_hReceivePipe = CreateFile(@"\\.\pipe\Send_" + _name,
GENERIC_READ,
0,
IntPtr.Zero,
OPEN_EXISTING,
0,
IntPtr.Zero);
if (_hReceivePipe.IsInvalid)
throw new Win32Exception(Marshal.GetLastWin32Error());
}
Here we swap the server's send
pipe with the client's receive
pipe and vice versa. The pipe on which the server is sending messages is the client's receive
pipe and the pipe on which the server is receiving messages is the client's send
pipe.
The rest of the wrapper code is pretty straight forward.
public void Open()
{
if (_type == ManagedPipeType.Server)
CreateServer();
else
CreateClient();
_textReader = new StreamReader(new FileStream(_hReceivePipe,
FileAccess.Read));
_textWriter = new StreamWriter(new FileStream(_hSendPipe,
FileAccess.Write));
}
public void Send(string message)
{
if (_hSendPipe == null)
throw new InvalidOperationException("Pipe is not open.");
_textWriter.WriteLine(message);
_textWriter.Flush();
}
public string Receive()
{
if (_hReceivePipe == null)
throw new InvalidOperationException("Pipe is not open.");
return _textReader.ReadLine();
}
When Open()
is called, we initialize a text stream reader and a writer. We attach them to receive
and send
pipes respectively. The text reader and writer are used to send
and receive
text messages between the server and client.
Points of Interest
throw new Win32Exception(Marshal.GetLastWin32Error())
is a great time saver statement in .NET. I remember how painful it was to get the Win32 error message during my Visual C++, MFC days.
History
- 09/10/2007: First published
- 05/02/2008: Edited the "Points of Interest" section