C# Pipe入門:名前付きパイプでプロセス間通信を実装する方法とサンプルコード

はじめに

C#でプロセス間通信を実装したい場合、よく使われる仕組みの1つが「Pipe」です。特に名前付きパイプは、同じPC上で動作する複数のプロセス間でデータをやり取りしたいときに便利です。たとえば、常駐アプリと一時的に起動するクライアントアプリの通信、GUIアプリとバックグラウンドプロセスの連携、ローカル環境でのコマンド送受信などに利用できます。

C#では、System.IO.Pipes名前空間に用意されているNamedPipeServerStreamNamedPipeClientStreamを使うことで、名前付きパイプによる通信を比較的シンプルに実装できます。Microsoftの公式ドキュメントでも、名前付きパイプはパイプサーバーと1つ以上のパイプクライアント間のプロセス間通信に利用できる仕組みとして説明されています。Microsoft Learn

この記事では、C#のPipeの基本から、名前付きパイプサーバーとクライアントの実装方法、同期・非同期通信、よくあるエラー、実務での活用例までをサンプルコード付きで解説します。

1. C#のPipeとは?プロセス間通信で使われる仕組み

1-1. Pipeの基本概念

Pipeとは、プロセス間でデータを受け渡すための通信路です。あるプロセスがPipeへデータを書き込み、別のプロセスがそのデータを読み取ることで、プロセス同士が情報をやり取りできます。

通常、アプリケーションはプロセスごとに独立したメモリ空間を持っています。そのため、別プロセスの変数やオブジェクトに直接アクセスすることはできません。そこで、Pipeやソケット、共有メモリ、ファイルなどの仕組みを使って、プロセス間通信、つまりIPCを実現します。

C#でPipeを扱う場合は、主にSystem.IO.Pipes名前空間のクラスを使います。この名前空間には、名前付きパイプ用のNamedPipeServerStreamNamedPipeClientStream、匿名パイプ用のAnonymousPipeServerStreamAnonymousPipeClientStreamなどが用意されています。Microsoft Learn

1-2. C#でPipeを使う主な目的

C#でPipeを使う主な目的は、別々に動作しているプロセス間でデータをやり取りすることです。

たとえば、次のような用途があります。

  • 常駐しているサーバープロセスに、別プロセスからコマンドを送る

  • GUIアプリからバックグラウンドプロセスへ処理依頼を送る

  • 子プロセスの処理結果を親プロセスへ返す

  • 既に起動しているアプリへ、2重起動時の引数を渡す

  • ローカル環境で軽量な通信を行う

特に同一マシン上での通信であれば、TCPサーバーを立てるよりも名前付きパイプの方が自然に実装できるケースがあります。ポート番号の管理が不要で、通信相手をパイプ名で指定できるためです。

1-3. 標準入出力・ソケット・共有メモリとの違い

Pipeと似た仕組みに、標準入出力、ソケット、共有メモリがあります。それぞれの違いを理解しておくと、適切な通信方法を選びやすくなります。

標準入出力は、親プロセスが起動した子プロセスと通信する場合に便利です。ProcessStartInfo.RedirectStandardInputRedirectStandardOutputを使えば、子プロセスの入力や出力を制御できます。ただし、基本的には親子関係のあるプロセスで使うケースが中心です。

ソケットは、ネットワーク通信に向いています。同じPC内だけでなく、別マシンとの通信にも使えます。TCPやUDPを使った通信を実装したい場合はソケットが適しています。ただし、ポート番号、ファイアウォール、プロトコル設計などを考える必要があります。

共有メモリは、大量のデータを高速に共有したい場合に向いています。ただし、同期制御や排他制御を慎重に設計する必要があります。

名前付きパイプは、これらの中間的な選択肢です。ストリームとして扱えるため実装しやすく、同一マシン内のプロセス間通信に適しています。また、名前付きパイプは一方向通信だけでなく双方向通信にも対応できます。Microsoftの説明でも、名前付きパイプは一方向または二重パイプを提供できるとされています。Microsoft Learn

1-4. Pipeが向いているケースと向いていないケース

Pipeが向いているのは、同じPC上で動く複数プロセスが、比較的小さなメッセージやコマンドをやり取りするケースです。たとえば、常駐アプリに「設定を再読み込みしてほしい」「ファイルを開いてほしい」「現在の状態を返してほしい」といった命令を送る用途に適しています。

一方で、不特定多数の外部クライアントから接続を受けるWeb APIのような用途には、HTTPやTCPの方が向いています。また、大容量データを継続的に高速転送したい場合や、複雑な通信プロトコルが必要な場合は、gRPC、TCP、共有メモリなども候補になります。

Pipeは便利ですが、万能ではありません。通信範囲、データ量、接続数、セキュリティ要件、クロスプラットフォーム対応の必要性を考慮して選ぶことが重要です。

2. C#で利用できるPipeの種類

2-1. 名前付きパイプとは

名前付きパイプとは、名前を持つPipeです。サーバー側が特定のパイプ名で待ち受け、クライアント側が同じパイプ名を指定して接続します。

C#では、サーバー側でNamedPipeServerStreamを作成し、クライアント側でNamedPipeClientStreamを作成します。パイプ名が一致すれば、別々に起動されたプロセス同士でも通信できます。

