Inter Process Communication in C#

Introduction

Alright, so you guys might know about the famous web-browser, Google Chrome. One remarkable thing in this (pretty) simple looking application is the use of a programming technique called IPC or Interprocess Communication; with IPC you can communicate with another process without invoking and other third party events. One of the most irritating about .Net is that the app freezes if it is trying to communicate with a web-service. With IPC we can eradicate this problem.

What can you do with it?

So, lets first take an example of what exactly we can do with this: suppose that you have a licensing application named LicMan.exe and a processing app called LicPro.exe. LicMan has the UI and every other part of standard human interference. And LicPro.exe just takes the License Key provided and then processes it and sends it back to LicMan.exe for display. This way the app would not hang and you have a happy customer :-)

Lots of bad ways of doing IPC:

  • Shared Memory: Difficult to manage and set-up.
  • Shared Files / Registry: Very slow doe to the writing & reading to/from disk. Difficult to manage as well.
  • SendMessage / PostMessage: Locks up the UI thread while the message is processed. Messages are limited to integer values. Can't communicate from a non-admin process to a admin process. Assumes that your process has a window. 

Named Pipes:

Inter Process Communication using named pipes is what Google Chrome uses. So, let me teach you about Inter Process Communication using Named Pipes

What do you put in the pipes?

Virtually you can push any data through pipes and transfer it to and fro your apps. Now the question arises that what you should transfer. The rule of thumb says that, "Keep it short and simple." You can use command codes and/or response codes.

Coding time: The code which I have included is self-explanatory and is very well commented, so have a look at it.

So, that's it from Chris Harrison for Ultimate Programming Tutorials; catch me next week when I write about Make your .NET app shine with professionalism.

Code:

PipeClient :

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Win32.SafeHandles;

namespace VisualTech.DevTools
{
    /// 
    /// Allow pipe communication between a server and a client
    /// 
    public class PipeClient
    {
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern SafeFileHandle CreateFile(
           String pipeName,
           uint dwDesiredAccess,
           uint dwShareMode,
           IntPtr lpSecurityAttributes,
           uint dwCreationDisposition,
           uint dwFlagsAndAttributes,
           IntPtr hTemplate);

        /// 
        /// Handles messages received from a server pipe
        /// 
        /// The byte message received
        public delegate void MessageReceivedHandler(byte[] message);

        /// 
        /// Event is called whenever a message is received from the server pipe
        /// 
        public event MessageReceivedHandler MessageReceived;


        /// 
        /// Handles server disconnected messages
        /// 
        public delegate void ServerDisconnectedHandler();

        /// 
        /// Event is called when the server pipe is severed.
        /// 
        public event ServerDisconnectedHandler ServerDisconnected;

        const int BUFFER_SIZE = 4096;

        FileStream stream;
        SafeFileHandle handle;
        Thread readThread;

        /// 
        /// Is this client connected to a server pipe
        /// 
        public bool Connected { get; private set; }

        /// 
        /// The pipe this client is connected to
        /// 
        public string PipeName { get; private set; }

        /// 
        /// Connects to the server with a pipename.
        /// 
        /// The name of the pipe to connect to.
        public void Connect(string pipename)
        {
            if (Connected)
                throw new Exception("Already connected to pipe server.");

            PipeName = pipename;

            handle =
               CreateFile(
                  PipeName,
                  0xC0000000, // GENERIC_READ | GENERIC_WRITE = 0x80000000 | 0x40000000
                  0,
                  IntPtr.Zero,
                  3, // OPEN_EXISTING
                  0x40000000, // FILE_FLAG_OVERLAPPED
                  IntPtr.Zero);

            //could not create handle - server probably not running
            if (handle.IsInvalid)
                return;

            Connected = true;

            //start listening for messages
            readThread = new Thread(Read)
                             {
                                 IsBackground = true
                             };
            readThread.Start();
        }

        /// 
        /// Disconnects from the server.
        /// 
        public void Disconnect()
        {
            if (!Connected)
                return;

            // we're no longer connected to the server
            Connected = false;
            PipeName = null;

            //clean up resource
            if (stream != null)
                stream.Close();
            handle.Close();

            stream = null;
            handle = null;
        }

