EternalWindows
名前付きパイプ / パイプの作成と接続

プロセス間通信を行うにあたって、Windowメッセージが最も手軽な手段であることは異論のないところでしょう。 Windowメッセージであれば、通信に代表される「接続」といった操作を行うこともありませんし、 相手側からしても「接続してくるまで待機」といった操作を行う必要もありません。 ただし、Windowメッセージがあらゆる場面でも使用できるというと、必ずしもそうとはいえません。 たとえば、Windows Vistaから登場したUIPIという機能は、 整合性レベルが低いプロセスが整合性レベルの高いプロセスにメッセージを送ることを防止しているため、 こうした場合はWindowメッセージを使いにくいと感じるかもしれません。 また、通信相手がサービスのような通常のアプリケーションとは異なるデスクトップで実行している場合は、 メッセージを送受信すること自体ができなくなりますから、名前付きパイプのような別の通信手段を考える必要があります。

名前付きパイプにおける通信は、サーバーが名前付きパイプというハンドルを作成し、 クライアントがそれに接続することで成立しています。 こうした要領はWinsockにおけるソケットとよく似ていますが、 名前付きパイプにはWindowsのセキュリティモデルに統合されているという特徴があります。 Windowsのセキュリティといえば、たとえばアクセスコントロールというものがありますが、 これはオブジェクトにセキュリティ記述子を指定して、任意のアカウントからのアクセスを制限するというものです。 つまり、名前付きパイプにセキュリティ記述子を指定すれば、アクセスするクライアントを制限できます。 また、Windowsのセキュリティはスレッドを実行するユーザーをトークンで識別しますが、 名前付きパイプのサーバーはクライアントのトークンを偽装できます。 偽装するというのは、サーバーがクライアントのアカウントとしてコードを実行するということであり、 オブジェクトへのアクセスをクライアントとして行いたい場合に便利です。 名前付きパイプを使用すればこうしたWindowsの機能を使用できるため、 プロセス間通信においてはWinsockより名前付きパイプの方が向いているといえるでしょう。 逆にインターネット上の通信では、ソケットという既知の概念を使用するWinsockが便利です。

名前付きパイプ(以下パイプ)を作成するには、CreateNamedPipeを呼び出します。

HANDLE WINAPI CreateNamedPipe(
  LPCTSTR lpName,
  DWORD dwOpenMode,
  DWORD dwPipeMode,
  DWORD nMaxInstances,
  DWORD nOutBufferSize,
  DWORD nInBufferSize,
  DWORD nDefaultTimeOut,
  LPSECURITY_ATTRIBUTES lpSecurityAttributes
);

lpNameは、作成するパイプの名前を指定します。 名前の形式は\\.\pipe\pipenameであり、pipenameの部分には任意の文字列を指定できます。 dwOpenModeは、アクセスモードなどを指定します。 PIPE_ACCESS_DUPLEXを指定したパイプは読み取りにも書き込みにも使用できることを意味し、 PIPE_ACCESS_INBOUNDを指定した場合は読み取りのみ(クライアントは書き込みのみ)、 PIPE_ACCESS_OUTBOUNDを指定した場合は書き込み(クライアントは読み取りのみ)が可能になります。 dwPipeModeは、パイプのタイプモードを指定します。 通常は、バイトストリームのデータを扱うことを示すPIPE_TYPE_BYTEでよいと思われます。 nMaxInstancesは、パイプに対して作成できるインスタンスの最大数を指定します。 指定できる値は1からPIPE_UNLIMITED_INSTANCES(255)までであり、 PIPE_UNLIMITED_INSTANCESを指定すれば、リソースの許す限りインスタンスを作成できるとされています。 nOutBufferSizeは、出力バッファとして予約すべきバイト数を指定します。 nInBufferSizeは、入力バッファとして予約すべきバイト数を指定します。 これらバイト数は参考として使用されるだけで、実際にこの数だけバッファが割り当てられるとは限りません。 nDefaultTimeOutは、クライアントが呼び出すWaitNamedPipeのタイムアウト値をミリ秒単位で指定します。 lpSecurityAttributesは、パイプに設定するセキュリティ記述子を指定します。

パイプに対して作成できるインスタンスという言葉は、少し分かりにくいところがあるかもしれません。 たとえば、nMaxInstancesを2としてCreateNamedPipeを呼び出した場合、 作成されるのはパイプの1つ目のインスタンスです。 クライアントがパイプへの接続を行おうとした場合、この1つ目のインスタンスと接続されます。 次にサーバーがCreateNamedPipeを最初と同じ引数で呼び出した場合、 今度はパイプの2つ目のインスタンスが作成されます。 次のクライアントはこの2つ目のインスタンスに接続するため、 1つ目のインスタンスで扱われるデータが、2つ目のインスタンスに影響を与えることはありません。 サーバーがさらにCreateNamedPipeを呼び出した場合、また新しいインスタンスが作成されるように思えますが、 nMaxInstancesが2である場合は、同時に作成できるインスタンスは2つ限界であるため、これは失敗します。 それでは、3つ目のクライアントは決してインスタンスに接続できないかというと、そういうわけではありません。 たとえば、1つ目のインスタンスに接続していたクライアントが終了した場合、 1つ目のインスタンスに接続しているクライアントが存在しなくなりますから、 このインスタンスに3つ目のクライアントは接続できます。 つまり簡単に言えば、nMaxInstancesとは同時に接続できるクライアントの数ということになります。