名前付きパイプの大きな特徴は、親子関係のないプロセス同士でも通信しやすい点です。たとえば、既に起動している常駐プロセスに対して、後から起動した別アプリが接続するような構成を作れます。

2-2. 匿名パイプとは

匿名パイプとは、名前を持たないPipeです。主に親プロセスと子プロセスの間で通信するために使われます。匿名パイプでは、パイプ名ではなくハンドルを受け渡して通信します。

C#では、AnonymousPipeServerStreamAnonymousPipeClientStreamを使います。Microsoftの公式ドキュメントでも、匿名パイプはローカルコンピューター上のプロセス間通信を提供し、ネットワーク経由の通信には使用できないと説明されています。Microsoft Learn

匿名パイプはシンプルでオーバーヘッドが少ない一方、名前付きパイプほど柔軟ではありません。親プロセスが子プロセスを起動し、その子プロセスへパイプハンドルを渡すような場面で使うのが一般的です。

2-3. 名前付きパイプと匿名パイプの違い

名前付きパイプと匿名パイプの主な違いは、接続方法と利用できる範囲です。

名前付きパイプは、サーバーとクライアントが同じパイプ名を使って接続します。そのため、親子関係のないプロセス同士でも通信できます。また、複数クライアントや双方向通信にも対応しやすいです。

匿名パイプは、名前ではなくハンドルを使って通信します。そのため、親プロセスと子プロセスのように、ハンドルを渡しやすい関係にあるプロセス間で使うのに向いています。

実務では、独立したアプリ同士の通信には名前付きパイプ、親子プロセス間の単純な通信には匿名パイプを選ぶと分かりやすいです。

2-4. NamedPipeServerStreamとNamedPipeClientStreamの役割

NamedPipeServerStreamは、名前付きパイプのサーバー側を表すクラスです。指定したパイプ名で待ち受け、クライアントからの接続を受け付けます。

NamedPipeClientStreamは、名前付きパイプのクライアント側を表すクラスです。サーバー名とパイプ名を指定して、サーバー側のNamedPipeServerStreamへ接続します。

基本的な関係は次の通りです。

C#
// サーバー側
using var server = new NamedPipeServerStream("sample-pipe");

// クライアント側
using var client = new NamedPipeClientStream(".", "sample-pipe", PipeDirection.InOut);

クライアント側のサーバー名に.を指定すると、ローカルコンピューターを意味します。NamedPipeClientStreamのコンストラクターでは、既定のサーバー名として.が使われるものもあります。Microsoft Learn

3. 名前付きパイプによるプロセス間通信の全体像

3-1. サーバー側とクライアント側の関係

名前付きパイプ通信では、一般的にサーバー側が先に起動して待ち受けます。クライアント側は、サーバーが作成したパイプ名を指定して接続します。

サーバー側の役割は、接続を待つこと、クライアントから送られたデータを受信すること、必要に応じて応答を返すことです。

クライアント側の役割は、サーバーへ接続すること、メッセージを送信すること、サーバーからの応答を受信することです。

この関係はWebサーバーとHTTPクライアントに似ていますが、名前付きパイプは主にプロセス間通信で使われ、C#ではストリームとして扱える点が特徴です。

3-2. 通信の流れ

名前付きパイプ通信の基本的な流れは次の通りです。

まず、サーバー側でNamedPipeServerStreamを作成します。次に、WaitForConnectionを呼び出してクライアント接続を待ちます。クライアント側では、NamedPipeClientStreamを作成し、Connectを呼び出してサーバーへ接続します。

接続が確立したら、StreamReaderStreamWriter、またはReadWriteReadAsyncWriteAsyncなどを使ってデータを送受信します。通信が終わったら、ストリームを閉じてリソースを解放します。

サーバー側のWaitForConnectionは、クライアントが接続するまでブロックするメソッドです。公式ドキュメントでも、WaitForConnectionを呼び出すと、クライアントが接続するまでNamedPipeServerStreamがブロックされると説明されています。Microsoft Learn

3-3. 同期通信と非同期通信

名前付きパイプでは、同期通信と非同期通信の両方を実装できます。

同期通信では、WaitForConnectionReadLineWriteLineなどを使います。実装が分かりやすく、簡単なサンプルや小規模なツールでは十分です。ただし、接続待ちや読み取り待ちの間、スレッドがブロックされます。

非同期通信では、WaitForConnectionAsyncReadAsyncWriteAsyncなどを使います。GUIアプリで画面を固めたくない場合や、複数接続を効率よく処理したい場合に適しています。

最初は同期版で仕組みを理解し、実務では必要に応じて非同期版へ拡張するとよいでしょう。

3-4. PipeDirectionによる送受信方向の指定

PipeDirectionは、Pipeの通信方向を指定する列挙型です。主に次の値を使います。

C#
PipeDirection.In     // 読み取り専用
PipeDirection.Out // 書き込み専用
PipeDirection.InOut // 読み書き両方

クライアントからサーバーへメッセージを送るだけなら、サーバー側はIn、クライアント側はOutでも構いません。しかし、サーバーが応答を返す場合は、双方でInOutを指定すると分かりやすくなります。

