Chapter 22 - Overlapped IO 모델

22-1 : Overlapped IO 모델의 이해

IO의 중첩 : 하나의 쓰레드 내에서 동시에 둘 이상의 영역으로 데이터를 송수신함으로써 입출력이 중첩되는 상황

  • 입출력 함수가 호출 즉시 반환해야함 - 비동기 IO가 필요
  • 따라서 입출력 함수는 논블로킹 모드로 동작해야함

Overlapped IO에서는 입출력 자체보다 입출력이 완료된 상황의 확인이 중요
Overlapped IO에 적합한 소켓을 생성하는데는 WSASocket() 함수를 사용

#include <winsock2.h>

SOCKET WSASocket(int af, int type, int protocol, LPWSAPROTOCOL_INFO lpProtocolInfo, GROUP g, DWORD dwFlags);
// 성공시 소켓 핸들, 실패시 INVALID_SOCKET 반환
  • af : 프로토콜 체계 정보
  • type : 소켓의 데이터 전송방식
  • protocol : 두 소켓 사이에 사용되는 프로토콜 정보
  • lpProtocolInfo : 생성되는 소켓의 특성을 정의하는 WSAPROTOCOL_INFO 구조체 변수의 주소값
  • g : 소켓 그룹 ID, 0 입력시 그룹 작업이 수행되지 않음
  • dwFlags : 소켓의 추가 속성정보, WSA_FLAG_OVERLAPPED 전달시 Overlapped IO를 지원하는 소켓을 생성


Overlapped IO에서 두 소켓간의 연결은 기존과 동일, 데이터의 입출력에 차이가 존재 Overlapped IO에서 데이터 송신에는 WSASend() 함수를 사용

#include <winsock2.h>

int WSASend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
// 성공시 0, 실패시 SOCKET_ERROR 반환

typedef struct __WSABUF
{
	u_long		len;	// 전송할 데이터 크기
	char FAR	*buf;	// 버퍼 주소값
} WSABUF, *LPWSABUF;

typedef struct _WSAOVERLAPPED
{
	DWORD	Internal;
	DWORD	InternalHigh;
	DWORD	Offset;
	DWORD	OffsetHigh;
	WSAEVENT	hEvent;
} WSAOVERLAPPED, *LPWSAOVERLAPPED;
  • lpBuffers : 전송할 데이터 정보를 가진 WSABUF 구조체 배열의 주소값
  • dwBufferCount : lpBUffers로 전달된 배열의 길이
  • lpNumberOfBytesSent : 전송될 바이트 수가 저장될 변수의 주소값
  • dwFlags : 함수의 데이터 전송특성 지정
  • lpOverlapped : WSAOVERLAPPED 구조체 변수의 주소값, Event 오브젝트를 사용해 데이터 전송의 완료를 확인하는데 사용
  • lpCompletionRoutine : 데이터 전송의 완료를 확인하기 위한 Completion Routine 함수의 주소값 전달

Overlapped IO를 진행하기 위해서는 WSASend() 함수의 매개변수 lpOverlapped의 값이 항상 유효한 구조체 변수의 주소값이여야함
또한 WSASend() 함수를 통해 동시에 둘 이상의 영역으로 데이터를 전송하는 경우 lpOverlapped 구조체 변수를 각각 별도로 구성해야함

WSASend() 함수를 사용한 데이터 전송 예시

WSAEVENT		event;
WSAOVERLAPPED	overlapped;
WSABUF			dataBuf;
char			buf[BUF_SIZE] = {"data to send"}
int				recvBytes = 0;
...
event = WSACreateEvent();
memset(&overlapped, 0, sizeof(overlapped));	// 모든 비트 0으로 초기화
overlapped.hEvent = event;
dataBuf.len = sizeof(buf);
dataBUf.buf = buf;
WSASend(hSocket, &dataBuf, 1, &recvBytes, 0, &overlapped, NULL);
...


전송하는 데이터의 크기가 크지 않은 경우 함수호출과 동시에 데이터 전송이 완료되어 WSASend() 함수는 0을 반환하고 lpNumberOfByteSent로 실제 전송된 데이터의 크기정보가 저장됨
반면 계속해서 데이터의 전송이 이루어진다면 WSASend() 함수는 SOCKET_ERROR를 반환하고 WSAGetLastError() 함수를 통해 확인가능한 오류코드에 WSA_IO_PENDING이 등록되며, WSAGetOverlappedResult() 함수를 통해 실제 전송된 데이터 크기를 확인해야함

