トップC言語入門 > パイプによるプロセス間通信
Win32 APIの名前付きパイプによるプロセス間通信(サーバーC#、クライアントC言語)

1. プロセス間通信

複数のプロセス間でデータのやりとりをするための仕組みをプロセス間通信という。 たとえば、プログラムAがファイルに書き込む時間は毎時0分、30分、プログラムBがそれを 読み込む時間は毎時 15分、45分と決めておけば、書き込みと読み込みがぶつかることはまずありえない。 何ら特別な技法は用いないがこれもプロセス間通信と言える。

通常は任意の時間に相手にデータを渡したいため、これをサポートする何らかの手段が必要となる。

2. Windowsの名前付きパイプによるプロセス間通信

本ページでは、Windowsの名前付きパイプによるプロセス間通信を取り上げる。 UNIXにも名前付きパイプがあるが、内容が異なる。

文献[1]に分かりやすい例があるので、まず、これを実行してみよう。

サーバー側のプログラムを下に示す。名前付きパイプを生成して、それに接続して、クライアントからのデータを待っている。 クライアントからのデータを行単位で読み込んでいる。

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

int main() {
    HANDLE hPipe = CreateNamedPipe("\\\\.\\pipe\\mypipe", //lpName
        PIPE_ACCESS_DUPLEX,             // dwOpenMode
        PIPE_TYPE_BYTE | PIPE_WAIT,     // dwPipeMode
        3,                              // nMaxInstances
        0,                              // nOutBufferSize
        0,                              // nInBufferSize
        100,                            // nDefaultTimeOut
        NULL);                          // lpSecurityAttributes
    if (hPipe == INVALID_HANDLE_VALUE) {
        return 1;
    }
    if (!ConnectNamedPipe(hPipe, NULL)) {
        CloseHandle(hPipe);
        return 1;
    }
    while (1) {
        char szBuff[256];
        DWORD dwBytesRead;
        if (!ReadFile(hPipe, szBuff, sizeof(szBuff), &dwBytesRead, NULL)) {
            break;
        }
        szBuff[dwBytesRead] = '\0';
        printf("PipeServer: %s", szBuff);
    }
    FlushFileBuffers(hPipe);
    DisconnectNamedPipe(hPipe);
    CloseHandle(hPipe);
    return 0;
}

クライアント側のプログラムを下に示す。CreateFile関数により名前付きパイプに接続する。 WriteFile関数により行単位でデータをパイプに送り込んでいる。 全データを送り終わったら、CloseHandle関数によりパイプを閉じる。

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

int main() {
    HANDLE hPipe = CreateFile("\\\\.\\pipe\\mypipe",
        GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hPipe == INVALID_HANDLE_VALUE) {
        return 1;
    }
    while (1) {
        char szBuff[32];
        DWORD dwBytesWritten;
        fgets(szBuff, sizeof(szBuff), stdin);
        if (!WriteFile(hPipe, szBuff, strlen(szBuff), &dwBytesWritten, NULL)) {
            break;
        }
    }
    CloseHandle(hPipe);
    return 0;
}

クライアント側が複数行からなるひと塊のデータを送る方法は推測できたが、 上のサーバ側のプログラムは行単位の受信を繰り返すだけのプログラムとなっている。

クライアントから送られたデータには誤りがあるかも知れないので、 データの中身で通信の始まりや終わりを判断することはできない。


文献[2]では、パイプを作成し、 ConnectNamedPipe関数で一つの接続を待ち、接続が起きると、スレッドを生成して、本体は再び 同じ名前でパイプを作り、ConnectNamedPipe関数で別の接続を待っている。 前の接続が終わらないうちに次の接続が起きた場合、同時に二つのスレッドが動くことになる。

同時複数接続を考えないときには、スレッドの生成はいらない。

3. サーバーは C#、クライアントは C言語プログラムとする

TSPプログラムはC言語プログラムで作成しているが、ノードや経路の描画プログラムは C#言語で作成している。 C#でサーバーを記述するには次のようにすれば良いようだ。 C言語プログラムから経路などの情報を送り、C#プログラムで描画する。

        Task.Factory.StartNew(() => {
            while (true) {
                using (var server = new NamedPipeServerStream("tsp")) {
                    server.WaitForConnection();
                    StreamReader reader = new StreamReader(server);
                    while (server.IsConnected) {
                        var line = reader.ReadLine();
                        Console.WriteLine(line);
                    }
                }
            }
        });

4. プログラム事例[4]

C#で書いたサーバー側のプログラムを下に示す。アプリケーション処理も同じメソッドに含めたので 行数は増えたが、プロセス間通信特有の処理は少ない。 基本的にはNamedPipeServerStreamクラスで名前付きパイプ(名前は tspviewer)を開設し、 WaitForConnctionメソッドでクライアントが接続してくるのを待つ。 接続後の処理はテキストファイルの読み込みと全く同じである。

    void PipeThread() {
        while (true) {
            try {
                var server = new NamedPipeServerStream("tspviewer", PipeDirection.InOut, 3);
                History.AppendText("Wait For Connection\r\n");
                server.WaitForConnection();
                StreamReader reader = new StreamReader(server);
                while (server.IsConnected) {
                    var line = reader.ReadLine();
                    if (line == null) { Thread.Sleep(100); continue; }
                    History.AppendText(line + "\r\n");
                    char[] delim = new char[] { '\t', ' ', ',' };
                    string[] items = line.Split(delim, StringSplitOptions.RemoveEmptyEntries);
                    string command = items[0].ToLower();
                    if (command == "load" && items.Length > 1) {
                        LoadMap(items[1], items.Length > 2 ? items[2] : null);
                        Refresh();
                    } else if (command == "tour" && items.Length > 2) {
                        Tour = new int[items.Length-1];
                        for (int n = 0; n < Tour.Length; n++) {
                            Tour[n] = int.Parse(items[n+1]);
                        }
                        Refresh();
                    } else {
                        History.AppendText("コマンドエラー\r\n");
                    }
                }
                reader.Close();
                server.Dispose();
            } catch (Exception e) { 
                Console.WriteLine(e.Message); 
                History.AppendText("ERROR: " + e.Message + "\r\n");
            }
        }
   }

上のプログラムは実際のアプリケーションプログラムから抜粋したものであるが、 下のクライアント側のプログラムはテストプログラムである。

StartUpTSPViewerはサーバー側のアプリケーション TSPViewer が起動済みかどうかチェックして、 起動していなかった場合、このアプリを起動するものである。 この機能は必ずしも必要としないが、便宜を考えて用意したものである。

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

int StartUpTSPViewer() {
    PROCESS_INFORMATION pi = { 0 }; 
    STARTUPINFO si = { sizeof(STARTUPINFO) };
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32 pe32 = { sizeof(PROCESSENTRY32) };

    int fFound = Process32First(hSnapshot, &pe32);
    while (fFound) {
        if (strstr(pe32.szExeFile, "TSPViewer") != NULL) return 0;    // 起動済み
        fFound = Process32Next(hSnapshot, &pe32);
    }
    CloseHandle (hSnapshot) ;
    CreateProcess(NULL, "TSPViewer", NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS,
                            NULL, NULL, &si, &pi);
    return 1;
}

int main() {
    char szBuff[32], *pszCmd;
    DWORD dwBytesWritten;
    HANDLE hPipe;

    if (StartUpTSPViewer()) Sleep(1000);
    hPipe = CreateFile("\\\\.\\pipe\\tspviewer", GENERIC_READ | GENERIC_WRITE, 
                0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hPipe == INVALID_HANDLE_VALUE) return 1;
    pszCmd = "load tsplib/fl1400.tsp\n";// fl1400.lkh";
    WriteFile(hPipe, pszCmd, strlen(pszCmd), &dwBytesWritten, NULL);
    pszCmd = "tour 1,2,3,34,15,16,27\n";
    WriteFile(hPipe, pszCmd, strlen(pszCmd), &dwBytesWritten, NULL);
    printf("n=%d\n", dwBytesWritten);
    CloseHandle(hPipe);
    return 0;
}

main関数中の Sleep関数はおそらくいらない。このテストプログラムでは、TSPViewer起動直後に パイプに接続しようとしている。まだ、パイプが開設されない段階で接続を試みるとエラーになる。 これを避けるために 1秒間Sleepしている。

クライアント側のプログラムはファイルへの書き込みと全く同じである。

参考文献

[1] Win32API 名前付きパイプによるプロセス間通信 CreateNamedPipe
[2] プロセス間通信とバックドア
[3] 名前付きパイプを用いたプロセス間通信
[4] TSPの拠点と巡回路の描画