この記事のサンプルでは、クライアントがメッセージを送り、サーバーが応答を返す構成にするため、PipeDirection.InOutを使います。

3-5. PipeTransmissionModeの選び方

PipeTransmissionModeは、Pipe上のデータをどのように扱うかを指定します。主な値はByteMessageです。

Byteは、データを連続したバイト列として扱います。通常のストリームと同じ感覚で使えるため、多くのケースではByteで十分です。

Messageは、メッセージ単位を意識した通信に使います。公式ドキュメントでは、Byteはデータをバイトのストリームとして送受信し、Messageはメッセージのストリームとして送受信する値として説明されています。また、匿名パイプはMessageモードをサポートしません。Microsoft Learn

実務では、まずPipeTransmissionMode.Byteを選び、1行ごとにメッセージを区切る、先頭にデータ長を付ける、JSONを1行で送るといったアプリケーション側のプロトコルを作る方法が扱いやすいです。

4. C#で名前付きパイプサーバーを実装する方法

4-1. NamedPipeServerStreamの基本的な使い方

名前付きパイプサーバーを実装するには、NamedPipeServerStreamを作成します。最小構成は次のようになります。

C#
using System.IO.Pipes;

using var pipeServer = new NamedPipeServerStream("sample-pipe");
pipeServer.WaitForConnection();

これだけで、sample-pipeという名前のパイプを作成し、クライアントからの接続を待てます。

ただし、実務では通信方向、最大接続数、通信モード、非同期オプションなどを指定することが多いです。

C#
using var pipeServer = new NamedPipeServerStream(
"sample-pipe",
PipeDirection.InOut,
1,
PipeTransmissionMode.Byte,
PipeOptions.None);

この例では、双方向通信、最大1接続、バイトモード、同期処理を指定しています。

4-2. クライアント接続を待ち受ける方法

クライアント接続を待つには、WaitForConnectionを呼び出します。

C#
Console.WriteLine("クライアント接続待機中...");
pipeServer.WaitForConnection();
Console.WriteLine("クライアントが接続しました。");

WaitForConnectionは同期メソッドなので、クライアントが接続するまで処理が停止します。コンソールアプリのサンプルでは分かりやすいですが、GUIアプリでメインスレッドから呼び出すと画面が固まる原因になります。その場合は、別スレッドで実行するか、WaitForConnectionAsyncを使います。

4-3. StreamReaderとStreamWriterで文字列を送受信する方法

名前付きパイプはStreamとして扱えるため、文字列通信にはStreamReaderStreamWriterを使えます。

C#
using var reader = new StreamReader(pipeServer);
using var writer = new StreamWriter(pipeServer)
{
AutoFlush = true
};

string? message = reader.ReadLine();
writer.WriteLine("受信しました: " + message);

ReadLineを使う場合、送信側はWriteLineで改行付きのメッセージを送る必要があります。Writeだけで改行を送らないと、受信側のReadLineが待ち続けることがあります。

また、StreamWriterではAutoFlush = trueを設定しておくと、WriteLine後に自動的にデータが送信されます。AutoFlushを設定しない場合は、必要なタイミングでFlushを呼び出します。

4-4. サーバー側のサンプルコード

次のコードは、クライアントから1行のメッセージを受信し、応答を返す名前付きパイプサーバーのサンプルです。

C#
using System;
using System.IO;
using System.IO.Pipes;
using System.Text;

class Program
{
private const string PipeName = "csharp-sample-pipe";

static void Main()
{
Console.OutputEncoding = Encoding.UTF8;
Console.WriteLine("名前付きパイプサーバーを起動しました。");

using var pipeServer = new NamedPipeServerStream(
PipeName,
PipeDirection.InOut,
1,
PipeTransmissionMode.Byte,
PipeOptions.None);

Console.WriteLine("クライアントの接続を待っています...");
pipeServer.WaitForConnection();
Console.WriteLine("クライアントが接続しました。");

using var reader = new StreamReader(pipeServer, Encoding.UTF8);
using var writer = new StreamWriter(pipeServer, Encoding.UTF8)
{
AutoFlush = true
};

string? message = reader.ReadLine();
Console.WriteLine($"受信: {message}");

writer.WriteLine($"サーバーで受信しました: {message}");

Console.WriteLine("応答を送信しました。Enterキーで終了します。");
Console.ReadLine();
}
}

このサーバーは、csharp-sample-pipeという名前のパイプを作成し、クライアントからの接続を待ちます。接続後、クライアントから送られた文字列を読み取り、確認メッセージを返します。

4-5. 複数クライアント接続に対応する考え方

1つのNamedPipeServerStreamインスタンスは、基本的に1つの接続を処理します。複数クライアントに対応したい場合は、接続ごとに新しいNamedPipeServerStreamを作成し、各接続を別タスクや別スレッドで処理する設計にします。

考え方は次のようになります。

C#
while (true)
{
var pipeServer = new NamedPipeServerStream(
PipeName,
PipeDirection.InOut,
NamedPipeServerStream.MaxAllowedServerInstances,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous);

_ = Task.Run(async () =>
{
using (pipeServer)
{
await pipeServer.WaitForConnectionAsync();
// 接続ごとの処理
}
});
}

