AI対戦サーバープログラム作成のヒント

その3 パイプを使って通信する

3回目となる今回は、その1、その2の内容を1つにして、親子間でのプロセス通信を行います。
ひとまず、プログラムの条件として、以下の盛り込むことにします。

作るプログラムの条件

おそらく言っている意味がわからないでしょうから、先に進んでプログラムを見ながら理解してみてください。

子プロセスを作成する

まず、子プロセスとなるプログラムを作ります。
内容は、1回標準入力からテキストを受け取り、標準出力へテキストを流します。

#include <stdio.h>

int main()
{
  char str[128];
  fprintf(stderr, "child start\n");

  if(fgets(str, 128, stdin) == NULL)
    fprintf(stderr, "fgets return NULL");
  else
    fprintf(stderr, "fgets recieve %s\n", str);

  printf("send test message from child.");
  fprintf(stderr, "child end\n");
  return 0;
}

標準入力から一度fgets関数でchar配列に退避させています。

確認用に、stderrにテキストを出しています。
標準出力(stdout)でない理由は、親プロセスの解説のときにします。

これをコンパイルして出来た実行ファイルを「child.exe」としておきます。

親プロセスを作成する

では、本題の親となるプログラムを作ります。
まずは前半部分の、main関数です。

main関数

#include <windows.h>
#include <stdio.h>

#define R 0
#define W 1

int execute();
int createPipe(HANDLE *readPipe, HANDLE *writePipe, BOOL readInherit, BOOL writeInherit);

int main(void) {

  int r;
  printf("parent start.\n");
  r = execute();
  printf("parent end.\n");

  return r;
}

新たにR,Wをdefineしたり、createPipe関数を宣言したりしています。
必要なものは全てexecute関数内で用意しているので、非常にスッキリです。

createPipe関数

次に、createPipe関数を確認します。
windows.hには、CreatePipe関数が用意されていますが、この簡易拡張を行っています。

int createPipe(HANDLE *readPipe, HANDLE *writePipe, BOOL readInherit, BOOL writeInherit)
{
  HANDLE readTemp, writeTemp;

  if(!CreatePipe(&readTemp, &writeTemp, NULL, 0)) {
    fprintf(stderr, "CreatePipe\n");
    return -1;
  }

  if(!DuplicateHandle(
      GetCurrentProcess(), readTemp,
      GetCurrentProcess(), readPipe,
      0, readInherit, DUPLICATE_SAME_ACCESS)) {
    fprintf(stderr, "DuplicacteHandle\n");
    if(!CloseHandle(readTemp))
      fprintf(stderr, "CloseHandle(readTemp)\n");
    return -1;
  }
  if(!CloseHandle(readTemp)) {
    fprintf(stderr, "CloseHandle(readTemp)\n");
    return -1;
  }

  if(!DuplicateHandle(
      GetCurrentProcess(), writeTemp,
      GetCurrentProcess(), writePipe,
      0, writeInherit, DUPLICATE_SAME_ACCESS)) {
    fprintf(stderr, "DuplicacteHandle\n");
    if(!CloseHandle(writeTemp))
      fprintf(stderr, "CloseHandle(writeTemp)\n");
    return -1;
  }
  if(!CloseHandle(writeTemp)) {
    fprintf(stderr, "CloseHandle(writeTemp)\n");
    return -1;
  }

  return 0;
}

一度Tempハンドルにパイプを作成してから、実際に返すハンドルに複製をしています。
ポイントはDuplicateHandle関数の引数にreadInherit, writeInheritを与えていることです。
この部分は、「子プロセスにパイプを継承するか」を決めているのですが、継承していないと子プロセスがパイプを使うことが出来ません。
TRUEを与えることで子プロセスが使用可能に、FALSEを与えることで使用不可能になります。
また、あとで問題が起きてしまうため、子プロセスが使用しないパイプハンドルは必ず継承不可(FALSE)にします。

execute関数

最後にexecute関数を確認していきます。
だいぶ長くなっているので、少しずつ分割して見ていくことにします。
実際には、全てのプログラムを連結させたものがexecute関数の全体です。