        void Read()
        {
            stream = new FileStream(handle, FileAccess.ReadWrite, BUFFER_SIZE, true);
            byte[] readBuffer = new byte[BUFFER_SIZE];

            while (true)
            {
                int bytesRead = 0;

                using (MemoryStream ms = new MemoryStream())
                {
                    try
                    {
                        // read the total stream length
                        int totalSize = stream.Read(readBuffer, 0, 4);

                        // client has disconnected
                        if (totalSize == 0)
                            break;

                        totalSize = BitConverter.ToInt32(readBuffer, 0);

                        do
                        {
                            int numBytes = stream.Read(readBuffer, 0, Math.Min(totalSize - bytesRead, BUFFER_SIZE));

                            ms.Write(readBuffer, 0, numBytes);

                            bytesRead += numBytes;

                        } while (bytesRead < totalSize);

                    }
                    catch
                    {
                        //read error has occurred
                        break;
                    }

                    //client has disconnected
                    if (bytesRead == 0)
                        break;

                    //fire message received event
                    if (MessageReceived != null)
                        MessageReceived(ms.ToArray());
                }
            }

            // if connected, then the disconnection was
            // caused by a server terminating, otherwise it was from
            // a call to Disconnect()
            if (Connected)
            {
                //clean up resource
                stream.Close();
                handle.Close();

                stream = null;
                handle = null;

                // we're no longer connected to the server
                Connected = false;
                PipeName = null;

                if (ServerDisconnected != null)
                    ServerDisconnected();
            }
        }

        /// 
        /// Sends a message to the server.
        /// 
        /// The message to send.
        /// True if the message is sent successfully - false otherwise.
        public bool SendMessage(byte[] message)
        {
            try
            {
                // write the entire stream length
                stream.Write(BitConverter.GetBytes(message.Length), 0, 4);

                stream.Write(message, 0, message.Length);
                stream.Flush();
                return true;
            }
            catch
            {
                return false;
            }
        }
    }
}

PipeServer :
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Win32.SafeHandles;

namespace VisualTech.DevTools
{
    /// 
    /// Allow pipe communication between a server and a client
    /// 
    public class PipeServer
    {
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern SafeFileHandle CreateNamedPipe(
           String pipeName,
           uint dwOpenMode,
           uint dwPipeMode,
           uint nMaxInstances,
           uint nOutBufferSize,
           uint nInBufferSize,
           uint nDefaultTimeOut,
           IntPtr lpSecurityAttributes);

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern int ConnectNamedPipe(
           SafeFileHandle hNamedPipe,
           IntPtr lpOverlapped);

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool DisconnectNamedPipe(SafeFileHandle hHandle);

        [StructLayoutAttribute(LayoutKind.Sequential)]
        struct SECURITY_DESCRIPTOR
        {
            public byte revision;
            public byte size;
            public short control;
            public IntPtr owner;
            public IntPtr group;
            public IntPtr sacl;
            public IntPtr dacl;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct SECURITY_ATTRIBUTES
        {
            public int nLength;
            public IntPtr lpSecurityDescriptor;
            public int bInheritHandle;
        }

        private const uint SECURITY_DESCRIPTOR_REVISION = 1;

        [DllImport("advapi32.dll", SetLastError = true)]
        static extern bool InitializeSecurityDescriptor(ref SECURITY_DESCRIPTOR sd, uint dwRevision);
        
        [DllImport("advapi32.dll", SetLastError = true)]
        static extern bool SetSecurityDescriptorDacl(ref SECURITY_DESCRIPTOR sd, bool daclPresent, IntPtr dacl, bool daclDefaulted);

        public class Client
        {
            public SafeFileHandle handle;
            public FileStream stream;
        }

        /// 
        /// Handles messages received from a client pipe
        /// 
        /// The byte message received
        public delegate void MessageReceivedHandler(byte[] message);

        /// 
        /// Event is called whenever a message is received from a client pipe
        /// 
        public event MessageReceivedHandler MessageReceived;


        /// 
        /// Handles client disconnected messages
        /// 
        public delegate void ClientDisconnectedHandler();

        /// 
        /// Event is called when a client pipe is severed.
        /// 
        public event ClientDisconnectedHandler ClientDisconnected;

        const int BUFFER_SIZE = 4096;

        Thread listenThread;
        readonly List clients = new List();

        /// 
        /// The total number of PipeClients connected to this server
        /// 
        public int TotalConnectedClients
        {
            get
            {
                lock (clients)
                {
                    return clients.Count;
                }
            }
        }

        /// 
        /// The name of the pipe this server is connected to
        /// 
        public string PipeName { get; private set; }

        /// 
        /// Is the server currently running
        /// 
        public bool Running { get; private set; }


