LoginSignup
12

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-10-06

概要

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

きっかけ

  • 二重起動を禁止する にあるように、Mutex を用いて二重起動を防止する WPFアプリを作りました。
  • また、uap:Protocol を設定して、windows.protocol (例: hogehoge://) で、アプリを起動するようにしていました。
  • Protocol で起動すると新たにプロセスが起動しますが、すでに起動していたプロセスに、Protocol のパラメータを渡したく、IPC をしたかったのがきっかけです。
  • Handle Protocol Activation and Redirection in Packaged Apps に、やりたいことがズバリ紹介されていますが、NamedPipe でパラメータを渡す際の Serialize を JsonSerializer にしてよりシンプルにしています。

プロセス間通信(IPC)の種類

プロセス間通信 - Win32 apps | Microsoft Learn に記載されているように、IPCにはほんとにいろんな方法がありますが、今回やりたいことは、「すでに起動している同一ホスト内のプロセスに、パラメータ文字列を渡す」だけなので、NamedPipe を採用しました。

実は、最初に汎用的な gRPC を検討したのですが同一PC内のシンプルなIPCでよく、次に SendMessage で実装したのですが、SendMessage はメッセージの送り先を ウィンドウへのハンドル (HWND) で指定するため、バックグラウンドで起動中のプロセスには送れなく、結局 NamedPipe になりました。

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

検索するといろんなドキュメントが出てきますが、方法: ネットワークのプロセス間通信で名前付きパイプを使用する | Microsoft Learn がわかりやすくて良いです。

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

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

いろんな方法があるようですが、「C# を使用した JSON のシリアル化と逆シリアル化 - .NET | Microsoft Learn」に記載されているように、実装の簡単さやセキュリティ などを考慮して、JsonSerializer を採用しました。

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

サンプルアプリ

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

スクリーンショット 2022-10-06 120356.png

Server

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

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 するだけ。

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;
        }
    }
}

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

基本的なシリアル化 が参考になった。

前述の Handle Protocol Activation and Redirection in Packaged Apps にある BinaryFormatter は、今回のようなシンプルなメッセージを、少ない頻度で送信する分には面倒で難解と感じたので、セキュリティの利点も合わさって JsonSerializer を用いることにしました。

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;
        }
    }

}

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

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 がお手軽で良い。

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
12