yotiky Tech Blog

とあるエンジニアの備忘録

.NET - NamedPipe(名前付きパイプ)を使ったプロセス間通信

今回は名前付きパイプの実装例です。

サンプルは1つ目の Console アプリで入力した文字列を、2つ目の Console アプリ、WPF アプリ、Unity でそれぞれ読み取って表示するものになります。 パイプはサーバー側で同じパイプ名を共有するサーバーインスタンスの最大数を指定できますが、既定値のままなので1対1での通信を想定しています。

f:id:yotiky:20200807203006g:plain

目次

概要

パイプの説明を引用しておきます。導入されたのは .NET Framework 3.5 からのようです。

パイプは、プロセス間通信の手段となります。 パイプには、2 種類あります。

  • 匿名パイプ。
    匿名パイプは、ローカル コンピューターでのプロセス間通信を実現します。 匿名パイプは、名前付きパイプより必要なオーバーヘッドは少ないですが、提供するサービスは限られています。 匿名パイプは一方向であり、ネットワーク経由で使用することはできません。 これでは、1 つのサーバー インスタンスのみをサポートしています。 匿名パイプは、スレッド間、またはパイプ ハンドルを作成時に子プロセスに簡単に渡すことができる親と子のプロセス間の通信で便利です。
    .NET では、AnonymousPipeServerStream クラスと AnonymousPipeClientStream クラスを使用して匿名パイプを実装します。

  • 名前付きパイプ。
    名前付きパイプは、パイプ サーバーと 1 つ以上のパイプ クライアントとの間でのプロセス間通信を提供します。 名前付きパイプは、一方向であることも、双方向であることも可能です。 これでは、メッセージ ベースの通信がサポートされ、同じパイプ名を使用して複数のクライアントが同時にサーバー プロセスに接続することができます。 名前付きパイプでは、プロセスを接続してリモート サーバーで独自のアクセス許可を使用する、偽装もサポートしています。
    .NET では、NamedPipeServerStream クラスと NamedPipeClientStream クラスを使用して名前付きパイプを実装します。

docs.microsoft.com

実装例

Console

送信側の Console アプリのサンプルコードです。

static void Main(string[] args)
{
    Console.WriteLine("Please enter a message, and then press Enter.");

    Task.Run(async () =>
    {
        try
        {
            using (var stream = new NamedPipeServerStream("NamedPipe"))
            {
                await stream.WaitForConnectionAsync();

                using (var writer = new StreamWriter(stream))
                {
                    writer.AutoFlush = true;
                    while (true)
                    {
                        var data = Console.ReadLine();
                        await writer.WriteLineAsync(data);
                    }
                }
            }
        }
        catch (IOException)
        {
            Console.WriteLine("Connection is closed.");
        }
    }).Wait();

    Console.WriteLine("End of sample.");
    Console.ReadLine();
}

StreamWriter は開きっぱなしなので Flush をしてあげないと書き込んだ値が流れません。見落とさないように気をつけましょう。

1度書き込むだけの実装例だとusingを抜けることで流れるようになっているので Flush がないことが多いです。

NamedPipeServerStreamIsConnected を持っていますが、クライアントが切断されても true のままでした。状態を確認する方法は載っておらず、IOException が閉じた合図として扱われています。(ひどい)

余談ですが、Task を Wait (同期呼び出し)した場合、キャッチされなかった例外は AggregateException でラップされてスローされます。 Task 全体を try-catch で囲んで AggregateException の中身を調べるか、Task.Run と Wait の間に ContinueWIth を挟んで例外を調べましょう。 docs.microsoft.com

続いて受信側の Console アプリです。

static void Main(string[] args)
{
    Task.Run(async () =>
    {
        using (var stream = new NamedPipeClientStream("NamedPipe"))
        {
            await stream.ConnectAsync();

            using (var reader = new StreamReader(stream))
            {
                while (stream.IsConnected)
                {
                    var str = await reader.ReadLineAsync();
                    Console.WriteLine("Data :" + str);
                    Thread.Sleep(100);
                }
            }
        }
    }).Wait();
}

ポーリング処理は、IsConnected でループしているので接続が切れると抜けるようになっています。(正しく false になる) ReadLineAsync は、Stream にデータが流れてくるまで await されます。この時に切断されると null が返ってくるため、次のループに処理が移り、終了となります。

WPF

受信側の WPF アプリのサンプルコードです。 Load のイベントで処理して、XAML で追加した textBlock に表示しています。

private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    await Task.Run(async () =>
    {
        using (var stream = new NamedPipeClientStream("NamedPipe"))
        {
            await stream.ConnectAsync();

            using (var reader = new StreamReader(stream))
            {
                while (stream.IsConnected)
                {
                    var str = await reader.ReadLineAsync();
                    Dispatcher.Invoke(() => textBlock.Text += "Data :" + str + "\r\n");
                    Thread.Sleep(100);
                }
            }
        }
    });
    textBlock.Text += "End of sample.";
}

2つ目の Console 同様に、IsConnected でループしているので切断された際に抜けるようになります。

Unity

最後は受信側の Unity アプリのサンプルコードです。 (動作確認は Editor でのみ)

CancellationTokenSource tokenSource = new CancellationTokenSource();

async void Start()
{
    await Task.Run(async () =>
    {
        using (var stream = new NamedPipeClientStream("NamedPipe"))
        {
            // ConnectAsync : The method or operation is not implemented.
            stream.Connect();

            using (var reader = new StreamReader(stream))
            {
                while (true)
                {
                    var str = await reader.ReadLineAsync();
                    if (str == null)
                    {
                        break;
                    }
                    Debug.Log("Data :" + str);
                    Thread.Sleep(100);

                    if(tokenSource.Token.IsCancellationRequested)
                    {
                        break;
                    }
                }
            }
        }
        Debug.Log("End of sample.");
    }, 
    tokenSource.Token);

}

void OnDestroy()
{
    tokenSource.Cancel();
    Debug.Log("task cancelled.");
}

Unity の場合は注意点がいくつかあります。

まず ConnecAsync が実装されていません。Connect を使うため、サーバー側が先に待機していないと例外が発生します。

次に接続が切れても Stream の IsConnectedfalse になりません。ReadLineAsyncnull が返ってくるところまでは他と同じためそれで抜けるようにしてあります。

3つ目は、何もケアしないと Editor 実行を終了してもタスクが破棄されません。通信がつながった状態となり、コンソールにログが出力され続けます。 そこで、OnDestroy で明示的にタスクをキャンセルする処理を入れてあります。

サンプルプロジェクト

ソースコード一式は以下においてあります。

github.com

参考

以下のサイトを参考にさせていただきました。ありがとうございます。