ただし、このままだと無制限に待ち受けを作る危険があります。実務では、最大接続数、キャンセル処理、例外処理、ログ出力、アプリ終了時の停止処理を含めて設計する必要があります。

5. C#で名前付きパイプクライアントを実装する方法

5-1. NamedPipeClientStreamの基本的な使い方

名前付きパイプクライアントを実装するには、NamedPipeClientStreamを作成します。

C#
using System.IO.Pipes;

using var pipeClient = new NamedPipeClientStream(
".",
"sample-pipe",
PipeDirection.InOut);

第1引数の.はローカルコンピューターを表します。第2引数には、サーバー側で指定したパイプ名を指定します。第3引数には通信方向を指定します。

サーバー側とクライアント側でパイプ名が一致していないと接続できません。大文字小文字や余分な空白も含めて、同じ名前を使うようにしましょう。

5-2. サーバーへ接続する方法

サーバーへ接続するには、Connectを呼び出します。

C#
pipeClient.Connect();

サーバーが起動していない場合や、指定したパイプ名で待ち受けていない場合、Connectは接続できるまで待機します。タイムアウトを指定したい場合は、ミリ秒単位の引数を渡します。

C#
pipeClient.Connect(5000);

この例では、最大5秒間接続を待ちます。公式ドキュメントでも、Connectは利用可能なパイプインスタンスが現れるまで待機し、timeoutに無限タイムアウトを指定できると説明されています。Microsoft Learn

5-3. メッセージを送信・受信する方法

クライアント側でも、サーバー側と同じようにStreamReaderStreamWriterを使えます。

C#
using var reader = new StreamReader(pipeClient, Encoding.UTF8);
using var writer = new StreamWriter(pipeClient, Encoding.UTF8)
{
AutoFlush = true
};

writer.WriteLine("こんにちは、サーバー");
string? response = reader.ReadLine();
Console.WriteLine(response);

WriteLineで送信し、ReadLineで応答を受け取ります。サーバー側とクライアント側で送受信の順番を合わせることが重要です。

たとえば、サーバーもクライアントも最初にReadLineを呼ぶと、双方が相手の送信を待ち続けてデッドロックします。どちらが先に送るのか、どちらが応答するのかを決めておきましょう。

5-4. クライアント側のサンプルコード

次のコードは、サーバーへメッセージを送り、サーバーからの応答を受信する名前付きパイプクライアントのサンプルです。

C#
using System;
using System.IO;
using System.IO.Pipes;
using System.Text;

class Program
{
private const string PipeName = "csharp-sample-pipe";

static void Main()
{
Console.OutputEncoding = Encoding.UTF8;
Console.WriteLine("名前付きパイプクライアントを起動しました。");

using var pipeClient = new NamedPipeClientStream(
".",
PipeName,
PipeDirection.InOut);

Console.WriteLine("サーバーへ接続しています...");
pipeClient.Connect(5000);
Console.WriteLine("サーバーへ接続しました。");

using var reader = new StreamReader(pipeClient, Encoding.UTF8);
using var writer = new StreamWriter(pipeClient, Encoding.UTF8)
{
AutoFlush = true
};

string message = "こんにちは、名前付きパイプ通信です。";
writer.WriteLine(message);
Console.WriteLine($"送信: {message}");

string? response = reader.ReadLine();
Console.WriteLine($"受信: {response}");

Console.WriteLine("Enterキーで終了します。");
Console.ReadLine();
}
}

このクライアントを実行する前に、先ほどのサーバー側プログラムを起動しておきます。サーバーが待ち受け状態になってからクライアントを起動すると、メッセージの送受信を確認できます。

5-5. 接続タイムアウトを設定する方法

接続タイムアウトを設定するには、Connect(int timeout)を使います。

C#
try
{
pipeClient.Connect(3000);
}
catch (TimeoutException)
{
Console.WriteLine("サーバーへ接続できませんでした。");
}

タイムアウトを指定しないConnect()は、接続できるまで待機します。ツールやGUIアプリでは、無限待機によって操作不能に見えることがあります。そのため、実務ではタイムアウトを設定し、接続できなかった場合のメッセージ表示や再試行処理を用意するのがおすすめです。

6. 実行して確認する:名前付きパイプ通信のサンプル

6-1. サンプルプロジェクトの構成

サンプルを実行するには、サーバー用とクライアント用に2つのコンソールアプリを作成します。

PipeSample/
├─ PipeServer/
│ └─ Program.cs
└─ PipeClient/
└─ Program.cs

.NET CLIを使う場合は、次のように作成できます。

Bash
dotnet new console -n PipeServer
dotnet new console -n PipeClient

PipeServer/Program.csにはサーバー側コードを、PipeClient/Program.csにはクライアント側コードを貼り付けます。

6-2. サーバープロセスを起動する

まず、サーバープロセスを起動します。

Bash
cd PipeServer
dotnet run

実行すると、次のような表示になります。

名前付きパイプサーバーを起動しました。
クライアントの接続を待っています...

この状態で、サーバーはWaitForConnectionによりクライアントからの接続を待っています。

6-3. クライアントプロセスを起動する

別のターミナルを開き、クライアントプロセスを起動します。