int execute() {

  HANDLE stdinPipe[2];
  HANDLE stdoutPipe[2];

  HANDLE childProcess;
  BOOL bInheritHandles = TRUE;
  DWORD creationFlags = 0;

  STARTUPINFO si = {};
  PROCESS_INFORMATION pi = {};
  LPTSTR commandLine = TEXT("child.exe");

  const char message[] = "example-pipe";
  DWORD numberOfBytesWritten;

  if(createPipe(&stdinPipe[R], &stdinPipe[W], TRUE, FALSE)) {
    fprintf(stderr, "createPipe\n");
    return -1;
  }
  if(createPipe(&stdoutPipe[R], &stdoutPipe[W], FALSE, TRUE)) {
    fprintf(stderr, "createPipe\n");
    return -1;
  }

まずは、必要な変数の用意とパイプの作成です。
今回、親->子の通信と、子->親の通信の両方を行うために、2本のパイプを用意しています。
パイプは一方通行なので、通す方向の数だけ用意する必要があります。

  // STARTUPINFO の設定
  si.cb = sizeof(STARTUPINFO);
  si.dwFlags = STARTF_USESTDHANDLES;
  si.hStdInput = stdinPipe[R]; // 標準入力にパイプを接続
  si.hStdOutput = stdoutPipe[W]; // 標準出力にパイプを接続
  si.hStdError = GetStdHandle(STD_ERROR_HANDLE); // stderrは親プロセスと同じものを使う

  if (si.hStdOutput == INVALID_HANDLE_VALUE) {
    fprintf(stderr, "GetStdHandle(STD_OUTPUT_HANDLE)");
    return -1;
  }
  if (si.hStdError == INVALID_HANDLE_VALUE) {
    fprintf(stderr, "GetStdHandle(STD_ERROR_HANDLE)");
    return -1;
  }

  // 子プロセスの起動
  if (!CreateProcess(
    commandLine, //実行するファイル名
    NULL,	//実行時引数の指定
    NULL,	//プロセスのセキュリティー記述子
    NULL,	//スレッドのセキュリティー記述子
    FALSE,	//ハンドルを継承しない(子は親を操作できない)
    0,		//作成フラグ
    NULL,	//環境変数は引き継ぐ
    NULL,	//カレントディレクトリーは同じ
    &si,
    &pi))
  {
    fprintf(stderr, "SreateProcess\n");
    return -1;
  }

  childProcess = pi.hProcess;
  if(!CloseHandle(pi.hThread))
    fprintf(stderr, "CloseHandle(hThread)\n");

子プロセスの起動はその1とほぼ同じなので省略します。
ただし、子プロセスの起動の前にsiの設定を行っています。
ここで、子プロセスの標準入力と、標準出力をパイプに接続しています。
これによって、親がパイプに書き込んだものを子が標準入力から受け取ることができ、
子が標準出力に書き込んだものを親がパイプから受け取ることができるようになります。
この仕組みによって、子プロセスはパイプの存在を気にしないで済むようになります。

また、エラー出力には親プロセスと同じものを使うようにしています。
親のstderrは親のstdoutとほぼ同じで、画面に出力されます。
これを利用して、子プロセスでは、確認用に標準エラーに出力を行っています。

  // 親で使わない読み込みパイプハンドルをクローズ
  if(!CloseHandle(stdinPipe[R]))
    fprintf(stderr, "CloseHandle(stdinPipe[R])\n");
  stdinPipe[R] = NULL;
  if(!CloseHandle(stdoutPipe[W]))
    fprintf(stderr, "CloseHandle(stdoutPipe[W])\n");
  stdinPipe[R] = NULL;

  // messageの送信
  if (!WriteFile(stdinPipe[W], message, strlen(message), &numberOfBytesWritten, NULL)) {
    fprintf(stderr, "WriteFile\n");
    return -1;
  }
  // 送信が完了したら閉じる
  if(!CloseHandle(stdinPipe[W])) {
    fprintf(stderr, "CloseHandle(stdinPipe[W])\n");
    return -1;
  }
  stdinPipe[W] = NULL;

  // 子プロセスの終了待ち
  DWORD r = WaitForSingleObject(childProcess, INFINITE);
 
  CloseHandle(childProcess);

親プロセスでは使用しないパイプのハンドルはあらかじめ閉じておきます。
詳しい説明は省きますが、閉じておかないと子プロセス側で問題が発生する可能性があるからです。

その後、子プロセスの標準入力に渡したパイプの対となるパイプにメッセージを送信して、子プロセスの終了を待っています。
子プロセスはprintfで親プロセスにメッセージを送ってくるので、それを待っています。

ここでも、不要になったハンドルは片っ端から閉じています。
さて、次がexecute関数の最後の部分です。あと少しですので、頑張ってください!

  // パイプからの読み込み
  {
    char buf[256+1];
    DWORD numberOfBytesRead;
    if (!ReadFile(stdoutPipe[R], buf, sizeof(buf)-1, &numberOfBytesRead, NULL)) {
      if (GetLastError() == ERROR_BROKEN_PIPE) {
        printf("pipe end\n");
      }
      printf("ReadFile");
      return -1;
    }

    buf[numberOfBytesRead] = '\0';
    printf("read=[%s]\n", buf);
  }
  // 読込パイプのクローズ
  if (!CloseHandle(stdoutPipe[R])) {
    printf("CloseHandle(stdoutPipe[R])");
    return -1;
  }
  stdoutPipe[R] = NULL;
 
  return 0;
}

子プロセスからのメッセージを受け取る部分がブロックになっているのは、変数を用意したかったからです。深い意味はありません。
ReadFile関数で、子プロセスから来たメッセージを1回だけ受け取り、出力しています。
あとはこの読み込みパイプを閉じておしまいです。

実行結果

最後にこのプログラムの実行結果を確認して終わりにします。 2つの実行ファイルを同じフォルダに入れて親側のプログラムを実行させます。

parent start.
child start
fgets recieve example-pipe
child end
read=[send test message from child.]
parent end.

親プロセスの中で、子プロセスが呼び出されています。
子プロセスでstderrに出力したメッセージが親側のstderrに出ていて、 親から子へのメッセージが正しく伝わっているのが確認できます。
また、子がprintfで出力したメッセージが親側に正しく渡っていることも確認できます。

今回はここまでです。長い文章でしたが、お疲れさまでした。

補足

前回:その2 パイプを使ってみる  次回:その4 複数回の通信とタイムアウト(予定)

コメント



トップ   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS