[C#] .NET で NamedPipe を使ってプロセス間通信 (IPC) を行う (WPFサンプル)

## 概要

* .NET Core (6.0) で作られた WPFアプリにて、プロセス間通信(IPC)を行う方法。
* 同じPCで動作するアプリケーション間で通信できればよかったので、NamedPipe を使うことにしました。
* NamedPipe を使うことに至った経緯と、[サンプルアプリケーション](https://github.com/lifework/WPF_Mutex_NamedPipe)を紹介します。


## きっかけ

* [二重起動を禁止する](https://dobon.net/vb/dotnet/process/checkprevinstance.html) にあるように、Mutex を用いて二重起動を防止する WPFアプリを作りました。
* また、[uap:Protocol](https://learn.microsoft.com/ja-jp/uwp/schemas/appxpackage/uapmanifestschema/element-uap-protocol) を設定して、windows.protocol (例: hogehoge://) で、アプリを起動するようにしていました。
* Protocol で起動すると新たにプロセスが起動しますが、すでに起動していたプロセスに、Protocol のパラメータを渡したく、IPC をしたかったのがきっかけです。
* [Handle Protocol Activation and Redirection in Packaged Apps](https://blog.magnusmontin.net/2019/05/10/handle-protocol-activation-and-redirection-in-packaged-apps/) に、やりたいことがズバリ紹介されていますが、NamedPipe でパラメータを渡す際の Serialize を JsonSerializer にしてよりシンプルにしています。
 
## プロセス間通信(IPC)の種類

[プロセス間通信 - Win32 apps | Microsoft Learn](https://learn.microsoft.com/ja-jp/windows/win32/ipc/interprocess-communications) に記載されているように、IPCにはほんとにいろんな方法がありますが、今回やりたいことは、「すでに起動している同一ホスト内のプロセスに、パラメータ文字列を渡す」だけなので、NamedPipe を採用しました。

実は、最初に汎用的な gRPC を検討したのですが同一PC内のシンプルなIPCでよく、次に [SendMessage](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-sendmessage) で実装したのですが、SendMessage はメッセージの送り先を ウィンドウへのハンドル (HWND) で指定するため、バックグラウンドで起動中のプロセスには送れなく、結局 NamedPipe になりました。

## NamedPipe を用いたプロセス間通信

検索するといろんなドキュメントが出てきますが、[方法: ネットワークのプロセス間通信で名前付きパイプを使用する | Microsoft Learn](https://learn.microsoft.com/ja-jp/dotnet/standard/io/how-to-use-named-pipes-for-network-interprocess-communication) がわかりやすくて良いです。


## オブジェクトのシリアル化

文字列だけなら簡単なのですが、複雑なデータ(オブジェクト)を送ろうとした場合、シリアル化と逆シリアル化する必要があります。

いろんな方法があるようですが、「[C# を使用した JSON のシリアル化と逆シリアル化 - .NET | Microsoft Learn](https://learn.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json/overview?pivots=dotnet-6-0)」に記載されているように、実装の簡単さやセキュリティ などを考慮して、JsonSerializer を採用しました。

今回送信したいデータは、オブジェクトとはいえ非常にシンプルな class なので、JsonSerializer で、シリアル化と逆シリアル化の際の劣化や、パフォーマンス等も問題ないし、デバッグし易いので良かったです。

## サンプルアプリ

* NamedPipe を用いた、一方通行のチャットアプリ。
* ソースは、[https://github.com/lifework/WPF_Mutex_NamedPipe](https://github.com/lifework/WPF_Mutex_NamedPipe) 。
* 先に起動したプロセスが、Server となり、後に起動するClient からのメッセージを待つ。

![スクリーンショット 2022-10-06 120356.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2709043/08cae233-ab9c-9d65-5de0-bdc1ceca7fb0.png)

#### Server

NamedPipeServerStream を開いて、WaitForConnectionAsync しています。

```C#
namespace WPF_Mutex_NamedPipe.Utilities
{
    internal class NamedPipeServer : NamedPipeBase
    {
        public static async Task ReceiveMessageAsync(Action<Message?> action)
        {
            while (true)
            {
                using var stream = new NamedPipeServerStream(PipeName);
                await stream.WaitForConnectionAsync();
                using var reader = new StreamReader(stream);
                var json = await reader.ReadToEndAsync();
                action(Message.Deserialize(json));
            }
        }
    }
}
```

#### Client

NamedPipeClientStream を開いて、StreamWriter で WriteAsync するだけ。

```C#
namespace WPF_Mutex_NamedPipe.Utilities
{
    internal class NamedPipeClient : NamedPipeBase
    {
        public static async Task<bool> SendMessageAsync(Message message)
        {
            using var stream = new NamedPipeClientStream(PipeName);
            await stream.ConnectAsync();

            using var writer = new StreamWriter(stream);

            var json = message.Serialize();
            Debug.WriteLine($"SendMessageAsync: {json}");
            await writer.WriteAsync(json);

            return true;
        }
    }
}
```

#### 送信するメッセージ (オブジェクト)

[基本的なシリアル化](https://learn.microsoft.com/ja-jp/dotnet/standard/serialization/basic-serialization) が参考になった。

前述の [Handle Protocol Activation and Redirection in Packaged Apps](https://blog.magnusmontin.net/2019/05/10/handle-protocol-activation-and-redirection-in-packaged-apps/) にある BinaryFormatter は、今回のようなシンプルなメッセージを、少ない頻度で送信する分には面倒で難解と感じたので、セキュリティの利点も合わさって JsonSerializer を用いることにしました。

```C#
namespace WPF_Mutex_NamedPipe.Utilities
{
    internal class NamedPipeBase
    {
        public const string PipeName = "875aec39-8d51-d559-41cc-551c17518c72";
    }

    // .NET での JSON のシリアル化と逆シリアル化
    // https://learn.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-overview?pivots=dotnet-6-0
    public class Message
    {
        public static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) { WriteIndented = true };

        public string Sender { get; private set; }
        public string Text { get; private set; }

        public Message(string sender, string text)
        {
            Sender = sender;
            Text = text;
        }

        public string Serialize()
        {
            return JsonSerializer.Serialize<Message>(this, JsonOptions);
        }

        public static Message? Deserialize(string? serialized)
        {
            try
            {
                if (string.IsNullOrEmpty(serialized))
                {
                    return null;
                }
                return JsonSerializer.Deserialize<Message>(serialized, JsonOptions);
            }
            catch(System.Text.Json.JsonException e)
            {
                Debug.WriteLine(e);
            }
            return null;
        }
    }

}
```
#### 入力と表示をするアプリケーション

* [起動時に Mutex を取得できたかで、Server もしくは Client に切り替わり](https://github.com/lifework/WPF_Mutex_NamedPipe/blob/master/WPF_Mutex_NamedPipe/App.xaml.cs)
* サーバーは、メッセージの取得と、ProcessReceivedMessage での処理。
  * NamedPipeServer.ReceiveMessageAsync(message => ProcessReceivedMessage(message)));
* クライアントは、 NamedPipeClient.SendMessageAsync(message) しているだけです。

```C#
namespace WPF_Mutex_NamedPipe.ViewModels
{
    public class MainWindowViewModel : BindableBase, IDisposable
    {
        public IRegionManager RegionManager { get; private set; }
        public DelegateCommand<object> SendCommand { get; private set; }
        
        private CompositeDisposable Disposable { get; } = new CompositeDisposable();
        public ReactivePropertySlim<string?> Title { get; set; } = new("WPF_NamedPipe");
        public ReactivePropertySlim<string?> ReceivedMessage { get; set; } = new();
        public ReactivePropertySlim<string?> InputMessage { get; set; } = new();

        public bool IsServer => WPF_Mutex_NamedPipe.App.IsNamedPipeServer;
        public bool SendButtonEnabled => !IsServer;
        public string SenderName => $"{Environment.ProcessId}";

        public void Dispose()
        {
            Disposable.Dispose();
        }

        public MainWindowViewModel(IRegionManager regionManager)
        {
            RegionManager = regionManager;
            SendCommand = new DelegateCommand<object>(OnSendCommand);

            Title.Value = $"{(IsServer ? "Server" : "Client")} - {SenderName}";

            Disposable.Add(Title);
            Disposable.Add(ReceivedMessage);
            Disposable.Add(InputMessage);

            if (IsServer)
            {
                Task.Run(() => NamedPipeServer.ReceiveMessageAsync(message => ProcessReceivedMessage(message)));
            }
        }

        private async void OnSendCommand(object parameter)
        {
            if (string.IsNullOrEmpty(InputMessage.Value))
            {
                return;
            }

            if (await SendPipedMessage(InputMessage.Value))
            {
                InputMessage.Value = "";
            }
        }

        private async Task<bool> SendPipedMessage(string text)
        {
            var message = new Message(SenderName, text);
            return await NamedPipeClient.SendMessageAsync(message);
        }

        private bool ProcessReceivedMessage(Message? message)
        {
            if (message != null)
            {
                ReceivedMessage.Value += $"[{Now()}][{message.Sender}] {message.Text}\n";
            }
            return true;
        }

        private string Now()
        {
            return DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss");
        }
    }
}
```

## まとめ

* .NET で、同一ホスト内でさくっとプロセス間通信したい場合は、NamedPipe が良い。
* SendMessage よりも簡単だし、バックグラウンドプロセスにも送信できる。
* 簡易なオブジェクトのシリアライズは、JsonSerializer がお手軽で良い。