Bash
cd PipeClient
dotnet run

クライアント側では、サーバーへ接続してメッセージを送信します。

名前付きパイプクライアントを起動しました。
サーバーへ接続しています...
サーバーへ接続しました。
送信: こんにちは、名前付きパイプ通信です。
受信: サーバーで受信しました: こんにちは、名前付きパイプ通信です。

サーバー側にも、受信したメッセージが表示されます。

クライアントが接続しました。
受信: こんにちは、名前付きパイプ通信です。
応答を送信しました。

6-4. メッセージ送受信の動作確認

このサンプルでは、次の流れで通信しています。

まず、サーバーがcsharp-sample-pipeという名前で待ち受けます。次に、クライアントが同じ名前を指定して接続します。接続後、クライアントがWriteLineでメッセージを送信し、サーバーがReadLineで受信します。最後に、サーバーがWriteLineで応答し、クライアントがReadLineで応答を受け取ります。

ポイントは、送信側がWriteLineを使い、受信側がReadLineを使っていることです。ReadLineは改行が来るまで待つため、送信側が改行を送らないと受信処理が終わりません。

6-5. 実行時によくあるエラーと確認ポイント

サンプル実行時によくある問題は、サーバーを起動する前にクライアントを実行しているケースです。この場合、クライアントは接続できず、タイムアウトや例外が発生します。

また、パイプ名が一致していない場合も接続できません。サーバー側とクライアント側のPipeName定数が同じ値になっているか確認しましょう。

さらに、ReadLineが戻ってこない場合は、送信側がWriteLineを使っているか、AutoFlush = trueを設定しているかを確認します。WriteLineしていてもバッファに残ったまま送信されていないと、相手側が受信できないことがあります。

7. 非同期処理で名前付きパイプ通信を実装する

7-1. WaitForConnectionAsyncの使い方

非同期で接続を待つには、WaitForConnectionAsyncを使います。

C#
await pipeServer.WaitForConnectionAsync();

WaitForConnectionAsyncは、非同期にクライアント接続を待機するメソッドです。公式ドキュメントでは、このメソッドは非同期のため直ちに戻り、クライアントがConnectまたはConnectAsyncを呼び出すと接続処理が完了すると説明されています。Microsoft Learn

非同期処理を使う場合は、NamedPipeServerStream作成時にPipeOptions.Asynchronousを指定しておくとよいです。

C#
using var pipeServer = new NamedPipeServerStream(
PipeName,
PipeDirection.InOut,
1,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous);

7-2. ReadAsyncとWriteAsyncによる非同期送受信

文字列を非同期に送受信する場合も、StreamReaderStreamWriterを使えます。

C#
string? message = await reader.ReadLineAsync();
await writer.WriteLineAsync("受信しました: " + message);
await writer.FlushAsync();

StreamWriterAutoFlush = trueにしておけば、WriteLineAsync後に自動的にフラッシュされます。ただし、明示的にFlushAsyncを呼ぶ設計にしても構いません。

大量データやバイナリデータを扱う場合は、PipeStreamReadAsyncWriteAsyncを直接使う方法もあります。

7-3. CancellationTokenでキャンセルに対応する

非同期処理では、アプリ終了時やタイムアウト時にキャンセルできるようにCancellationTokenを渡す設計が重要です。

C#
using var cts = new CancellationTokenSource();
await pipeServer.WaitForConnectionAsync(cts.Token);

ただし、WaitForConnectionAsyncのキャンセルには注意が必要です。公式ドキュメントでは、キャンセルトークンによるキャンセル要求は、NamedPipeServerStreamPipeOptions.Asynchronousで作成されている場合、またはWaitForConnectionAsyncが呼び出される前にキャンセルが発生した場合に機能すると説明されています。Microsoft Learn

そのため、キャンセル対応を行う非同期サーバーでは、PipeOptions.Asynchronousを指定するのが基本です。

7-4. 非同期版サーバーのサンプルコード

次のコードは、名前付きパイプサーバーを非同期で実装するサンプルです。

C#
using System;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

class Program
{
private const string PipeName = "csharp-async-pipe";

static async Task Main()
{
Console.OutputEncoding = Encoding.UTF8;
using var cts = new CancellationTokenSource();

Console.CancelKeyPress += (sender, e) =>
{
e.Cancel = true;
cts.Cancel();
Console.WriteLine("キャンセル要求を受け付けました。");
};

Console.WriteLine("非同期名前付きパイプサーバーを起動しました。");

try
{
await RunServerAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("サーバーを停止しました。");
}
}

private static async Task RunServerAsync(CancellationToken cancellationToken)
{
using var pipeServer = new NamedPipeServerStream(
PipeName,
PipeDirection.InOut,
1,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous);

Console.WriteLine("クライアントの接続を待っています...");
await pipeServer.WaitForConnectionAsync(cancellationToken);
Console.WriteLine("クライアントが接続しました。");

using var reader = new StreamReader(pipeServer, Encoding.UTF8);
using var writer = new StreamWriter(pipeServer, Encoding.UTF8)
{
AutoFlush = true
};

string? message = await reader.ReadLineAsync(cancellationToken);
Console.WriteLine($"受信: {message}");

await writer.WriteLineAsync($"非同期サーバーで受信しました: {message}");
Console.WriteLine("応答を送信しました。");
}
}

