プロセス間通信をしてみた(1)
Windows10の名前付きパイプでの通信

2021年2月7日

どもです。

今回のエントリはプロセス間通信、特にVisualStudio/Windows10でのプロセス間通信について書きます。

1.プロセス間通信

1.1.プロセス間通信とは

プロセス間通信とは、

互いに独立して動作するプロセスを共働して動作させる際に、このプロセスの間で情報(動作するタイミングやデータ)をやり取りするための方法

です。
この説明での「プロセス」は、Windows10では「アプリケーション」と考えてください。

1.2.プロセス間通信の種類

一言で「プロセス間通信」といっても、様々な種類があります。
具体的には、

  • 共有メモリ
  • 同期
  • メッセージ交換
  • パイプ(PIPE)

が挙げられます。
今回のエントリでは、この中の「パイプ(PIPE)」、特に「名前付きパイプ」によるプロセス間通信を行ってみた内容について書きます。

1.3.「名前付きパイプ」とは

Microsoftのページによれば、名前付きパイプは

  • 一方通行、または両方向の通信
  • サーバと1つ以上のクライアントとの通信を行う。
  • 全てのパイプのインスタンスは同じ名前をもつが、インスタンスごとにバッファとハンドルをもつ。

というものです。
より詳細な説明は、上述のリンク先を参照してください。
今回のエントリでは、

  • 一方通行(クライアント→サーバ)
  • サーバとクライアントは1対1で通信

での名前付きパイプを使用したプロセス間通信を行います。

1.4.「名前付きパイプ」のイイトコロ

名前付きパイプのイイトコロ(メリット)は、データの送信/受信がファイルへの書き込みと同じ処理で実現できる、ということです。
即ち、WriteFileReadFileでデータの送信/受信が可能になります。
上手く設計すれば、既存のファイル書き込み/読み出し処理の流用、あるいはその逆も可能になります。

2.開発環境

開発環境は、下表の通りです。

項目 内容
OS Windows10 Pro(1909)
CPU i7-8700
メモリ 16GB
IDE VisualStudioCommnuity 2019
version 16.5.1
※プロジェクトの設定

  • コンソールアプリケーション
  • MFC使用

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);
    //...(省略)...
}

サーバー側のプログラムでは、次の処理を行います。

  1. 名前付きパイプの生成
  2. パイプの接続待ち
  3. 名前付きパイプからデータ読み出し
    • ただし、データ読み出しに連続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);
}

クライアント側のプログラムの処理内容は、次の通りです。

  1. 名前付きパイプへの接続
  2. データの送信
  3. 終了処理

上述のプログラムでは、「CLIENT SEND DATA = (ループの回数)」というフォーマットで、サーバにデータを5回送信する、という動作をします。
サーバ側同様、処理内容と使用している関数の簡単な説明を行います。

2.2.1.名前付きパイプへの接続

名前付きパイプには、CreateFile()で接続します。
CreateFile()関数の詳細は、Microsoftのサイトを確認してください。
実際にファイルに値を書き込む場合との違いは、第1引数です。
名前付きパイプの場合には、パイプの名前を指定します。
この時の「パイプの名前」は、サーバ側と同じ値にします。

2.2.2.データの送信

データの送信は、WriteFile()を使用します。
これも、通常のファイル書き込みと同じ使い方です。

2.2.3.終了処理

データの送信が完了した後は、CloseHandle()関数で名前付きパイプとの接続を解除します。

3.実行結果

上記プログラムの実行結果は、下図の通りになります。
まずは、クライアント側。
named_pipe_windows_001_001
次に、サーバ側です。
named_pipe_windows_001_002
このように、クライアントから送信したデータを、サーバが受信できていることが分かります。

3.1.プラットフォームの違いは?

ところで、VisualStudioでの開発では、プラットフォームをx64(64bit)/x86(32bit)のいずれかからか選択ができます。
サーバ側とクライアント側のプラットフォームが異なっている場合でも、通信ができるのかを確認してみました。
確認結果は、下表の通りです。

サーバ側
x64 x86
クライアント側 x64 通信可 通信可
x86 通信可 通信可

この通り、名前付きパイプによるプロセス間通信では、サーバ/クライアントのプラットフォームに依存せず通信が可能であることが分かります。

4.まとめ

今回のエントリでは、名前付きパイプによるプロセス間通信を紹介しました。
紹介した内容は、名前付きパイプの最も基本的な使い方です。
より精密な制御を行うためには、本エントリで紹介している内容では不十分です。
そういった内容については、今後のエントリで紹介していく予定です。

また、名前付きパイプによるプロセス間通信は、プラットフォームに依存することなく通信が可能であることが分かりました。
アプリケーションの開発をしていると、プラットフォームの違いは、色々問題になる場面があります。
今回の確認結果が、そういった問題の解決の一助になれば嬉しいです。

ではっ!