24

I am trying to achieve a full-duplex client-server communication scheme, on 2 different machines (only), where each end-point (client or server) can send stuff at any time, asynchronously (non-blocking pipe), and the other end will pick it up and read it.

(Please don’t answer by referring to other technologies besides named pipes; I need an answer using this particular approach.)

I've read that Named Pipes have to be one-direction only or they lock up, but I'm guessing that's probably wrong. I think pipes are socket-based, and I can't imagine the underlying socket would be one-way only.

Any answers to this question need to address these issues for it to be really useful:

  1. The answers need to address asynchronous pipes, I can't use a synchronous solution.
  2. the answers need to demonstrate or allow for the fact the pipes stay OPEN. I'm tired of reading examples where the pipe is opened, a string is transmitted, then the pipe is immediately closed. I'd like an answer that assumes the pipes stay open and transmit a LOT of junk, at random times, and keep repeating. with no hangs.
  3. C#-based solution

I'm sorry to sound demanding and snotty, but after days of scouring the internet I still haven't found a good example, and I don't want to use WCF. If you know the details of this answer and answer it well, this topic will be a real winner for ages to come, I'm sure. I will post the answer myself if I figure it out.

If you're about to write and say "You need to use two pipes", please explain why, and how you know this is true, because nothing I've read about this explains why this is the case.

thanks!

Glenn Slayden
  • 17,543
  • 3
  • 114
  • 108
eric frazer
  • 1,492
  • 1
  • 12
  • 19
  • 1
    Why dont you want to use WCF with named pipes binding? Have you seen solutions like that http://stackoverflow.com/questions/16432813/async-two-way-communication-with-windows-named-pipes-net – admax Dec 27 '15 at 07:58
  • Hey, why so angry, I just asked **why** you dont want wcf. And it was a comment, not a suggested solution. If you aware of some drawbacks of wcf please share, it might be useful to the community. – admax Dec 28 '15 at 10:23
  • 5
    Not angry, just specific. The reason is that I try and steer away from technologies that are considered "deprecated", even if they're still in use, especially if they don't add a lot of value-add for the buck. WCF doesn't add a lot on top of Named Pipes + JSON, thus I'd rather not use it. I like to stay "minimal". I'm only transferring a few bytes back and forth, to keep a distributed system running, all I need is a named pipe! – eric frazer Dec 29 '15 at 16:57
  • 5
    by the way, I answered my own question, and it's useful and clear. Can you stop down-voting the original, please? – eric frazer Dec 29 '15 at 16:58

3 Answers3

23

You don't have to use two pipes. I found a LOT of answers on the net that state you need to use two pipes. I dug around, stayed up all night, tried and tried again, and figured out how to do it, it's super simple, but you gotta get everything right (especially get things in the right calling order), or it just won't work. Another trick is to always ensure you have a read call outstanding, or it will lock up too. Don't write before you know somebody is reading. Don't start a read call unless you have the event set up first. That kind of thing.

here's the pipe class I'm using. It's probably not robust enough to deal with pipe errors, closures, and overflows.

Okay I have no idea what's wrong here, but the formatting is slightly off! vvvv

namespace Squall
{
    public interface PipeSender
    {
        Task SendCommandAsync(PipeCommandPlusString pCmd);
    }

    /******************************************************************************
     * 
     * 
     * 
     * 
     ******************************************************************************/
    public class ClientPipe : BasicPipe
    {
        NamedPipeClientStream m_pPipe;

        public ClientPipe(string szServerName, string szPipeName)
            : base("Client")
        {
            m_szPipeName = szPipeName; // debugging
            m_pPipe = new NamedPipeClientStream(szServerName, szPipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
            base.SetPipeStream(m_pPipe); // inform base class what to read/write from
        }

        public void Connect()
        {
            Debug.WriteLine("Pipe " + FullPipeNameDebug() + " connecting to server");
            m_pPipe.Connect(); // doesn't seem to be an async method for this routine. just a timeout.
            StartReadingAsync();
        }

        // the client's pipe index is always 0
        internal override int PipeId() { return 0; }
    }