このコードでは、Ctrl+CでキャンセルできるようにCancellationTokenSourceを使っています。接続待ち中にキャンセルされた場合、OperationCanceledExceptionを捕捉してサーバーを終了します。

7-5. 非同期処理を使うべきケース

非同期処理を使うべきなのは、接続待ちや読み書きの間にスレッドを占有したくないケースです。

たとえば、GUIアプリではメインスレッドをブロックすると画面が固まります。サーバーアプリで複数クライアントを処理する場合も、同期処理だけで実装するとスレッド数が増えやすくなります。

一方、単純なコンソールツールや、1回だけ接続してすぐ終了する処理であれば、同期版でも問題ありません。重要なのは、用途に応じて同期と非同期を選ぶことです。

8. 名前付きパイプ実装で注意すべきポイント

8-1. パイプ名の付け方

パイプ名は、アプリケーション内で一意になるように付けます。短すぎる名前や一般的すぎる名前は、他のアプリと衝突する可能性があります。

おすすめは、会社名、アプリ名、用途を含める方法です。

C#
private const string PipeName = "com-example-myapp-command-pipe";

Windowsの名前付きパイプでは、実際には\\.\pipe\パイプ名のような形で扱われますが、C#のNamedPipeServerStreamNamedPipeClientStreamでは通常、パイプ名部分だけを指定します。

8-2. usingによるリソース解放

NamedPipeServerStreamNamedPipeClientStreamは、使い終わったら必ず解放します。C#ではusingを使うのが基本です。

C#
using var pipeServer = new NamedPipeServerStream(PipeName);

ストリームを解放しないと、パイプが開いたままになったり、次回接続時に予期しないエラーが発生したりする可能性があります。StreamReaderStreamWriterも同様に、不要になったら破棄します。

8-3. Flushの必要性

StreamWriterを使って送信する場合、書き込んだデータがすぐに相手へ届くとは限りません。バッファリングされる場合があるためです。

そのため、次のいずれかの対応を行います。

C#
using var writer = new StreamWriter(pipe, Encoding.UTF8)
{
AutoFlush = true
};

または、送信後に明示的にFlushを呼び出します。

C#
writer.WriteLine("message");
writer.Flush();

ReadLineで受信しているのにメッセージが届かない場合、送信側のFlush忘れが原因であることがよくあります。

8-4. 文字コードの指定

文字列を送受信する場合は、文字コードを明示的に指定するのがおすすめです。

C#
using var reader = new StreamReader(pipe, Encoding.UTF8);
using var writer = new StreamWriter(pipe, Encoding.UTF8);

サーバー側とクライアント側で文字コードが異なると、日本語が文字化けする可能性があります。現在のC#アプリではUTF-8を使うケースが多いため、特別な理由がなければEncoding.UTF8で統一するとよいでしょう。

8-5. 例外処理と再接続処理

名前付きパイプ通信では、接続先が起動していない、通信途中で相手が終了した、アクセス権限がない、タイムアウトしたなど、さまざまな例外が発生します。

代表的な例外には、TimeoutExceptionIOExceptionUnauthorizedAccessExceptionOperationCanceledExceptionなどがあります。

実務では、次のように例外を分けて処理します。

C#
try
{
pipeClient.Connect(3000);
}
catch (TimeoutException)
{
Console.WriteLine("接続がタイムアウトしました。");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("パイプへのアクセスが拒否されました。");
}
catch (IOException ex)
{
Console.WriteLine($"I/Oエラーが発生しました: {ex.Message}");
}

クライアント側では、サーバーがまだ起動していない可能性を考慮し、一定回数だけ再接続する処理を用意することもあります。

8-6. セキュリティとアクセス権限の注意点

名前付きパイプはプロセス間通信に使える便利な仕組みですが、誰が接続できるかを考慮する必要があります。特に、権限の異なるプロセス間で通信する場合や、管理者権限のプロセスが通常権限のプロセスから接続を受ける場合は注意が必要です。

Windowsでは、名前付きパイプに対するアクセス制御や偽装といった仕組みが関係します。公式ドキュメントでも、名前付きパイプはクライアント偽装をサポートし、接続プロセスがリモートサーバー上で独自のアクセス許可セットを使用できると説明されています。Microsoft Learn

また、Linux上の.NETでは、これらのAPIの実装にUnixドメインソケットが使われると説明されています。クロスプラットフォーム対応を行う場合は、OSごとの挙動や権限モデルの違いも確認しましょう。Microsoft Learn

9. 名前付きパイプ通信で発生しやすいエラーと対処法

9-1. サーバーに接続できない

クライアントがサーバーに接続できない場合、まずサーバープロセスが起動しているか確認します。名前付きパイプでは、サーバー側がNamedPipeServerStreamを作成し、接続待ち状態になっている必要があります。

次に、パイプ名が一致しているか確認します。

C#
// サーバー側
private const string PipeName = "csharp-sample-pipe";

// クライアント側
private const string PipeName = "csharp-sample-pipe";

また、クライアント側のサーバー名が正しいかも確認します。同一PC上であれば、通常は.を指定します。

