윈도우즈 시스템 프로그래밍 8장 - 파이프 방식의 IPC
윈도우즈 시스템 프로그래밍이라는 책과 해당 책의 저자이신 윤성우님의 강의를 통해 공부한 내용을 정리하는 글입니다.
메일슬롯과 파이프
메일슬롯 | 이름없는 파이프 | 이름있는 파이프 | |
방향성 | 단방향, 브로드캐스팅 | 단방향 | 양방향 |
통신범위 | 제한없음 | 부모, 자식 프로세스 | 제한없음 |
이름이 있다는 의미는 구분 가능한 주소 정보가 있다는 뜻이다. 그러므로 서로 관계가 없는 프로세스들 사이에서도 데이터를 주고 받을 수 있다.
⦁ 메일슬롯 : 브로드캐스트 방식의 단방향 통신방식을 취하며, 메일슬롯에 할당된 주소를 기반으로 통신하기 때문에 관계없는 프로세스들 사이에서도 통신이 가능하다.
⦁ 이름없는 파이프(Anonymous Pipe) : 단방향 통신방식을 취하며, 파이프를 통해서 생성된 핸들을 기반으로 통신하기 때문에 프로세스들 사이에는 관계가 있어야만 한다.
⦁ 이름있는 파이프(Named Pipe) : 메일슬롯과 유사하다. 차이점은, 브로드캐스트 방식을 지원하지 않는 대신에 양방향 통신을 지원한다.
이름없는 파이프(Anonymous Pipe)
⦁ 이름없는 파이프를 생성하기 위한 함수 CreatePipe
BOOL CreatePipe(
PHANDLE hReadPipe,
PHANDLE hWritePipe,
LPSECURITY_ATTRIBUTES lpPipeAttributes,
DWORD nSize
);
// If the function fails, the return value is zero.
⦁ hReadPipe : 한쪽 끝에서는 데이터가 들어가고 다른 한쪽에서는 데이터가 나오는 원리이므로 두 개의 끝을 가지고 있다. 파이프 생성시 각각의 끝에 접근하기 위한 두 개의 핸들을 얻게 된다. 이 인자를 통해서는 데이터를 읽기 위한 파이프 끝에 해당하는 핸들을 얻게 된다.
⦁ hWritePipe : 다른 한쪽 끝에 해당하는 핸들을 얻게 된다. (데이터를 쓰기 위한)
⦁ lpPipeAttributes : 보안 관련 정보 전달
⦁ nSize : 파이프의 버퍼 사이즈를 지정하는 용도 사용된다. 그러나 이 값을 통해서 전달되는 값으로 무조건 버퍼의 크기가 형성되지 않는다. 그래도 전달되는 값이 크면 큰 버퍼가, 작으면 작은 버퍼가 생성된다. 0을 인자로 전달하면 디폴트 사이즈로 버퍼 크기가 결정된다.
이름없는 파이프의 특성을 파악하는 예제
⦁ anonymous_pipe.cpp
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
int _tmain(int argc, TCHAR* argv[])
{
HANDLE hReadPipe, hWritePipe;
TCHAR sendString[] = _T("anonymous pipe");
TCHAR recvString[100];
DWORD bytesWritten;
DWORD bytesRead;
CreatePipe(&hReadPipe, &hWritePipe, NULL, 0);
WriteFile(hWritePipe, sendString, lstrlen(sendString) * sizeof(TCHAR), &bytesWritten, NULL);
_tprintf(_T("string send: %s \n"), sendString);
ReadFile(hReadPipe, recvString, bytesWritten, &bytesRead, NULL);
recvString[bytesRead / sizeof(TCHAR)] = 0;
_tprintf(_T("string recv: %s \n"), recvString);
CloseHandle(hReadPipe);
CloseHandle(hWritePipe);
return 0;
}
⦁ 실행결과
파이프 생성 시 얻게 되는 입력용, 출력용 핸들을 하나의 프로세스 내에서 사용한 예제이다. 만약 자식 프로세스를 생성하여 핸들을 상속하면 부모 자식 프로세스간에 메시지 전송이 가능해진다.
이름있는 파이프(Named Pipe)
이름있는 파이프를 만드는 방법은 다음과 같다.
⦁ CreateNamedPipe 함수를 통해서 파이프를 생성한다.
⦁ 그리고 이 파이프를 연결 요청을 기다리는 파이프로 상태를 변경시키기 위해서 ConnectNamedPipe 함수를 호출한다.
⦁ 만들어 놓은 파이프에 연결하기 위해서 CreateFile 함수 호출을 통해 처리한다.
⦁ CreateNamedPipe 함수
HANDLE CreateNamedPipe (
LPCTSTR lpName,
DWORD dwOpenMode,
DWORD dwPipeMode,
DWORD nMaxInstances,
DWORD nOutBufferSize,
DWORD nInBufferSize,
DWORD nDefaultTimeOut,
LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
// If the function fails, the return value is INVALID_HANDLE_VALUE;
⦁ lpName : 파이프 이름을 지정한다.
⦁ dwOpenMode : 다음 셋 중 하나를 지정한다.
- PIPE_ACCESS-DUPLEX : 읽기, 쓰기가 모두 가능하도록 설정.
- PIPE_ACCESS_INBOUND : 읽기만 가능하다(CreateNamePipe 함수를 호출하는 입장에서)
- PIPE_ACCESS_OUTBOUND : 쓰기만 가능하다(CreateNamePipe 함수를 호출하는 입장에서)
⦁ dwPipeMode : 데이터 전송 타입, 데이터 수신 타입, 블로킹 모드를 설정한다.
⦁ nMaxInstance : 생성할 수 있는 파이프의 최대 개수 지정. 값의 범위는 1~PIPE_UNLIMITED_INSTANCES(255)인데, PIPE_UNLIMITED_INSTANCES가 전달된다고 해서 255개까지 생성 가능한 것이 아니라, 생성 가능한 최대 개수만큼 생성을 허용한다.
⦁ nOutBufferSize : 이름 있는 파이프의 출력 버퍼 사이즈 지정. 0 입력시 디폴트
⦁ nInBufferSize : 이름 있는 파이프의 입력 버퍼 사이즈 지정. 0 입력시 디폴트
⦁ nDefaultTimeOut : WaitNamedPipe 함수에 적용할 기본 만료 시간을 밀리 세컨드 단위로 지정
⦁ lpSecurityAttributes : 보안 속성 지정
⦁ dwPipeMode
아래와 같은 설정 값들이 OR 연산되어 전달된다.
⦁ 데이터 전송방식 : PIPE_TYPE_BYTE(바이트), PIPE_TYPE_MESSAGE(메시지)
바이너리 형태로 전송할 것이지, 메시지 방식(텍스트)으로 전송할 것인지 결정
⦁ 데이터 수신방식 : PIPE_READMODE_BYTE(바이트), PIPE_READMODE_MESSAGE(메시지)
바이너리(바이트 스트림) 방식으로 읽을 것인지, 메시지(텍스트) 방식으로 읽을 것인지 결정
⦁ 함수 리턴방식 : PIPE_WAIT(블로킹), PIPE_NOWAIT(넌-블로킹)
PIPE_NOWAIT은 호환성을 위해 제공되는 전달인자이므로 반드시 PIPE_WAIT으로 전달해야 한다.
예를들어 문자열을 주고 받는 것이라면 아래와 같이 조합하면 된다.
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT
바이너리 데이터를 주고 받는 것이라면 아래와 같이 조합하면 된다.
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT
⦁ ConnectNamedPipe 함수
CreateNamedPipe 함수를 통해 생성한 파이프를 연결 요청 대기 상태로 변경시킬 때 사용하는 함수이다.
BOOL ConnectNamedPipe (
HANDLE hNamedPipe,
LPOVERLAPPED lpOverlapped
);
// If the function fails, the return value is zero.
⦁ hNamedPipe : CreateNamedPipe 함수 호출을 통해서 생성한 파이프의 핸들 전달
⦁ lpOverlapped : 중첩 I/O를 위한 전달인자.
이름 있는 파이프 예제
클라이언트가 서버에 접속하고, 클라이언트는 파일 이름 하나를 메시지 형태로 전달하면 서버는 해당 파일을 열어서 파일의 전체 내용을 클라이언트에게 문자열로 전달한다. 즉, 텍스트 기반의 통신이 이루어지는 예제이다.
⦁ namedpipe_server.cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
#define BUF_SIZE 1024
int CommToClient(HANDLE);
int _tmain(int argc, TCHAR* argv[])
{
LPTSTR pipeName = _T("\\\;\.\\pipe\\simple_pipe");
HANDLE hPipe;
while (1)
{
hPipe = CreateNamedPipe(pipeName, PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, BUF_SIZE, BUF_SIZE, 20000, NULL);
if (hPipe == INVALID_HANDLE_VALUE)
{
_tprintf(_T("CreatePipe failed"));
return -1;
}
BOOL isSuccess = 0;
isSuccess = ConnectNamedPipe(hPipe, NULL) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);
if (isSuccess)
CommToClient(hPipe);
else
CloseHandle(hPipe);
}
return 1;
}
int CommToClient(HANDLE hPipe)
{
TCHAR fileName[MAX_PATH];
TCHAR dataBuf[BUF_SIZE];
BOOL isSuccess;
DWORD fileNameSize;
isSuccess = ReadFile(hPipe, fileName, MAX_PATH * sizeof(TCHAR), &fileNameSize, NULL);
if (!isSuccess || fileNameSize == 0)
{
_tprintf(_T("Pipe read message error! \n"));
return -1;
}
FILE* filePtr = _tfopen(fileName, _T("r"));
if (filePtr == NULL)
{
_tprintf(_T("File open fault! \n"));
return -1;
}
DWORD bytesWritten = 0;
DWORD bytesRead = 0;
while (!feof(filePtr))
{
bytesRead = fread(dataBuf, 1, BUF_SIZE, filePtr);
WriteFile(hPipe, dataBuf, bytesRead, &bytesWritten, NULL);
if (bytesRead != bytesWritten)
{
_tprintf(_T("Pipe wirte message error! \n"));
break;
}
}
FlushFileBuffers(hPipe);
DisconnectNamedPipe(hPipe);
CloseHandle(hPipe);
return 1;
}
⦁ namedpipe_client.cpp
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
#define BUF_SIZE 1024
int _tmain(int argc, TCHAR* argv[])
{
HANDLE hPipe;
TCHAR readDataBuf[BUF_SIZE + 1];
LPTSTR pipeName = _T("\\\\.\\pipe\\simple_pipe");
while (1)
{
hPipe = CreateFile(pipeName, GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hPipe != INVALID_HANDLE_VALUE)
break;
if (GetLastError() != ERROR_PIPE_BUSY)
{
_tprintf(_T("Could not open pipe \n"));
return 0;
}
if (!WaitNamedPipe(pipeName, 20000))
{
_tprintf(_T("Could not open pipe \n"));
return 0;
}
}
DWORD pipeMode = PIPE_READMODE_MESSAGE | PIPE_WAIT;
BOOL isSuccess = SetNamedPipeHandleState(hPipe, &pipeMode, NULL, NULL);
if (!isSuccess)
{
_tprintf(_T("SetNamePipeHandleState failed"));
return 0;
}
LPCTSTR fileName = _T("news.txt");
DWORD bytesWritten = 0;
isSuccess = WriteFile(hPipe, fileName, (_tcslen(fileName) + 1) * sizeof(TCHAR), &bytesWritten, NULL);
if (!isSuccess)
{
_tprintf(_T("WriteFile failed"));
return 0;
}
DWORD bytesRead = 0;
while (1)
{
isSuccess = ReadFile(hPipe, readDataBuf, BUF_SIZE * sizeof(TCHAR), &bytesRead, NULL);
if (!isSuccess && GetLastError() != ERROR_MORE_DATA)
break;
readDataBuf[bytesRead] = 0;
_tprintf(_T("%s \n"), readDataBuf);
}
CloseHandle(hPipe);
return 0;
}
⦁ WaitNamedPipe 함수
BOOL WaitNamedPipe (
LPCTSTR lpNamedPipeName,
DWORD nTimeOut
);
// If an instance of the pipe is not avilable before the time-out interval elapses, the return value is zero.
⦁ lpNamePipeName : 상태 확인의 대상이 되는 파이프 이름을 지정
⦁ nTimeOut : 타임아웃 시간 설정. NMPWAIT_WAIT_FOREVER로 설정하면 연결 가능 상태가 될 떄까지 기다리고, NMPWAIT_USE_DEFAULT_WAIT로 설정할 경우 디폴트로 정의된 시간만큼만 기다린다. 이 디폴트 시간은 서버에서 CreateNamedPipe 함수를 호출하면서 전달한 7번째 인자를 통해 결전된다.
⦁ PipeHandleState 함수
BOOL SetNamedPipeHandleState (
HANDLE hNamePipe,
LPDWORD lpMode,
LPDWORD lpMaxCollectionCount,
LPDWORD lpCollectDataTimeout
);
// If the function fails, the return value is zero.
⦁ hNamePipe : 파이프와의 연결 속성을 변경시키기 위한 핸들 지정
⦁ lpMode : 읽기 모드와 함수 리턴방식에 대한 값을 OR 연산하여 전달. CreateNamedPipe 함수의 전달 인자와 같은 이름으로 구성됨.
⦁ lpMaxCollectionCount : 서버로 데이터를 보내기에 앞서서 버퍼링할 수 있는 최대 바이트 크기를 지정하는데 사용. 클라이언트와 서버가 같은 PC상에서 동작한다면 NULL을 전달해야함.
⦁ lpCollectDataTimeout : 서버로 데이터를 보내기 앞서서 버퍼링을 허용하는 최대 시간을 지정하는데 사용. 클라이언트와 서버가 같은 PC상에서 동작한다면 NULL을 전달해야함.