윈도우즈 시스템 프로그래밍

윈도우즈 시스템 프로그래밍 8장 - 파이프 방식의 IPC

111-000-111 2021. 7. 29. 19:51

 

 

윈도우즈 시스템 프로그래밍이라는 책과 해당 책의 저자이신 윤성우님의 강의를 통해 공부한 내용을 정리하는 글입니다.

 

 

 


 

 

 

메일슬롯과 파이프

 

  메일슬롯 이름없는 파이프 이름있는 파이프
방향성 단방향, 브로드캐스팅 단방향 양방향
통신범위 제한없음 부모, 자식 프로세스 제한없음

 

이름이 있다는 의미는 구분 가능한 주소 정보가 있다는 뜻이다. 그러므로 서로 관계가 없는 프로세스들 사이에서도 데이터를 주고 받을 수 있다.

 

메일슬롯 : 브로드캐스트 방식의 단방향 통신방식을 취하며, 메일슬롯에 할당된 주소를 기반으로 통신하기 때문에 관계없는 프로세스들 사이에서도 통신이 가능하다.

이름없는 파이프(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을 전달해야함.