今回は名前付きパイプの実装例です。
サンプルは1つ目の Console アプリで入力した文字列を、2つ目の Console アプリ、WPF アプリ、Unity でそれぞれ読み取って表示するものになります。 パイプはサーバー側で同じパイプ名を共有するサーバーインスタンスの最大数を指定できますが、既定値のままなので1対1での通信を想定しています。
目次
概要
パイプの説明を引用しておきます。導入されたのは .NET Framework 3.5 からのようです。
パイプは、プロセス間通信の手段となります。 パイプには、2 種類あります。
匿名パイプ。
匿名パイプは、ローカル コンピューターでのプロセス間通信を実現します。 匿名パイプは、名前付きパイプより必要なオーバーヘッドは少ないですが、提供するサービスは限られています。 匿名パイプは一方向であり、ネットワーク経由で使用することはできません。 これでは、1 つのサーバー インスタンスのみをサポートしています。 匿名パイプは、スレッド間、またはパイプ ハンドルを作成時に子プロセスに簡単に渡すことができる親と子のプロセス間の通信で便利です。
.NET では、AnonymousPipeServerStream クラスと AnonymousPipeClientStream クラスを使用して匿名パイプを実装します。名前付きパイプ。
名前付きパイプは、パイプ サーバーと 1 つ以上のパイプ クライアントとの間でのプロセス間通信を提供します。 名前付きパイプは、一方向であることも、双方向であることも可能です。 これでは、メッセージ ベースの通信がサポートされ、同じパイプ名を使用して複数のクライアントが同時にサーバー プロセスに接続することができます。 名前付きパイプでは、プロセスを接続してリモート サーバーで独自のアクセス許可を使用する、偽装もサポートしています。
.NET では、NamedPipeServerStream クラスと NamedPipeClientStream クラスを使用して名前付きパイプを実装します。
実装例
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 がないことが多いです。
NamedPipeServerStream
は IsConnected
を持っていますが、クライアントが切断されても true
のままでした。状態を確認する方法は載っておらず、IOException
が閉じた合図として扱われています。(ひどい)
続いて受信側の 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 の IsConnected
が false
になりません。ReadLineAsync
で null
が返ってくるところまでは他と同じためそれで抜けるようにしてあります。
3つ目は、何もケアしないと Editor 実行を終了してもタスクが破棄されません。通信がつながった状態となり、コンソールにログが出力され続けます。
そこで、OnDestroy
で明示的にタスクをキャンセルする処理を入れてあります。
サンプルプロジェクト
ソースコード一式は以下においてあります。
参考
以下のサイトを参考にさせていただきました。ありがとうございます。
- named-pipe カテゴリーの記事一覧 - いちろぐ
- プロセス | C# プログラミング解説
- sh-akira/UnityNamedPipe: 名前付きパイプでUnityを外部アプリからコントロール
- MITライセンスのライブラリなので試してみるのも一手