#include <winsock2.h>

BOOL WSAGetOverlappedResult(SOCEKT s, LPWSAOVERLAPPED lpOverlapped, LPDWORD lpcbTransfer, BOOL fWait, LPDWORD lpdwFlags);
// 성공시 TRUE, 실패시 FALSE 반환
  • lpcbTransfer : 실제 송수신된 바이트 크기를 저장할 변수의 주소값
  • fWait : 여전히 IO가 진행중인 경우 - TRUE일시 완료될 때까지 대기, FALSE일시 즉시 FALSE를 반환
  • lpdwFlags : WSARecv() 함수 호출시 부수적인 정보를 얻기 위해 사용, 불필요할시 NULL


Overlapped IO에서 데이터 수신에는 WSARecv() 함수를 사용

#include <winsock2.h>

int WSARecv(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
// 성공시 0, 실패시 SOCKET_ERROR 반환

여러 데이터를 모아서 한번에 전송하고 수신된 데이터를 여러 버퍼에 나눠서 저장하는 것을 Gather/Scatter IO라고 함
리눅스에서는 writev(), readv() 함수가 이를 지원하고 윈도우에서는 WSASend(), WSARecv() 함수를 사용할 수 있음


22-2 : Overlapped IO에서의 입출력 완료의 확인

Event 오브젝트 기반 : lpOVerlapped 활용
CompetionRoutine 기반 : lpCompletionRoutine 활용

Event 오브젝트 활용 Sender 예시

// OverlappedSend_win.cpp

#include <iostream>
#include <winsock2.h>

using namespace std;

void Errorhandling(char *msg);

int main(int argc, char *argv[])
{
	WSADATA		wsaData;
	SOCKET		hSocket;
	SOCKADDR_IN	sendAdr;

	WSABUF		dataBuf;
	char		msg[] = "Network is Computer!";
	int			sendBytes = 0;

	WSAEVENT		evObj;
	WSAOVERLAPPED	overlapped;

	if(argc != 3)
	{
		cout << "Usage : " << argv[0] << " <IP> <port>\n";
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error");

	hSocket = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
	memset(&sendAdr, 0, sizeof(sendAdr));
	sendAdr.sin_family = AF_INET;
	sendAdr.sin_addr.s_addr = inet_addr(argv[1]);
	sendAdr.sin_port = htons(atoi(argv[2]));

	if (::connect(hSocket, (SOCKADDR*)&sendAdr, sizeof(sendAdr)) == SOCKET_ERROR)
		ErrorHandling("connect() error");

	evObj = WSACreateEvent();
	memset(&overlapped, 0, sizeof(overlapped));
	overlapped.hEvent = evObj;
	dataBuf.len = strlen(msg) + 1;
	dataBuf.buf = msg;

	if (WSASend(hSocket, &dataBuf, 1, &sendBytes, 0, &overlapped, NULL) == SOCKET_ERROR)
	{
		if (WSAGetLastError() == WSA_IO_PENDING)
		{
			cout << "Background data send\n";
			WSAWaitForMultipleEvents(1, &evObj, TRUE, WSA_INFINITE, FALSE);
			WSAGetOverlappedResult(hSocket, &overlapped, &sendBytes, FALSE, NULL);
		}
		else
			ErrorHandling("WSASend() error");
	}

	cout << "Send data size : " << sendBytes << endl;
	WSACloseEvent(evObj);
	closesocket(hSocket);
	WSACleanup();
	return 0;
}

void	ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}
  • IO가 완료되면 WSAOVERLAPPED 구조체 변수가 참조하는 Event 오브젝트가 signaled 상태가 됨
  • IO의 완료 및 결과 확인을 위해 WSAGetOverlappedResult() 함수를 사용

Event 오브젝트 활용 Receiver 예시

// OverlappedRecv_win.cpp

#include <iostream>
#include <winsock2.h>

using namespace std;

#define BUF_SIZE 1024
void Errorhandling(char *msg);

int main(int argc, char *argv[])
{
	WSADATA			wsaData;
	SOCKET			hLisnSock, hRecvSock;
	SOCKADDR_IN		lisnAdr, recvAdr;
	int				recvAdrSz;

	WSABUF			dataBuf;
	WSAEVENT		evObj;
	WSAOVERLAPPED	overlapped;

	char			buf[BUF_SIZE];
	int				recvBytes = 0, flags = 0;

	if(argc != 2)
	{
		cout << "Usage : " << argv[0] << " <port>\n";
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error");

	hLisnSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
	memset(&lisnAdr, 0, sizeof(lisnAdr));
	lisnAdr.sin_family = AF_INET;
	lisnAdr.sin_addr.s_addr = htonl(INADDR_ANY);
	lisnAdr.sin_port = htons(atoi(argv[1]));

	if (::bind(hLisnSock, (SOCKADDR*)&lisnAdr, sizeof(lisnAdr)) == SOCKET_ERROR)
		ErrorHandling("bind() error");

	if (::listen(hLisnSock, 5) == SOCKET_ERROR)
		ErrorHandling("listen() error");

	recvAdrSz = sizeof(recvAdr);
	hRecvSock = accept(hLisnSock, (SOCKADDR*)&recvAdr, &recvAdrSz);

	evObj = WSACreateEvent();
	memset(&overlapped, 0, sizeof(overlapped));
	overlapped.hEvent = evObj;
	dataBuf.len = BUF_SIZE;
	dataBuf.buf = msg;

	if (WSARecv(hRecvSock, &dataBuf, 1, &recvBytes, &flags, &overlapped, NULL) == SOCKET_ERROR)
	{
		if (WSAGetLastError() == WSA_IO_PENDING)
		{
			cout << "Background data receive\n";
			WSAWaitForMultipleEvents(1, &evObj, TRUE, WSA_INFINITE, FALSE);
			WSAGetOverlappedResult(hRecvSock, &overlapped, &recvBytes, FALSE, NULL);
		}
		else
			ErrorHandling("WSARecv() error");
	}

	cout << "Received message : " << buf << endl;
	WSACloseEvent(evObj);
	closesocket(hRecvSock);
	closesocket(hLisnSock);
	WSACleanup();
	return 0;
}

void	ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}


Completion Routine 방식 : Pending됭 IO 완료시 자동으로 호출된 함수를 등록

  • 단, IO를 요청한 쓰레드가 alertable wait 상태에 놓여있을 때만 루틴을 호출함
  • WaitForSingleObjectEx(), WaitForMultipleObjectsEx(), WSAWaitForMultipleEvents(), SleepEx() 함수 호출시 쓰레드가 alertable wait 상태가 됨
  • Competion Routine이 실행되면 위 함수들은 WAIT_IO_COMPLETION을 반환하면서 함수를 빠져나옴

Competion Routine 활용 Receiver 예시

// CmplRoutineRecv_win.cpp

#include <iostream>
#include <winsock2.h>

using namespace std;

#define BUF_SIZE 1024
void CALLBACK	CompRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void			ErrorHandling(char *msg);

WSABUF	dataBuf;
char	buf[BUF_SIZE];
int		recvBytes = 0;

int main(int argc, char *argv[])
{
	WSADATA			wsaData;
	SOCKET			hLisnSock, hRecvSock;
	SOCKADDR_IN		lisnAdr, recvAdr;

	WSAEVENT		evObj;
	WSAOVERLAPPED	overlapped;
	int				recvAdrSz, flags = 0;

	if(argc != 2)
	{
		cout << "Usage : " << argv[0] << " <port>\n";
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error");

	hLisnSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
	memset(&lisnAdr, 0, sizeof(lisnAdr));
	lisnAdr.sin_family = AF_INET;
	lisnAdr.sin_addr.s_addr = htonl(INADDR_ANY);
	lisnAdr.sin_port = htons(atoi(argv[1]));

	if (::bind(hLisnSock, (SOCKADDR*)&lisnAdr, sizeof(lisnAdr)) == SOCKET_ERROR)
		ErrorHandling("bind() error");

	if (::listen(hLisnSock, 5) == SOCKET_ERROR)
		ErrorHandling("listen() error");

	recvAdrSz = sizeof(recvAdr);
	hRecvSock = accept(hLisnSock, (SOCKADDR*)&recvAdr, &recvAdrSz);
	if (hRecvSock == INVALID_SOCKET)
		Errorhandling("accept() error");

	evObj = WSACreateEvent();	// Dummy event object
	memset(&overlapped, 0, sizeof(overlapped));
	dataBuf.len = BUF_SIZE;
	dataBuf.buf = msg;

	if (WSARecv(hRecvSock, &dataBuf, 1, &recvBytes, &flags, &overlapped, CompRoutine) == SOCKET_ERROR)
	{
		if (WSAGetLastError() == WSA_IO_PENDING)
			cout << "Background data receive\n";
	}

	idx = WSAWaitForMultipleEvents(1, &evObj, FALSE, WSA_INFINITE, TRUE);
	if (idx == WAIT_IO_COMPLETION)
		cout << "Overlapped I/O Completed\n";
	else	// if error occurred!
		ErrorHandling("WSARecv() error");

	WSACloseEvent(evObj);
	closesocket(hRecvSock);
	closesocket(hLisnSock);
	WSACleanup();
	return 0;
}

void CALLBACK	CompRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVErLAPPED lpOverlapped, DWORD flags)
{
	if (dwError != 0)
		ErrorHandling("CompRoutine error");
	else
	{
		recvBytes = szRecvBytes;
		cout << "Received message : " << buf << endl; 
	}
}

void	ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

내용 확인문제

  1. Asnchronous(비동기) Notification IO 모델과 Overlapped IO 모델 사이에서 비동기로 처리되는 영역이 어떻게 차이가 나는지 설명해보자.

    Notification IO 모델은 IO 관련 이벤트의 발생을 알리는 알림을 비동기로 처리하고, Overlapped IO 모델은 IO가 완료된 상황을 확인하는 과정을 비동기로 처리함

  2. 논-블로킹 IO, 비동기 IO 그리고 Overlapped IO의 관계를 하나의 문장으로 연결해서 설명해보자.

    비동기 IO는 IO가 완료된 상황을 확인하는 과정이 비동기로 처리됨을 의미하며, 이를 위해선 IO가 논블로킹 모드로 동작해야하며 이를 바탕으로 IO가 중첩된 형태인 Overlapped IO가 가능

  3. 다음 코드의 일부를 보면서 문제점이 있다면 어떠한 문제가 있는지 지적해보자. 그리고 이에대한 해결책도 함께 제시해보자.
     while (1)
     {
         hRecvSock = accept(hLisnSock, (SOCKADDR*)&recvAdr, &recvAdrSz);
         evObj = WSACreateEvent();
         memset(&overlapped, 0, sizeof(overlapped));
         overlapped.hEvent = evObj;
         dataBuf.len = BUF_SIZE;
         dataBuf.buf = buf;
         WSARecv(hRecvSock, &dataBuf, 1, &recvBytes, &flags, &overlapped, NULL);
     }
    

    참고로, 위의 코드가 완벽하진 않지만 이 코드만 가지고도 충분히 발견할 수 있는 구조적인 문제점이 있다.

    반복문을 통해 호출되는 WSARecv() 함수의 OVERLAPPED 구조체가 모두 동일한 변수인 것이 문제
    OVERLAPPED 구조체 변수를 동적할당하여 해결할 수 있음

  4. WSASend 함수호출 이후에 IO가 Pending된 상황과 그렇지 않은 상황을 확인하는 방법에 대해서 소스코드 수준에서 설명해보자.

    WSASend() 함수가 SOCKET_ERROR를 반환하고 WSAGetLastError() 함수가 WSA_IO_PENDING을 반환하는 경우

  5. 쓰레드의 alertable wait 상태는 어떠한 상태를 의미하는가? 그리고 쓰레드를 이 상태가 되도록 하는 함수들 중 두가지만 말해보자.

    쓰레드가 운영체제가 전달하는 메시지의 수신을 대기하는 상태로 Completion Routine을 호출해도 문제가 없음
    WaitForSingleObjectEx(), WaitForMultipleObjectsEx(), WSAWaitForMultipleEvents(), SleepEx()