11
15

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-02-05

パイプ関連記事 もくじ

やりたいこと

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

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

やったこと

そういう「プロセス間で情報をやりとりする」ことを、**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)で試した。

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

実験プログラムの動作イメージ

image.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(お互いが自分の好きなタイミングで送信を開始する)

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

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

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();
    }
}
// こっから下は実験1と同じ

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

下記ページによると、名前付きパイプだと、同じコンピュータ上のプロセスだけではなく、ネットワーク上の別のコンピュータ上のプロセスとも通信できるらしい。
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/

11
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
15