パイプを作成したサーバーは、ConnectNamedPipeでクライアントからの接続を待ちます。

BOOL WINAPI ConnectNamedPipe(
  HANDLE hNamedPipe,
  LPOVERLAPPED lpOverlapped
);

hNamedPipeは、CreateNamedPipeで作成したパイプのハンドルを指定します。 lpOverlappedは、OVERLAPPED構造体のアドレスを指定します。 これはConnectNamedPipeで待機するのではなく、イベントオブジェクト(OVERLAPPED.hEvent)を使用して待機を行いたい場合に使用します。 この場合、ConnectNamedPipeは直ちに制御を返し、別途GetOverlappedResultやWaitForMultipleObjectsを呼び出すことになるでしょう。 クライアントが接続してくるまで待機したい場合は、NULLを指定します。

それではまず、サーバーのコードを示します。

#include <windows.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HANDLE hPipe;
	TCHAR  szData[256];
	DWORD  dwResult;

	hPipe = CreateNamedPipe(TEXT("\\\\.\\pipe\\SamplePipe"), PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE, 1, sizeof(szData), sizeof(szData), 1000, NULL);
	if (hPipe == INVALID_HANDLE_VALUE) {
		MessageBox(NULL, TEXT("パイプの作成に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
	
	ConnectNamedPipe(hPipe, NULL);
	
	ReadFile(hPipe, szData, sizeof(szData), &dwResult, NULL);
	WriteFile(hPipe, szData, (lstrlenW(szData) * sizeof(TCHAR)) + 1, &dwResult, NULL);

	MessageBox(NULL, TEXT("終了します。"), TEXT("サーバー"), MB_OK);
	
	CloseHandle(hPipe);
	
	return 0;
}

CreateNamedPipeの第2引数がPIPE_ACCESS_DUPLEXであることから、このパイプは読み取りと書き込みに使用できます。 また、第4引数が1であることから、パイプに同時に接続できるクライアントは1つのみです。 パイプを作成したら、クライアントがパイプに接続できるようにConnectNamedPipeを呼び出します。 第2引数がNULLであるため、クライアントが接続をしない限り関数は制御を返しません。 関数が制御を返したら、ReadFileを呼び出してクライアントからデータが書き込まれるまで待機します。 ただし、今回のクライアントは接続の後に直ちにデータを書き込むため、 ReadFileによる待機を感じることはないでしょう。 データを取得したサーバーは、その応答を返すためにWriteFileを呼び出しますが、 今回は簡単のため、取得したデータをそのまま返すようにしています。 実際の開発では暗号化に使用する鍵を双方に持たせるなど、意味のあるデータ交換を行うべきです。

続いて、クライアントのコードを示します。

#include <windows.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HANDLE hPipe;
	TCHAR  szBuf[256];
	TCHAR  szData[] = TEXT("sample-data");
	DWORD  dwResult;

	hPipe = CreateFile(TEXT("\\\\.\\pipe\\SamplePipe"), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
	if (hPipe == INVALID_HANDLE_VALUE) {
		MessageBox(NULL, TEXT("パイプへの接続に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	WriteFile(hPipe, szData, sizeof(szData), &dwResult, NULL);
	ReadFile(hPipe, szBuf, sizeof(szBuf), &dwResult, NULL);

	MessageBox(NULL, szBuf, TEXT("クライアント"), MB_OK);

	CloseHandle(hPipe);

	return 0;
}

クライアントはCreateFileを呼び出すことでパイプに接続できます。 第1引数は接続するパイプの名前であり、サーバーがSamplePipeという文字列を指定していたため、同じ文字列を指定します。 .の部分にはサーバーが存在するコンピュータ名を指定できますが、クライアントとサーバーが同じコンピュータに存在する場合は.で構いません。 サーバーはパイプをPIPE_ACCESS_DUPLEXで作成したため、パイプは読み書きの両方が可能です。 この場合はCreateFileの第2引数に、GENERIC_READとGENERIC_WRITEを指定します。 パイプへの接続に成功したらWriteFileでデータを書き込み、 サーバーからの応答をReadFileで取得します。 サーバーは書き込まれたデータをそのまま返していましたから、 "sample-data"という文字列が返ったら成功です。


戻る