C#
new NamedPipeClientStream(".", PipeName, PipeDirection.InOut);

9-2. メッセージが受信できない

メッセージが受信できない場合、送信側と受信側のメソッドの組み合わせを確認します。

受信側がReadLineを使っている場合、送信側はWriteLineで改行を送る必要があります。

C#
writer.WriteLine("hello");

writer.Write("hello")だけでは改行が送られないため、ReadLineが待ち続ける可能性があります。

また、AutoFlush = trueを設定しているか、Flushを呼び出しているかも確認しましょう。

9-3. 通信が途中で切断される

通信途中で切断される場合、相手側のプロセスが終了している可能性があります。サーバーまたはクライアントのどちらかがusingブロックを抜けると、ストリームが破棄され、通信できなくなります。

また、1回のリクエストとレスポンスだけで終了する設計なのか、接続を維持して複数メッセージをやり取りする設計なのかを明確にする必要があります。

複数メッセージを扱う場合は、次のようなループ構造にします。

C#
while (true)
{
string? message = reader.ReadLine();

if (message is null || message == "exit")
{
break;
}

writer.WriteLine("受信: " + message);
}

ReadLinenullを返した場合は、相手側が接続を閉じたと考えて処理を終了します。

9-4. IOExceptionが発生する

IOExceptionは、Pipeが壊れた、相手側が切断された、読み書き中にI/Oエラーが発生したなどの場面で発生します。公式ドキュメントでも、WaitForPipeDrainなどのPipe関連メソッドでは、パイプが壊れた場合や別のI/Oエラーが発生した場合にIOExceptionが発生することが示されています。Microsoft Learn

対処法としては、例外を捕捉し、ログを出力し、必要に応じて再接続します。

C#
catch (IOException ex)
{
Console.WriteLine($"通信エラー: {ex.Message}");
// 必要に応じて再接続処理を行う
}

通信相手が終了する可能性がある設計では、IOExceptionは異常というより想定されるイベントとして扱うこともあります。

9-5. アクセス拒否エラーが発生する

UnauthorizedAccessExceptionやアクセス拒否エラーが発生する場合、権限の違いを確認します。たとえば、サーバーを管理者権限で起動し、クライアントを通常権限で起動している場合などに問題が起きることがあります。

また、同じパイプ名を別のプロセスが既に使用している場合や、セキュリティ設定によって接続が拒否されている場合もあります。

開発中は、まずサーバーとクライアントを同じユーザー権限で実行して動作確認します。その後、必要に応じてアクセス制御を設計しましょう。

9-6. デバッグ時の確認手順

名前付きパイプ通信をデバッグするときは、次の順番で確認すると原因を絞り込みやすくなります。

まず、サーバーが起動しているか確認します。次に、サーバー側がWaitForConnectionまたはWaitForConnectionAsyncまで到達しているかログを出します。

次に、クライアント側が正しいパイプ名でConnectしているか確認します。接続できたら、送信前後、受信前後にログを出します。

C#
Console.WriteLine("送信前");
writer.WriteLine(message);
Console.WriteLine("送信後");

Console.WriteLine("受信前");
string? response = reader.ReadLine();
Console.WriteLine("受信後");

どのログで止まっているかを見れば、接続待ちなのか、送信待ちなのか、受信待ちなのかを判断できます。

10. C#のPipeを実務で活用するユースケース

10-1. 常駐アプリとクライアントアプリの通信

名前付きパイプは、常駐アプリとクライアントアプリの通信に適しています。

たとえば、常駐アプリがバックグラウンドで起動しており、別の小さなクライアントアプリから「状態を取得」「設定を再読み込み」「処理を開始」といったコマンドを送る構成です。

この場合、常駐アプリが名前付きパイプサーバーになり、クライアントアプリが必要なタイミングで接続します。HTTPサーバーを立てるほどではないローカル通信に向いています。

10-2. GUIアプリとバックグラウンドプロセスの連携

GUIアプリでは、重い処理を別プロセスに分離したいことがあります。たとえば、画像処理、ファイル変換、データ解析などです。

このとき、GUIアプリとバックグラウンドプロセスの間で名前付きパイプを使えば、GUI側から処理依頼を送り、バックグラウンド側から進捗や結果を返せます。

GUIアプリではメインスレッドをブロックしないように、非同期通信や別スレッドでの受信処理を使うのが重要です。

10-3. 複数プロセス間でのコマンド送受信

名前付きパイプは、コマンドベースの通信にも向いています。

たとえば、クライアントが次のようなJSONを送信します。

JSON
{"command":"reload-settings"}

サーバーはコマンドを解釈し、結果を返します。

JSON
{"status":"ok","message":"settings reloaded"}

このように、JSONを1行ずつ送受信するプロトコルにすると、StreamReader.ReadLineStreamWriter.WriteLineで扱いやすくなります。

10-4. ローカル環境で高速なIPCを実現する

同じPC内でプロセス間通信を行う場合、名前付きパイプは軽量な選択肢になります。TCPのようにポート番号を公開する必要がなく、パイプ名で通信できます。