        /// 
        /// Starts the pipe server on a particular name.
        /// 
        /// The name of the pipe.
        public void Start(string pipename)
        {
            PipeName = pipename;

            //start the listening thread
            listenThread = new Thread(ListenForClients)
                               {
                                   IsBackground = true
                               };

            listenThread.Start();

            Running = true;
        }

        void ListenForClients()
        {
            SECURITY_DESCRIPTOR sd = new SECURITY_DESCRIPTOR();

            // set the Security Descriptor to be completely permissive
            InitializeSecurityDescriptor(ref sd, SECURITY_DESCRIPTOR_REVISION);
            SetSecurityDescriptorDacl(ref sd, true, IntPtr.Zero, false);

            IntPtr ptrSD = Marshal.AllocCoTaskMem(Marshal.SizeOf(sd));
            Marshal.StructureToPtr(sd, ptrSD, false);

            SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES
                                         {
                                             nLength = Marshal.SizeOf(sd),
                                             lpSecurityDescriptor = ptrSD,
                                             bInheritHandle = 1
                                         };

            IntPtr ptrSA = Marshal.AllocCoTaskMem(Marshal.SizeOf(sa));
            Marshal.StructureToPtr(sa, ptrSA, false);


            while (true)
            {
                // Creates an instance of a named pipe for one client
                SafeFileHandle clientHandle =
                    CreateNamedPipe(
                        PipeName,

                        // DUPLEX | FILE_FLAG_OVERLAPPED = 0x00000003 | 0x40000000;
                        0x40000003,
                        0,
                        255,
                        BUFFER_SIZE,
                        BUFFER_SIZE,
                        0,
                        ptrSA);

                //could not create named pipe instance
                if (clientHandle.IsInvalid)
                    continue;

                int success = ConnectNamedPipe(clientHandle, IntPtr.Zero);

                //could not connect client
                if (success == 0)
                {
                    // close the handle, and wait for the next client
                    clientHandle.Close();
                    continue;
                }

                Client client = new Client
                                    {
                                        handle = clientHandle
                                    };

                lock (clients)
                    clients.Add(client);

                Thread readThread = new Thread(Read)
                                        {
                                            IsBackground = true
                                        };
                readThread.Start(client);
            }

            // free up the ptrs (never reached due to infinite loop)
            Marshal.FreeCoTaskMem(ptrSD);
            Marshal.FreeCoTaskMem(ptrSA);
        }

        void Read(object clientObj)
        {
            Client client = (Client)clientObj;
            client.stream = new FileStream(client.handle, FileAccess.ReadWrite, BUFFER_SIZE, true);
            byte[] buffer = new byte[BUFFER_SIZE];

            while (true)
            {
                int bytesRead = 0;

                using (MemoryStream ms = new MemoryStream())
                {
                    try
                    {
                        // read the total stream length
                        int totalSize = client.stream.Read(buffer, 0, 4);

                        // client has disconnected
                        if (totalSize == 0)
                            break;

                        totalSize = BitConverter.ToInt32(buffer, 0);

                        do
                        {
                            int numBytes = client.stream.Read(buffer, 0, Math.Min(totalSize - bytesRead, BUFFER_SIZE));

                            ms.Write(buffer, 0, numBytes);

                            bytesRead += numBytes;

                        } while (bytesRead < totalSize);

                    }
                    catch
                    {
                        //read error has occurred
                        break;
                    }

                    //client has disconnected
                    if (bytesRead == 0)
                        break;

                    //fire message received event
                    if (MessageReceived != null)
                        MessageReceived(ms.ToArray());
                }
            }

            // the clients must be locked - otherwise "stream.Close()"
            // could be called while SendMessage(byte[]) is being called on another thread.
            // This leads to an IO error & several wasted days.
            lock (clients)
            {
                //clean up resources
                DisconnectNamedPipe(client.handle);
                client.stream.Close();
                client.handle.Close();

                clients.Remove(client);
            }

            // invoke the event, a client disconnected
            if (ClientDisconnected != null)
                ClientDisconnected();
        }

        /// 
        /// Sends a message to all connected clients.
        /// 
        /// The message to send.
        public void SendMessage(byte[] message)
        {
            lock (clients)
            {
                //get the entire stream length
                byte[] messageLength = BitConverter.GetBytes(message.Length);

                foreach (Client client in clients)
                {
                    // length
                    client.stream.Write(messageLength, 0, 4);

                    // data
                    client.stream.Write(message, 0, message.Length);
                    client.stream.Flush();
                }
            }
        }
    }
}

Download Source Code:
                 PipeServer.zip

1 comments:

Post a Comment

Note: Only a member of this blog may post a comment.