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

その4 複数回通信と強制終了

今までは、親子間で1回だけメッセージを送受信するだけでした。
今回は、それを何回でもやり取りをできるようにします。
また、子プロセスの無限ループ対策に、子プロセスを強制終了させることも学びます。

作るプログラムの条件

前回まで、親プロセスでは、子プロセスの終了を待ってからメッセージの受信(ReadFile)を行っていました。
ややこしい話ですので詳細は省きますが、子プロセスを終了させないと、出力バッファにメッセージが入らない、という問題によるものだと思います。

これでは複数回の通信を可能にするために子プロセスを生存させたままちゃんとやり取りができるようにします。
具体的には、以下のプログラムを作ります。

やっぱり言っている意味がわからないでしょうから、まずは先に進んでプログラムを見て、それから意味をわかってください。

子プロセスを作成する

まず、子プロセスとなるプログラムを作ります。

#include <stdio.h>
#include <stdlib.h>

int input();

int main() {
  int l=0;

  while(l<10) {
    int d = input();
    printf("%d\n", d*10);
    fflush(stdout);
    l++;
  }

  return 0;
}

int input() {
  char buf[16];
  int d;

  if(fgets(buf, 16-1, stdin) == NULL){
    fprintf(stderr, "C:fgets return NULL\n");
    exit(1);
  }

  d = atoi(buf);

  return d;
}

今回は、入力部分をinput関数に分割しました。
標準入力から一度fgets関数でchar配列に受け取り、atoi関数で文字列を整数に変換しています。
また、fgets関数は失敗するとNULLを返すので、そこで強制終了をしています。

このプログラムで一番大事なのは、main関数中のfflush(stdout)です。
これを行わないと、出力が正しく行われず、このあと説明する親プロセスの方で、値を受け取ることが出来ず、エラーとなってしまいます。

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

親プロセスを作成する

親プロセスとなるプログラムを作成していきます。
ですが、その3から関数を変更したりしているのでそのあたりをゴメンナサイしながら解説していきます。

自作ライブラリpipelib

今後のプログラム作成を楽にするために、一部の関数をライブラリ化してしまいます。
記事執筆が終わったら、まとめてUPする予定です。

このライブラリは、「pipelib.h」と「pipelib.c」の2つのファイルからなります。
中身を見ていきましょう。まずはpipelib.hからです。

pipelib.h

#define R 0
#define W 1

int createPipe(HANDLE *readPipe, HANDLE *writePipe, BOOL readInherit, BOOL writeInherit);
int easyCreateProcess(LPTSTR commandLine, STARTUPINFO *si, PROCESS_INFORMATION *pi);

これだけです。その3で作った定数とcreatePipe関数、そして今回新たに導入するeasyCreateProcess関数のプロトタイプ宣言をしているだけです。
この関数の定義をpipelib.cの方でしています。

pipelib.c

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

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

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

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

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

  return 1;
}

int easyCreateProcess(LPTSTR commandLine, STARTUPINFO *si, PROCESS_INFORMATION *pi)
{
  BOOL bInheritHandles = TRUE;
  DWORD creationFlags = 0;
  return CreateProcess(
                NULL,
                commandLine,
                NULL,	//プロセスのセキュリティー記述子
                NULL,	//スレッドのセキュリティー記述子
                bInheritHandles,
                creationFlags,
                NULL,	//環境変数は引き継ぐ
                NULL,	//カレントディレクトリーは同じ
                si,
                pi);
}

その3から、createPipeの返り値を変更しました。
その3では、成功で0、失敗で0以外を返していましたが、元のCreatePipe関数やDuplicateHandle関数では、成功が0以外、失敗が0だったので、こちらに準拠しました。
その3のサンプルプログラムと、結果の真偽値が逆になります。

easyCreateHandle関数は、その名の通り、CreateHandle関数を簡単に扱うためのラッパー関数です。
プロセス名と2つの構造体だけを指定すればハンドルが作成できます。
返り値はCreateHandle関数と同じです。

main関数

これらのpipelibを使って、親プログラムを実装します。
例によってまたmain関数からです。

#include <windows.h>
#include <stdio.h>
#include <string.h>
#include "pipelib.h"

