概要
- .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 からのメッセージを待つ。
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;
}
}
}
入力と表示をするアプリケーション
- 起動時に Mutex を取得できたかで、Server もしくは Client に切り替わり
- サーバーは、メッセージの取得と、ProcessReceivedMessage での処理。
- NamedPipeServer.ReceiveMessageAsync(message => ProcessReceivedMessage(message)));
- クライアントは、 NamedPipeClient.SendMessageAsync(message) しているだけです。
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 がお手軽で良い。