Using Win32 Named Pipes in the .NET Framework |
Developers familiar with the Windows API may be aware of named pipes,
a mechanism for communicating between processes. The .NET Framework introduces
.NET Remoting, a new way of inter-process communication. Unfortunately, the lack
of native named pipe support in the .NET Framework makes it difficult to
co-exist with legacy applications that require named pipes.
This sample shows how to use the Platform Invoke functionality to call Win32 APIs for named pipes from a managed application. It only includes basic named pipe support - overlapped I/O and transactions are not included. The code here shows a class called NamedPipe, and a server and client application that use it. The application is quite simple; the client sends the server a message containing the word "HELLO", and the server sends back the string "ACK". Building the Sample To build the sample applications, use the following two lines. To use the NamedPipe class in your application, just change the namespace and build it into your project. Note: Because the NamedPipe class uses "unsafe" (unmanaged) code to call the Win32 ReadFile and WriteFile APIs, you need to build your project with the /unsafe option. csc /unsafe /out:Client.exe /r:System.dll NamedPipe.cs PipeClient.cs csc /unsafe /out:Server.exe /r:System.dll NamedPipe.cs PipeServer.cs NamedPipe.cs using System; using System.ComponentModel; using System.IO; using System.Runtime.InteropServices; namespace Acme { // The NamedPipeAccess enumeration controls whether you want to create a // pipe that has read or write access, or both. public enum NamedPipeAccess { Read, Write, ReadWrite, } // The NamedPipeMode enumeration controls whether you want a message or // byte based pipe. public enum NamedPipeMode { Message, Byte } public class NamedPipeStream : Stream, IDisposable { public const int DefaultBufferSize = 512; public const int DefaultTimeout = 20000; public const int UnlimitedInstances = 255; private IntPtr _handle; private bool _created; private bool _connected; private NamedPipeAccess _access; private Byte[] _bufferByte = new Byte[0]; private NamedPipeStream() { } // Various forms of Open calls. public static NamedPipeStream Open(string name) { return NamedPipeStream.Open(name, NamedPipeMode.Message); } public static NamedPipeStream Open(string name, NamedPipeMode mode) { return NamedPipeStream.Open(name, mode, NamedPipeAccess.ReadWrite); } public static NamedPipeStream Open(string name, NamedPipeMode mode, NamedPipeAccess access) { return NamedPipeStream.Open(name, mode, access, NamedPipeStream.DefaultTimeout); } public static NamedPipeStream Open(string name, NamedPipeMode mode, NamedPipeAccess access, int waitTimeout) { NamedPipeStream stream = new NamedPipeStream(); stream.OpenPipe(name, mode, access, waitTimeout); return stream; } // The method that does the real work of opening an existing pipe. private void OpenPipe(string name, NamedPipeMode mode, NamedPipeAccess access, int waitTimeout) { IntPtr handle; // Translate access to native flags. uint desiredAccess; switch (access) { case NamedPipeAccess.Read: desiredAccess = Interop.GENERIC_READ; break; case NamedPipeAccess.Write: desiredAccess = Interop.GENERIC_WRITE; break; default: desiredAccess = Interop.GENERIC_READ | Interop.GENERIC_WRITE; break; } // Format pipe name into "\\.\pipe\...." format string pipeName = FormatPipeName(name); while (true) { // Call CreateFile to obtain handle handle = Interop.CreateFile(pipeName, desiredAccess, 0, IntPtr.Zero, Interop.OPEN_EXISTING, 0, IntPtr.Zero); if (handle.ToInt32() != -1) { break; } // The pipe may be busy, so wait. if (Marshal.GetLastWin32Error() != Interop.ERROR_PIPE_BUSY || Interop.WaitNamedPipe(pipeName, (uint)waitTimeout) == 0) { throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not open pipe"); } } // If opened for writing, we can set the pipe mode. if (mode == NamedPipeMode.Message && access != NamedPipeAccess.Read) { uint readMode = Interop.PIPE_READMODE_MESSAGE; if (Interop.SetNamedPipeHandleState(handle, ref readMode, IntPtr.Zero, IntPtr.Zero) == 0) { Interop.CloseHandle(handle); throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not set pipe to message mode"); } } _handle = handle; _created = false; _connected = true; _access = access; } public override void Close() { if (_handle != IntPtr.Zero) { // If we created the pipe, we need to do additional // cleanup. if (_created) { if (_connected) { Interop.FlushFileBuffers(_handle); } Interop.DisconnectNamedPipe(_handle); } Interop.CloseHandle(_handle); _handle = IntPtr.Zero; _connected = false; } } // Dispose semantics. void IDisposable.Dispose() { Dispose(true); } ~NamedPipeStream() { Dispose(false); } protected virtual void Dispose(bool disposing) { Close(); } // Various forms of Create calls. public static NamedPipeStream Create(string name) { return NamedPipeStream.Create(name, NamedPipeMode.Message); } public static NamedPipeStream Create(string name, NamedPipeMode mode) { return NamedPipeStream.Create(name, mode, NamedPipeStream.DefaultBufferSize); } public static NamedPipeStream Create(string name, NamedPipeMode mode, int bufferSize) { return NamedPipeStream.Create(name, mode, bufferSize, NamedPipeStream.DefaultTimeout); } public static NamedPipeStream Create(string name, NamedPipeMode mode, int bufferSize, int timeout) { return NamedPipeStream.Create(name, mode, mode, NamedPipeAccess.ReadWrite, NamedPipeStream.UnlimitedInstances, bufferSize, bufferSize, timeout); } public static NamedPipeStream Create(string name, NamedPipeMode readMode, NamedPipeMode writeMode, NamedPipeAccess access, int maxInstances, int readBufferSize, int writeBufferSize, int waitTimeout) { NamedPipeStream stream = new NamedPipeStream(); stream.CreatePipe(name, readMode, writeMode, access, maxInstances, readBufferSize, writeBufferSize, waitTimeout); return stream; } // The method that does the real work of creating an existing pipe. public void CreatePipe(string name, NamedPipeMode readMode, NamedPipeMode writeMode, NamedPipeAccess access, int maxInstances, int readBufferSize, int writeBufferSize, int waitTimeout) { IntPtr handle; // Translate access settings to native flags. uint pipeAccess; switch (access) { case NamedPipeAccess.Read: pipeAccess = Interop.PIPE_ACCESS_INBOUND; break; case NamedPipeAccess.Write: pipeAccess = Interop.PIPE_ACCESS_OUTBOUND; break; default: pipeAccess = Interop.PIPE_ACCESS_DUPLEX; break; } // Translate pipe mode to native flags. uint mode = 0; mode |= ((writeMode == NamedPipeMode.Message) ? Interop.PIPE_TYPE_MESSAGE : Interop.PIPE_TYPE_BYTE); mode |= ((readMode == NamedPipeMode.Message) ? Interop.PIPE_READMODE_MESSAGE : Interop.PIPE_READMODE_BYTE); // Create the pipe. handle = Interop.CreateNamedPipe(FormatPipeName(name), pipeAccess, mode, (uint)maxInstances, (uint)writeBufferSize, (uint)readBufferSize, (uint)waitTimeout, IntPtr.Zero); if (handle.ToInt32() == -1) { throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not create pipe"); } _handle = handle; _created = true; _connected = false; _access = access; } // Overrides Stream.CanRead public override bool CanRead { get { return _connected && (_access == NamedPipeAccess.Read || _access == NamedPipeAccess.ReadWrite); } } // Overrides Stream.CanSeek. Named pipes can't seek. public override bool CanSeek { get { return false; } } // Overrides Stream.CanWrite public override bool CanWrite { get { return _connected && (_access == NamedPipeAccess.Write || _access == NamedPipeAccess.ReadWrite); } } // Overrides Stream.Length. The length of a pipe is unknown. public override long Length { get { throw new NotSupportedException(); } } // Overrides Stream.Position. Named pipes don't have position. public override long Position { get { throw new NotSupportedException(); } set { throw new NotSupportedException(); } } // Overrides Stream.Seek. Named pipes can't seek. public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } // Overrides Stream.Flush. Flushes the file handle. public override void Flush() { Interop.FlushFileBuffers(_handle); } // Overrides Stream.SetLength. Named pipes don't have preset lengths. public override void SetLength(long value) { throw new NotSupportedException(); } // Overrides Stream.Read public override int Read(Byte[] buffer, int startAt, int length) { if (!CanRead) { throw new Win32Exception(Marshal.GetLastWin32Error(), "Cannot read from stream"); } return Interop.ReadFileNative(_handle, buffer, startAt, length); } // Overrides Stream.Write public override void Write(Byte[] buffer, int startAt, int length) { if (!CanWrite) { throw new Win32Exception(Marshal.GetLastWin32Error(), "Cannot write to stream"); } Interop.WriteFileNative(_handle, buffer, startAt, length); } // Overrides Stream.ReadByte public override int ReadByte() { if (Interop.ReadFileNative(_handle, _bufferByte, 0, 1) == 1) { return _bufferByte[0]; } else { return -1; } } // Overrides Stream.WriteByte public override void WriteByte(Byte value) { _bufferByte[0] = value; Interop.WriteFileNative(_handle, _bufferByte, 0, 1); } // Connects to a client. public void Connect() { if (!_created) { throw new Win32Exception(Marshal.GetLastWin32Error(), "Cannot call Connect on existing pipe"); } if (_connected) { throw new Win32Exception(Marshal.GetLastWin32Error(), "Pipe already connected"); } if (Interop.ConnectNamedPipe(_handle, IntPtr.Zero) == 0) { throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not connect named pipe"); } _connected = true; } private string FormatPipeName(string name) { return String.Format("\\\\.\\pipe\\{0}", name); } // Interop class that includes all the PInvoke methods, native // constants, etc. private class Interop { [DllImport("Kernel32.dll", CharSet=CharSet.Auto, SetLastError=true)] internal static extern IntPtr CreateFile(String lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile); [DllImport("Kernel32.dll", SetLastError=true)] internal static extern void CloseHandle(IntPtr handle); [DllImport("Kernel32.dll", CharSet=CharSet.Auto, SetLastError=true)] internal static extern int WaitNamedPipe(string pipeName, uint timeout); [DllImport("Kernel32.dll", SetLastError=true)] internal static extern int SetNamedPipeHandleState(IntPtr handle, ref uint mode, IntPtr maxBytes, IntPtr maxTime); [DllImport("Kernel32.dll", SetLastError=true)] internal static extern int FlushFileBuffers(IntPtr handle); [DllImport("Kernel32.dll", SetLastError=true)] internal static extern int DisconnectNamedPipe(IntPtr handle); [DllImport("Kernel32.dll", CharSet=CharSet.Auto, SetLastError=true)] internal static extern IntPtr CreateNamedPipe(string pipeName, uint openMode, uint pipeMode, uint maxInstances, uint outBufferSize, uint inBufferSize, uint defaultTimeout, IntPtr securityAttributes); [DllImport("Kernel32.dll", SetLastError=true)] internal static extern int ConnectNamedPipe(IntPtr handle, IntPtr overlapped); [DllImport("Kernel32.dll", SetLastError=true)] internal static unsafe extern int ReadFile(IntPtr handle, byte* bytes, uint numBytesToRead, out uint numBytesRead, IntPtr overlapped); [DllImport("Kernel32.dll", SetLastError=true)] internal static unsafe extern int WriteFile(IntPtr handle, byte* bytes, uint numBytesToWrite, out uint numBytesWritten, IntPtr overlapped); internal const uint GENERIC_READ = (uint)0x80000000; internal const uint GENERIC_WRITE = (uint)0x40000000; internal const uint OPEN_EXISTING = 3; internal const uint ERROR_BROKEN_PIPE = 109; internal const uint ERROR_PIPE_BUSY = 231; internal const uint PIPE_ACCESS_INBOUND = 1; internal const uint PIPE_ACCESS_OUTBOUND = 2; internal const uint PIPE_ACCESS_DUPLEX = 3; internal const uint PIPE_TYPE_BYTE = 0; internal const uint PIPE_TYPE_MESSAGE = 4; internal const uint PIPE_READMODE_BYTE = 0; internal const uint PIPE_READMODE_MESSAGE = 2; // Unsafe method to read from a file handle using PInvoke. Unsafe // because we need to pass ReadFile a pointer to the buffer. internal static unsafe int ReadFileNative(IntPtr handle, Byte[] buffer, int offset, int length) { // Verify length if (buffer.Length == 0 || offset + length > buffer.Length) { throw new IndexOutOfRangeException(); } uint numBytesRead = 0; int ret; fixed (byte* p = buffer) { ret = ReadFile(handle, p + offset, (uint)length, out numBytesRead, IntPtr.Zero); } if (ret == 0) { if (Marshal.GetLastWin32Error() == ERROR_BROKEN_PIPE) { return -1; } else { throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not read from pipe"); } } return (int)numBytesRead; } // Unsafe method to write to a file handle using PInvoke. Unsafe // because we need to pass WriteFile a pointer to the buffer. internal static unsafe void WriteFileNative(IntPtr handle, Byte[] buffer, int offset, int length) { if (length == 0) { return; } if (buffer.Length == 0 || offset + length > buffer.Length) { throw new IndexOutOfRangeException(); } uint numBytesWritten = 0; int ret; fixed (byte* p = buffer) { ret = WriteFile(handle, p + offset, (uint)length, out numBytesWritten, IntPtr.Zero); } if (ret == 0 || numBytesWritten != length) { throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not write to pipe"); } } } } } PipeServer.cs The PipeServer application creates a new pipe called "MyPipe", and listens on it. Client requests are handled using the thread pooling functionality built into the .NET Framework. Each time a client request comes in, the queues a work item to process the client request. The system uses a thread from its thread pool to process the request. using Acme; using System; using System.IO; using System.Text; using System.Threading; public class PipeServer { public static void Main(String[] args) { WaitCallback _handlePipeConnection = new WaitCallback(PipeServer.HandlePipeConnection); while (true) { // Create a pipe, and wait for a client to connect to it. NamedPipeStream pipe = NamedPipeStream.Create("MyPipe"); pipe.Connect(); // Create a thread pool work item to handle the connection. ThreadPool.QueueUserWorkItem(_handlePipeConnection, pipe); } } public static void HandlePipeConnection(Object o) { NamedPipeStream pipe = (NamedPipeStream)o; Byte[] readBytes = new Byte[512]; while (true) { int read = pipe.Read(readBytes, 0, readBytes.Length); if (read > 0) { Console.WriteLine("Read: " + Encoding.ASCII.GetString(readBytes, 0, read)); Byte[] writeBytes = Encoding.ASCII.GetBytes("ACK"); pipe.Write(writeBytes, 0, writeBytes.Length); Console.WriteLine("Written: ACK"); } else { // Return value of 0 or less means the pipe has closed. break; } } pipe.Close(); } } PipeClient.cs The PipeClient application opens an existing pipe called "MyPipe", and then writes and reads from it. using Acme; using System; using System.IO; using System.Text; public class PipeClient { public static void Main(String[] args) { NamedPipeStream pipe = NamedPipeStream.Open("MyPipe"); Byte[] writeBytes = Encoding.ASCII.GetBytes("HELLO"); pipe.Write(writeBytes, 0, writeBytes.Length); Console.WriteLine("Written: HELLO"); Byte[] readBytes = new Byte[512]; int read = pipe.Read(readBytes, 0, readBytes.Length); if (read > 0) { Console.WriteLine("Read: " + Encoding.ASCII.GetString(readBytes, 0, read)); } pipe.Close(); } } |