int execute();

int main(void) {

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

  return r;
}

pipelib.hやstring.hをインクルードしています。
string.hは、execute関数内でstrlen関数を使用しているからです。

execute関数

execute関数を確認していきます。
やはりある程度は長いので、前後半に分けて見ていくことにします。

int execute() {

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

  HANDLE childProcess;

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

  int loop = 0;
  int ans = 0;
  char str[16];
  char buf[128];
  DWORD numberOfBytesWritten;
  DWORD numberOfBytesRead;

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

  si.cb = sizeof(STARTUPINFO);
  si.dwFlags = STARTF_USESTDHANDLES;
  si.hStdInput = stdinPipe[R];
  si.hStdOutput = stdoutPipe[W];
  si.hStdError = GetStdHandle(STD_ERROR_HANDLE);

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

  if(!easyCreateProcess(commandLine, &si, &pi)) {
    fprintf(stderr, "easyCreateProcess\n");
    return -1;
  }
  childProcess = pi.hProcess;

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

  if(!CloseHandle(stdoutPipe[W]))
    fprintf(stderr, "CloseHandle(stdoutPipe[W])\n");
  stdoutPipe[W] = NULL;
  if(!CloseHandle(stdinPipe[R]))
    fprintf(stderr, "CloseHandle(stdoutPipe[W])\n");
  stdoutPipe[W] = NULL;

この部分は通信の前準備の部分です。
今までも同じことをずっとしてきているので、特に言うこともありません。
その3と比べて、createPipe関数の成功判定に「!」が付いているくらいです。
一応、していることを箇条書きすると、

となります。

続いて、後半部分を見ていきます。

  while(loop<5) {
    int d = (loop+3)*2;
    sprintf(str, "%d\n", d);
    if(!WriteFile(stdinPipe[W], str, strlen(str), &numberOfBytesWritten, NULL)) {
      fprintf(stderr, "WriteFile\n");
      return -1;
    }
    printf("\nP:send child\n");
    ans = 0;
    if(!ReadFile(stdoutPipe[R], buf, sizeof(buf)-1, &numberOfBytesRead, NULL)) {
      if (GetLastError() == ERROR_BROKEN_PIPE) {
        printf("broken pipe.\n");
        break;
      }else
        fprintf(stderr, "ReadFile\n");
    }
    buf[numberOfBytesRead] = '\0';
    printf("\nP:%s\n", buf);
    loop++;
  }

  if(!TerminateProcess(childProcess, 0)) {
    fprintf(stderr, "TerminateProcess(chileProcess)\n");
    return -1;
  }

  if(!CloseHandle(stdoutPipe[R])) {
    fprintf(stderr, "CloseHandle(stdoutPipe[R])\n");
    return -1;
  }
  stdoutPipe[R] = NULL;
  if(!CloseHandle(stdinPipe[W])) {
    fprintf(stderr, "CloseHandle(stdinPipe[W])\n");
    return -1;
  }
  stdinPipe[W] = NULL;

  return 0;
}

whileループの中が通信を行っている本体部分です。このループは5回で抜けるようになっています。
このループでは次のような処理を行っています。

  1. ループ回数で決まる値をWriteFile関数で子プロセスに渡す
  2. ReadFile関数で子プロセスから値が返ってくるまで待つ
  3. 送られてきたら、返ってきた値を出力して1に戻る

これを5回繰り返したらループを抜けます。

ループを抜けたら、TerminateProcess関数で、子プロセスを強制終了しています。
子プロセスを終わらせるにはこの関数を使います。

最後に、使用したパイプをクローズして終了です。

実行結果

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

parent start.
P:send child 6
child start.
P:recieve 60
P:send child 8
P:recieve 80
P:send child 10
P:recieve 100
P:send child 12
P:recieve 120
P:send child 14
P:recieve 140
parent end.

ちゃんと親子間通信で、やりたかったこと(子は親からもらった値の10倍を返す)が達成できています。
また、子プロセスが強制終了されているので、「child end.」の文字は出力されていません。

補足

前回:その2 パイプを使ってみる  次回:その5 タイムアウトと文字列区切り(予定)

コメント



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