search
LoginSignup
8

posted at

updated at

Organization

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

概要

  • .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
What you can do with signing up
8