ただし、高頻度・大容量のデータ転送では、メッセージ形式やバッファサイズ、非同期処理、再接続処理をきちんと設計する必要があります。単純な文字列通信のサンプルをそのまま大規模データ処理に使うのではなく、用途に合わせてプロトコルを設計しましょう。

10-5. Windowsアプリ開発での活用例

Windowsアプリ開発では、名前付きパイプは特に相性のよいIPC手段です。

たとえば、既に起動しているデスクトップアプリに対して、2回目に起動されたプロセスからファイルパスを渡し、既存ウィンドウで開かせることができます。また、Windowsサービスとデスクトップアプリの連携、管理ツールと常駐エージェントの通信にも利用できます。

ただし、Windowsサービスと通常ユーザーのアプリでは実行ユーザーやセッションが異なる場合があります。アクセス権限やセキュリティ境界を考慮して設計することが重要です。

11. C# Pipeに関するよくある質問

11-1. 名前付きパイプはWindows専用なのか

C#のSystem.IO.Pipesは、.NETで利用できるAPIです。名前付きパイプという仕組みはWindowsでよく使われますが、.NETの実装ではLinux上でUnixドメインソケットが使われると公式ドキュメントに記載されています。Microsoft Learn

ただし、OSごとに内部実装や権限管理、対応する通信モードが異なる場合があります。Windows前提の挙動に強く依存する実装をすると、LinuxやmacOSで期待通りに動かない可能性があります。

クロスプラットフォーム対応が必要な場合は、対象OSで実際に動作確認を行いましょう。

11-2. TCP通信と名前付きパイプはどちらを使うべきか

同一マシン内のプロセス間通信で、外部ネットワークに公開する必要がない場合は、名前付きパイプが候補になります。ポート番号を管理する必要がなく、ローカルIPCとして扱いやすいためです。

一方、別マシンとの通信、言語や環境をまたいだ通信、HTTP APIとして公開したい通信にはTCPやHTTP、gRPCなどが向いています。

判断基準は、通信相手が同じPC内か、ネットワーク越しか、クライアントの種類が限定されるか、既存のプロトコルを使いたいかです。

11-3. 1つのパイプに複数クライアントは接続できるのか

名前付きパイプでは、同じパイプ名を複数のNamedPipeClientStreamで共有できます。公式ドキュメントでも、1つのパイプ名を複数のNamedPipeClientStreamオブジェクトで共有できると説明されています。Microsoft Learn

ただし、サーバー側では接続ごとにNamedPipeServerStreamインスタンスを用意し、複数接続を処理する設計が必要です。単一のサーバーインスタンスで複数クライアントを同時に処理できると考えると、実装で詰まりやすくなります。

11-4. バイナリデータも送受信できるのか

はい、送受信できます。名前付きパイプはStreamとして扱えるため、文字列だけでなくバイト配列も送受信できます。

C#
byte[] data = { 1, 2, 3, 4, 5 };
pipe.Write(data, 0, data.Length);

受信側では、ReadReadAsyncを使ってバイト配列へ読み込みます。

C#
byte[] buffer = new byte[1024];
int bytesRead = pipe.Read(buffer, 0, buffer.Length);

バイナリデータを扱う場合は、どこまでが1つのメッセージなのかを判定する仕組みが必要です。固定長にする、先頭にデータ長を付ける、終端記号を決めるなど、アプリケーション側でプロトコルを設計しましょう。

11-5. .NET Frameworkと.NETの違いはあるのか

System.IO.Pipesは.NET Frameworkでも.NETでも利用できますが、対象バージョンやOSによって対応範囲や挙動が異なる場合があります。

特に、.NET Frameworkは基本的にWindows向けで使われることが多く、現行の.NETはクロスプラットフォーム対応が前提です。そのため、Windowsだけで動かす業務アプリなのか、LinuxやmacOSでも動かすアプリなのかによって、注意点が変わります。

新規開発では、特別な理由がなければ現行の.NETを使い、対象OS上で動作確認を行うのがおすすめです。

まとめ

C#のPipeは、プロセス間通信を実装するための便利な仕組みです。特に名前付きパイプを使うと、親子関係のないプロセス同士でも、同じパイプ名を使って通信できます。

C#では、サーバー側にNamedPipeServerStream、クライアント側にNamedPipeClientStreamを使います。サーバーはWaitForConnectionで接続を待ち、クライアントはConnectで接続します。接続後は、StreamReaderStreamWriterを使って文字列を送受信できます。

基本的な実装では、PipeDirection.InOutPipeTransmissionMode.ByteStreamReader.ReadLineStreamWriter.WriteLineを組み合わせると分かりやすいです。実務では、Flush、文字コード、例外処理、タイムアウト、再接続、リソース解放、セキュリティ設定にも注意しましょう。

また、GUIアプリや複数接続を扱うサーバーでは、WaitForConnectionAsyncReadAsyncWriteAsyncを使った非同期処理が有効です。小さなサンプルでは同期処理で十分ですが、本番アプリではブロッキングやキャンセル処理も考慮する必要があります。

名前付きパイプは、常駐アプリとクライアントアプリの通信、GUIアプリとバックグラウンドプロセスの連携、ローカル環境でのコマンド送受信などに適しています。C#でローカルIPCを実装したい場合は、まず名前付きパイプを候補にしてみるとよいでしょう。