    /******************************************************************************
     * 
     * 
     * 
     * 
     ******************************************************************************/
    public class ServerPipe : BasicPipe
    {
        public event EventHandler<EventArgs> GotConnectionEvent;

        NamedPipeServerStream m_pPipe;
        int m_nPipeId;

        public ServerPipe(string szPipeName, int nPipeId)
            : base("Server")
        {
            m_szPipeName = szPipeName;
            m_nPipeId = nPipeId;
            m_pPipe = new NamedPipeServerStream(
                szPipeName,
                PipeDirection.InOut,
                NamedPipeServerStream.MaxAllowedServerInstances,
                PipeTransmissionMode.Message,
                PipeOptions.Asynchronous);
            base.SetPipeStream(m_pPipe);
            m_pPipe.BeginWaitForConnection(new AsyncCallback(StaticGotPipeConnection), this);
        }

        static void StaticGotPipeConnection(IAsyncResult pAsyncResult)
        {
            ServerPipe pThis = pAsyncResult.AsyncState as ServerPipe;
            pThis.GotPipeConnection(pAsyncResult);
        }

        void GotPipeConnection(IAsyncResult pAsyncResult)
        {
            m_pPipe.EndWaitForConnection(pAsyncResult);

            Debug.WriteLine("Server Pipe " + m_szPipeName + " got a connection");

            if (GotConnectionEvent != null)
            {
                GotConnectionEvent(this, new EventArgs());
            }

            // lodge the first read request to get us going
            //
            StartReadingAsync();
        }

        internal override int PipeId() { return m_nPipeId; }
    }

    /******************************************************************************
     * 
     * 
     * 
     * 
     ******************************************************************************/

    public abstract class BasicPipe : PipeSender
    {
        public static int MaxLen = 1024 * 1024; // why not
        protected string m_szPipeName;
        protected string m_szDebugPipeName;

        public event EventHandler<PipeEventArgs> ReadDataEvent;
        public event EventHandler<EventArgs> PipeClosedEvent;

        protected byte[] m_pPipeBuffer = new byte[BasicPipe.MaxLen];

        PipeStream m_pPipeStream;

        public BasicPipe(string szDebugPipeName)
        {
            m_szDebugPipeName = szDebugPipeName;
        }

        protected void SetPipeStream(PipeStream p)
        {
            m_pPipeStream = p;
        }

        protected string FullPipeNameDebug()
        {
            return m_szDebugPipeName + "-" + m_szPipeName;
        }

        internal abstract int PipeId();

        public void Close()
        {
            m_pPipeStream.WaitForPipeDrain();
            m_pPipeStream.Close();
            m_pPipeStream.Dispose();
            m_pPipeStream = null;
        }

        // called when Server pipe gets a connection, or when Client pipe is created
        public void StartReadingAsync()
        {
            Debug.WriteLine("Pipe " + FullPipeNameDebug() + " calling ReadAsync");

            // okay we're connected, now immediately listen for incoming buffers
            //
            byte[] pBuffer = new byte[MaxLen];
            m_pPipeStream.ReadAsync(pBuffer, 0, MaxLen).ContinueWith(t =>
            {
                Debug.WriteLine("Pipe " + FullPipeNameDebug() + " finished a read request");

                int ReadLen = t.Result;
                if (ReadLen == 0)
                {
                    Debug.WriteLine("Got a null read length, remote pipe was closed");
                    if (PipeClosedEvent != null)
                    {
                        PipeClosedEvent(this, new EventArgs());
                    }
                    return;
                }

                if (ReadDataEvent != null)
                {
                    ReadDataEvent(this, new PipeEventArgs(pBuffer, ReadLen));
                }
                else
                {
                    Debug.Assert(false, "something happened");
                }

                // lodge ANOTHER read request
                //
                StartReadingAsync();

            });
        }

        protected Task WriteByteArray(byte[] pBytes)
        {
            // this will start writing, but does it copy the memory before returning?
            return m_pPipeStream.WriteAsync(pBytes, 0, pBytes.Length);
        }

