プロセス間通信をしてみた(1)
Windows10の名前付きパイプでの通信
どもです。
今回のエントリはプロセス間通信、特にVisualStudio/Windows10でのプロセス間通信について書きます。
1.プロセス間通信
1.1.プロセス間通信とは
プロセス間通信とは、
互いに独立して動作するプロセスを共働して動作させる際に、このプロセスの間で情報(動作するタイミングやデータ)をやり取りするための方法
です。
この説明での「プロセス」は、Windows10では「アプリケーション」と考えてください。
1.2.プロセス間通信の種類
一言で「プロセス間通信」といっても、様々な種類があります。
具体的には、
- 共有メモリ
- 同期
- メッセージ交換
- パイプ(PIPE)
が挙げられます。
今回のエントリでは、この中の「パイプ(PIPE)」、特に「名前付きパイプ」によるプロセス間通信を行ってみた内容について書きます。
1.3.「名前付きパイプ」とは
Microsoftのページによれば、名前付きパイプは
- 一方通行、または両方向の通信
- サーバと1つ以上のクライアントとの通信を行う。
- 全てのパイプのインスタンスは同じ名前をもつが、インスタンスごとにバッファとハンドルをもつ。
というものです。
より詳細な説明は、上述のリンク先を参照してください。
今回のエントリでは、
- 一方通行(クライアント→サーバ)
- サーバとクライアントは1対1で通信
での名前付きパイプを使用したプロセス間通信を行います。
1.4.「名前付きパイプ」のイイトコロ
名前付きパイプのイイトコロ(メリット)は、データの送信/受信がファイルへの書き込みと同じ処理で実現できる、ということです。
即ち、WriteFileやReadFileでデータの送信/受信が可能になります。
上手く設計すれば、既存のファイル書き込み/読み出し処理の流用、あるいはその逆も可能になります。
2.開発環境
開発環境は、下表の通りです。
項目 | 内容 |
---|---|
OS | Windows10 Pro(1909) |
CPU | i7-8700 |
メモリ | 16GB |
IDE | VisualStudioCommnuity 2019 version 16.5.1 ※プロジェクトの設定
|
VisualStudioのプロジェクトは、コンソールアプリケーションかつMFCを使用する設定で作成しています。
この設定は、サーバ側/クライアント側の両方で共通です。
この環境で、サーバ側/クライアント側の実装を、それぞれ書きます。
2.1.サーバ側
まず、サーバ側です。
サーバ側の実装(一部抜粋ですが)は、以下です。
int main()
{
//...(省略)...
HANDLE hPipe = INVALID_HANDLE_VALUE;
hPipe = CreateNamedPipe(_T("\\\\.\\pipe\\sample_pipe"),
PIPE_ACCESS_INBOUND,
PIPE_TYPE_BYTE | PIPE_WAIT,
1,
0,
0,
100,
NULL);
if (INVALID_HANDLE_VALUE == hPipe) {
_tprintf(_T("The pipe can not open.\r\n"));
}
else {
_tprintf(_T("The pipe can open.\r\n"));
}
if (0 == ConnectNamedPipe(hPipe, NULL)) {
_tprintf(_T("Can not connect named pipe"));
}
else {
_tprintf(_T("Can connect named pipe"));
}
SHORT failedCount = 0;
do {
_TCHAR buffer[256];
ZeroMemory(buffer, 256);
DWORD readDataSize = 0;
BOOL readRes = ReadFile(hPipe, buffer, sizeof(buffer), (LPDWORD)&readDataSize, NULL);
if (!readRes) {
//_tprintf(_T("Can not read data.\r\n"));
failedCount++;
}
else {
_tprintf(_T("Can read data : \r\n"));
_tprintf(_T("Read data size : %d\r\n"), readDataSize);
_tprintf(_T("Read data : %s\r\n"), buffer);
failedCount = 0;
}
Sleep(10);
} while (failedCount < 2000); //Max 20 second
FlushFileBuffers(hPipe);
DisconnectNamedPipe(hPipe);
CloseHandle(hPipe);
//...(省略)...
}
サーバー側のプログラムでは、次の処理を行います。
- 名前付きパイプの生成
- パイプの接続待ち
- 名前付きパイプからデータ読み出し
- ただし、データ読み出しに連続2000回失敗したら、プログラム終了
簡単ですが、各処理の内容と使用している関数について、簡単にですが説明します。
2.1.1.名前付きパイプの生成
名前付きパイプは、CreateNamedPipe()関数で生成します。
当該関数の詳細は、Microsoftのサイトを確認してください。
CreateNamedPipe()関数の第1引数に名前付きパイプに指定する「名前」を設定します。
この名前は、
「\\.\pipe\(パイプ名)」
というフォーマットで指定します。
「\」をエスケープするために「\」を2つ重ね、結果として「\」が4つ連続しています。
第2引数は、通信方向を設定します。
今回は「クライアント→サーバ」の一方通行での通信なので、入力のみであることを示す「PIPE_ACCESS_INBOUND」を指定します。
なお、両方向での通信(クライアント⇔サーバ)の場合には、「PIPE_ACCESS_DUPLEX」を指定する、あるいは「PIPE_ACCESS_INBOUND」と「PIPE_ACCESS_OUTBOUND」の論理和を指定します。
第3引数では、パイプのモードを設定します。
データの送信方法を指定するもので、バイトストリーム/メッセージストリームを指定します。
今回は、バイトデータでデータを送信するので、「PIPE_ACCESS_INBOUND」を指定します。
2.1.2.パイプの接続(待ち)
パイプを生成したら、ConnectNamedPipe()でクライアント側からの接続を待ちます。
この関数の第1引数には、CreateNamedPipe()関数が返すハンドルを指定します。
なお、クライアント側がパイプに接続されるまで、ConnectNamedPipe()から制御は返りません。
2.1.3.名前付きパイプからデータ読み出し
名前付きパイプから、送信されたデータを読み出します。
読み出したデータを格納するバッファの指定などについては、Microsoftのサイトを確認してください。
ReadFile()関数については、第1引数にCreateNamedPipe()のハンドルを指定する以外に、特筆することはありません。
最初に書いたように、ファイルにデータを書き込む場合と同じ処理でOKです。
なお、念のための処理として2000回連続してReadFile()に失敗した場合には、プログラムが終了するようにしています。
※プログラムを終了させるための方法は、適宜変更してください。
2.2.クライアント側
次に、クライアント側の実装(一部抜粋)を示します。
int main()
{
HANDLE hPipe = CreateFile(_T("\\\\.\\pipe\\sample_pipe"),
GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (INVALID_HANDLE_VALUE == hPipe) {
_tprintf(_T("Can not create file, as PIPE\r\n"));
}
else {
_tprintf(_T("Can create file, as PIPE\r\n"));
int loopCount = 0;
int maxLoopNum = 5;
do {
_TCHAR buffer[256];
ZeroMemory(buffer, sizeof(buffer));
_stprintf_s(buffer, _T("CLIENT SEND DATA = %d"), loopCount);
DWORD writtenWordSize = 0;
DWORD numberOfByteToWrite = _tcsclen(buffer) * sizeof(TCHAR);
_tprintf(_T("Send data len : %d\r\n"), numberOfByteToWrite);
BOOL writeRes = WriteFile(hPipe, buffer, numberOfByteToWrite, (LPDWORD)&writtenWordSize, NULL);
if (!writeRes) {
_tprintf(_T("Can not write data into file.\r\n"));
}
else {
_tprintf(_T("Can write data into file.(data size = %d)\r\n"), writtenWordSize);
}
loopCount++;
Sleep(100);
} while (loopCount < maxLoopNum);
}
CloseHandle(hPipe);
}
クライアント側のプログラムの処理内容は、次の通りです。
- 名前付きパイプへの接続
- データの送信
- 終了処理
上述のプログラムでは、「CLIENT SEND DATA = (ループの回数)」というフォーマットで、サーバにデータを5回送信する、という動作をします。
サーバ側同様、処理内容と使用している関数の簡単な説明を行います。
2.2.1.名前付きパイプへの接続
名前付きパイプには、CreateFile()で接続します。
CreateFile()関数の詳細は、Microsoftのサイトを確認してください。
実際にファイルに値を書き込む場合との違いは、第1引数です。
名前付きパイプの場合には、パイプの名前を指定します。
この時の「パイプの名前」は、サーバ側と同じ値にします。
2.2.2.データの送信
データの送信は、WriteFile()を使用します。
これも、通常のファイル書き込みと同じ使い方です。
2.2.3.終了処理
データの送信が完了した後は、CloseHandle()関数で名前付きパイプとの接続を解除します。
3.実行結果
上記プログラムの実行結果は、下図の通りになります。
まずは、クライアント側。
次に、サーバ側です。
このように、クライアントから送信したデータを、サーバが受信できていることが分かります。
3.1.プラットフォームの違いは?
ところで、VisualStudioでの開発では、プラットフォームをx64(64bit)/x86(32bit)のいずれかからか選択ができます。
サーバ側とクライアント側のプラットフォームが異なっている場合でも、通信ができるのかを確認してみました。
確認結果は、下表の通りです。
サーバ側 | |||
---|---|---|---|
x64 | x86 | ||
クライアント側 | x64 | 通信可 | 通信可 |
x86 | 通信可 | 通信可 |
この通り、名前付きパイプによるプロセス間通信では、サーバ/クライアントのプラットフォームに依存せず通信が可能であることが分かります。
4.まとめ
今回のエントリでは、名前付きパイプによるプロセス間通信を紹介しました。
紹介した内容は、名前付きパイプの最も基本的な使い方です。
より精密な制御を行うためには、本エントリで紹介している内容では不十分です。
そういった内容については、今後のエントリで紹介していく予定です。
また、名前付きパイプによるプロセス間通信は、プラットフォームに依存することなく通信が可能であることが分かりました。
アプリケーションの開発をしていると、プラットフォームの違いは、色々問題になる場面があります。
今回の確認結果が、そういった問題の解決の一助になれば嬉しいです。
ではっ!
ディスカッション
コメント一覧
まだ、コメントがありません