[C#] 名前付きパイプでプロセス間の双方向通信を行う

# パイプ関連記事 もくじ  
https://tera1707.com/entry/2022/02/06/144447#Pipe  

# やりたいこと

以前、あるプロセスと別のプロセスを連携させるために、原始的(?)にファイルにお互いから書き込み/読み込みを行って、情報をやり取りする方法を取ったのだが、自分が書き込んだあとに相手が読み込むまでのタイムラグや、同時にファイルを読み書きしようとしたらおかしくなる(書き込み途中のものを見てしまう、下手するとファイルを開く権限がないとかで落ちる)、ファイルを消したらおかしくなる、など、問題が多発した。

なにか他の方法で、プロセス間で情報をやり取りしたい。

# やったこと

そういう「プロセス間で情報をやりとりする」ことを、**IPC(Interprocess communication)**というらしい。
https://docs.microsoft.com/ja-jp/windows/uwp/communication/interprocess-communication

Microsoftのページによると、複数のIPCするための方法がある様子。

- App services
- COM
- File System
    - BroadFileSystemAccess
    - PublisherCacheFolder
    - SharedAccessStorageManager
- FullTrustProcessLauncher
- LaunchUriForResultsAsync
- Loopback
- Pipes
- Registry
- RPC
- Shared Memory

たくさんある...あとMSのページをざっと見ただけでは、私にはそれぞれがどういうやり方なのか全然わからなかった...

雰囲気的に、説明をざっと見た感じでは、「パイプ」より上は、デスクトップアプリではなくパッケージされたアプリ向けの話な気がする。(本当にパッと見ただけ。違ってたらすみません)

全部は見切れないので、今回は身近で実績のあった**「名前付きパイプ」**でまずはやってみることにした。


# 実験プログラム

ベースは下記のMSのサンプル。
下記MSサンプルでは`Thread`とか使ってたので`Task`に直して、余計なのを消した。
https://docs.microsoft.com/ja-jp/dotnet/standard/io/how-to-use-named-pipes-for-network-interprocess-communication

※一方通行のパイプ通信などもあるようだが、今回は双方向(`PipeDirection.InOut`)で試した。

:::note alert
注意
以下のサンプルプログラムは、クライアントが接続を切ったとき等、動作がうまくいかないときがあるので、別で作成した下記のものを使うこと。
https://tera1707.com/entry/2022/10/12/001814
:::

### 実験プログラムの動作イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/be9ce2f5-494f-b1d2-3052-9d9e8e1187df.png)

### 実験プログラムの説明
同じのを2つ立ち上げて、1個はパイプサーバー、もう一つをクライアントにすると、お互いに名前付きパイプで通信をすることができる。

- クライアントは、サーバーに向けて文字列を送る。
- サーバーは、それを受けてクライアントに受信した旨を返信する。

※エラー処理とかちゃんとしてないので、片方だけ落としたりするともう片方が例外で落ちます。


```パイプサンプル.cs
using System;
using System.IO;
using System.IO.Pipes;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;

namespace PipeJikken
{
    class Program
    {
        private static int numThreads = 1;

        static void Main(string[] args)
        {
            Console.WriteLine("パイプ通信の実験をします。");
            Console.WriteLine("サーバーになりますか？クライアントになりますか？");
            Console.WriteLine("サーバー：s　クライアント：c　を入力してください");
            var sorc = Console.ReadLine();

            if (sorc == "s")
            {
                CreatePipeServerTask("pipe1").Wait();
            }
            else
            {
                CreateClientTask("pipe1").Wait();
            }
        }

        public static Task CreatePipeServerTask(string pipeName)
        {
            Console.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + " " + "Please enter a message, and then press Enter.");

            return Task.Run(() =>
            {
                // numThreadsで指定した数まで同じ名前のパイプを作れる
                NamedPipeServerStream pipeServer = null;

                while (true)
                {
                    try
                    {
                        pipeServer = new NamedPipeServerStream(pipeName, PipeDirection.InOut, numThreads);

                        // クライアントの接続待ち
                        pipeServer.WaitForConnection();

                        StreamString ss = new StreamString(pipeServer);

                        while (true)
                        {
                            // 受信待ち
                            var read = ss.ReadString();
                            // 受信したら応答を送信
                            var write = ss.WriteString("Server read OK.");
                            Console.WriteLine("Read Data = " + read);

                            if (read == "end") break;
                        }
                    }
                    catch (OverflowException ofex)
                    {
                        // クライアントが切断
                        Console.WriteLine(ofex.Message);
                    }
                    finally
                    {
                        pipeServer?.Close();
                    }
                }
            });
        }

        public static Task CreateClientTask(string pipeName)
        {
            return Task.Run(() =>
            {
                while (true)
                {
                    NamedPipeClientStream pipeClient = null;

                    try
                    {
                        pipeClient = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.None, TokenImpersonationLevel.Impersonation);
                        pipeClient.Connect();
                        var ss = new StreamString(pipeClient);

                        while (true)
                        {
                            // 入力された文字列を送信する
                            var writeData = Console.ReadLine();
                            var write = ss.WriteString(writeData);
                            // 応答待ち
                            var read = ss.ReadString();
                            Console.WriteLine("Server Response = " + read);

                            if (writeData == "end") break;
                        }
                    }
                    catch (OverflowException ofex)
                    {
                        Console.WriteLine(ofex.Message);
                    }
                    catch (IOException ioe)
                    {
                        // 送信失敗
                        Console.WriteLine(ioe.Message);
                    }
                    finally
                    {
                        pipeClient?.Close();
                    }
                }
            });
        }
    }
    // MSサンプルそのまま(streamに文字列を読み書きしてくれるクラス)
    public class StreamString
    {
        private Stream ioStream;
        private UnicodeEncoding streamEncoding;

        public StreamString(Stream ioStream)
        {
            this.ioStream = ioStream;
            streamEncoding = new UnicodeEncoding();
        }

        public string ReadString()
        {
            int len = 0;

            len = ioStream.ReadByte() * 256;
            len += ioStream.ReadByte();
            byte[] inBuffer = new byte[len];
            ioStream.Read(inBuffer, 0, len);

            return streamEncoding.GetString(inBuffer);
        }

        public int WriteString(string outString)
        {
            byte[] outBuffer = streamEncoding.GetBytes(outString);
            int len = outBuffer.Length;
            if (len > UInt16.MaxValue)
            {
                len = (int)UInt16.MaxValue;
            }
            ioStream.WriteByte((byte)(len / 256));
            ioStream.WriteByte((byte)(len & 255));
            ioStream.Write(outBuffer, 0, len);
            ioStream.Flush();

            return outBuffer.Length + 2;
        }
    }
}
```

# 実験プログラム２(お互いが自分の好きなタイミングで送信を開始する)

上の実験プログラムだと、サーバーはパイプを開いて受信待ちに入り、クライアントが送信をしたら、サーバーが受信後その応答を返す、という流れなので、サーバー側から送信を開始する、ということができてなかった。

メイン関数の部分を下記のように変えると、お互いが好きなタイミングで送信できた。
(単にパイプを2つ作ってサーバーとクライアントをテレコにしただけ。両方が、サーバーでクライアントという感じ。これが良いのかどうかはわからない...)

```cs
static void Main(string[] args)
{
    Console.WriteLine("パイプ通信の実験をします。");
    Console.WriteLine("サーバーになりますか？クライアントになりますか？");
    Console.WriteLine("サーバー：s　クライアント：c　を入力してください");
    var sorc = Console.ReadLine();

    if (sorc == "s")
    {
        CreatePipeServerTask("pipe1");
        CreateClientTask("pipe2").Wait();
    }
    else
    {
        CreateClientTask("pipe1");
        CreatePipeServerTask("pipe2").Wait();
    }
}
// こっから下は実験１と同じ
```


# 匿名パイプと名前付きパイプはどう違う？

下記ページによると、名前付きパイプだと、同じコンピュータ上のプロセスだけではなく、ネットワーク上の別のコンピュータ上のプロセスとも通信できるらしい。
https://docs.microsoft.com/ja-jp/windows/win32/ipc/named-pipes

現状未検証なので、一度やってみたい。

# 参考

いろんなプロセス間通信(MSdocs)
https://docs.microsoft.com/ja-jp/windows/uwp/communication/interprocess-communication

C#名前付きパイプ(MSdocs)
ここのサンプルを元にコード作成。
https://docs.microsoft.com/ja-jp/dotnet/standard/io/how-to-use-named-pipes-for-network-interprocess-communication

名前付きパイプの説明(MSdocs)
https://docs.microsoft.com/ja-jp/windows/win32/ipc/named-pipes

C++での名前付きパイプでの通信をこちらでされてる様子。
C++でやる際には参考にさせて頂こうと思う。
http://country-programmer.dfkp.info/2021/01/named_pipe_windows_004/
また同じ方が、別のプロセス間通信のやり方である「共有メモリ」をこちらで実践されている。
これも参考になりそう...
http://country-programmer.dfkp.info/2021/02/shared_memory_001/