        public Task SendCommandAsync(PipeCommandPlusString pCmd)
        {
            Debug.WriteLine("Pipe " + FullPipeNameDebug() + ", writing " + pCmd.GetCommand() + "-" + pCmd.GetTransmittedString());
            string szSerializedCmd = JsonConvert.SerializeObject(pCmd);
            byte[] pSerializedCmd = Misc.StringToBytes(szSerializedCmd);
            Task t = WriteByteArray(pSerializedCmd);
            return t;
        }
    }

    /******************************************************************************
     * 
     * 
     * 
     * 
     ******************************************************************************/

    public class PipeEventArgs
    {
        public byte[] m_pData;
        public int m_nDataLen;

        public PipeEventArgs(byte[] pData, int nDataLen)
        {
            // is this a copy, or an alias copy? I can't remember right now.
            m_pData = pData;
            m_nDataLen = nDataLen;
        }
    }

    /******************************************************************************
     * if we're just going to send a string back and forth, then we can use this
     * class. It it allows us to get the bytes as a string. sort of silly.
     ******************************************************************************/

    [Serializable]
    public class PipeCommandPlusString
    {
        public string m_szCommand;  // must be public to be serialized
        public string m_szString;   // ditto

        public PipeCommandPlusString(string sz, string szString)
        {
            m_szCommand = sz;
            m_szString = szString;
        }

        public string GetCommand()
        {
            return m_szCommand;
        }

        public string GetTransmittedString()
        {
            return m_szString;
        }
    }
}

and here is my pipe test, running on one process. It runs on two processes too, I checked

namespace NamedPipeTest
{
    public partial class Form1 : Form
    {
        SynchronizationContext _context;
        Thread m_pThread = null;
        volatile bool m_bDieThreadDie;
        ServerPipe m_pServerPipe;
        ClientPipe m_pClientPipe;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            _context = SynchronizationContext.Current;

            m_pServerPipe = new ServerPipe("SQUALL_PIPE", 0);
            m_pServerPipe.ReadDataEvent += M_pServerPipe_ReadDataEvent;
            m_pServerPipe.PipeClosedEvent += M_pServerPipe_PipeClosedEvent;

            // m_pThread = new Thread(StaticThreadProc);
            // m_pThread.Start( this );
        }

        private void M_pServerPipe_PipeClosedEvent(object sender, EventArgs e)
        {
            Debug.WriteLine("Server: Pipe was closed, shutting down");

            // have to post this on the main thread
            _context.Post(delegate
            {
                Close();
            }, null);
        }

        private void M_pServerPipe_ReadDataEvent(object sender, PipeEventArgs e)
        {
            // this gets called on an anonymous thread

            byte[] pBytes = e.m_pData;
            string szBytes = Misc.BytesToString(pBytes, e.m_pData.Length);
            PipeCommandPlusString pCmd = JsonConvert.DeserializeObject<PipeCommandPlusString>(szBytes);
            string szValue = pCmd.GetTransmittedString();

            if (szValue == "CONNECT")
            {
                Debug.WriteLine("Got command from client: " + pCmd.GetCommand() + "-" + pCmd.GetTransmittedString() + ", writing command back to client");
                PipeCommandPlusString pCmdToSend = new PipeCommandPlusString("SERVER", "CONNECTED");
                // fire off an async write
                Task t = m_pServerPipe.SendCommandAsync(pCmdToSend);
            }
        }

        static void StaticThreadProc(Object o)
        {
            Form1 pThis = o as Form1;
            pThis.ThreadProc();
        }

        void ThreadProc()
        {
            m_pClientPipe = new ClientPipe(".", "SQUALL_PIPE");
            m_pClientPipe.ReadDataEvent += PClientPipe_ReadDataEvent;
            m_pClientPipe.PipeClosedEvent += M_pClientPipe_PipeClosedEvent;
            m_pClientPipe.Connect();

            PipeCommandPlusString pCmd = new PipeCommandPlusString("CLIENT", "CONNECT");
            int Counter = 1;
            while (Counter++ < 10)
            {
                Debug.WriteLine("Counter = " + Counter);
                m_pClientPipe.SendCommandAsync(pCmd);
                Thread.Sleep(3000);
            }

            while (!m_bDieThreadDie)
            {
                Thread.Sleep(1000);
            }

            m_pClientPipe.ReadDataEvent -= PClientPipe_ReadDataEvent;
            m_pClientPipe.PipeClosedEvent -= M_pClientPipe_PipeClosedEvent;
            m_pClientPipe.Close();
            m_pClientPipe = null;
        }

        private void M_pClientPipe_PipeClosedEvent(object sender, EventArgs e)
        {
            // wait around for server to shut us down
        }

        private void PClientPipe_ReadDataEvent(object sender, PipeEventArgs e)
        {
            byte[] pBytes = e.m_pData;
            string szBytes = Misc.BytesToString(pBytes, e.m_nDataLen);
            PipeCommandPlusString pCmd = JsonConvert.DeserializeObject<PipeCommandPlusString>(szBytes);
            string szValue = pCmd.GetTransmittedString();

            Debug.WriteLine("Got command from server: " + pCmd.GetCommand() + "-" + pCmd.GetTransmittedString());

            if (szValue == "CONNECTED")
            {
                PipeCommandPlusString pCmdToSend = new PipeCommandPlusString("CLIENT", "DATA");
                m_pClientPipe.SendCommandAsync(pCmdToSend);
            }
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (m_pThread != null)
            {
                m_bDieThreadDie = true;
                m_pThread.Join();
                m_bDieThreadDie = false;
            }

            m_pServerPipe.ReadDataEvent -= M_pServerPipe_ReadDataEvent;
            m_pServerPipe.PipeClosedEvent -= M_pServerPipe_PipeClosedEvent;
            m_pServerPipe.Close();
            m_pServerPipe = null;

        }
    }
}
ctacke
  • 66,480
  • 18
  • 94
  • 155
eric frazer
  • 1,492
  • 1
  • 12
  • 19
3

Just create the pipe as overlapped and your code can block on a read in one thread while writing to the pipe from another.

    void StartServer()
    {
        Task.Factory.StartNew(() =>
        {
            var server = new NamedPipeServerStream("PipesOfPiece", PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
            server.WaitForConnection();
            reader = new StreamReader(server);
            writer = new StreamWriter(server);
        });
    }

    private async void timer1_Tick(object sender, EventArgs e)
    {
        timer1.Stop();
        if (null != reader)
        {
            char[] buf = new char[50];

            int count = await reader.ReadAsync(buf, 0, 50);

            if (0 < count)
            {
                m_textBox_from.Text = new string(buf, 0, count);
            }
        }
        timer1.Start();
    }
Andy Chang
  • 39
  • 3
  • 1
    On the one hand, the first sentence contains the important bit of information to the point (unlike the accepted answer), and the allocation of the `NamedPipeServerStream` demonstrates it. On the other hand, I find the rest of the code example... questionable... *crooked +1* – dialer Jul 01 '21 at 14:15
0

I think When you async-communication, you have to use two pipe.

One is recv pipe another is send pipe

Because, You don't know when you recv data.

When you send some data with one pipe, recv data can not write on pipe.

In contrast, you can not write send data on pipe.

therefore you have to two pipe for async communication.

lucidmaj7
  • 77
  • 1
  • 10
  • 1
    pipes are socket-backed. any normal unix socket and send and receive on both ends, if it's opened that way. I think a long time ago, Unix pipes were one-way only, but that's long-past. The trick to using full duplex pipes on windows is to make sure the order of how things are done, and then the pipe doesn't get confused. If you use two pipes, I'm sure it makes it simpler, but you're just wasting resources and port allocations. – eric frazer Dec 28 '15 at 01:54
  • oh..I see. I didn't know that. thank you for your knowledge.. I have to study more. – lucidmaj7 Dec 29 '15 at 02:33