WaitCommEvent
예제 프로그램에서는 UpdateCompletionPort 함수의 호출이 성공적으로 끝난 후에 클라이언트에서 보내는 데이터를 받기 위해서 WSARead 함수를 한번 호출한다. 참고로 다시 한번 이야기하자면 이 서버 프로그램은 에코우 서버이기 때문에 클라이언트가 보낸 데이터를 그대로 다시 클라이언트로 전송한다.
lpPerSocketContext = UpdateCompletionPort(sdAccept, ClientIoRead);
if (NULL == lpPerSocketContext)
{
CleanUp();
return 1;
}
// 소켓에 비동기 읽기를 수행한다.
nRet = WSARecv(sdAccept, &(lpPerSocketContext->pIOContext->wsabuf), 1,
&dwRecvNumBytes, &dwFlags,
&(lpPerSocketContext->pIOContext->Overlapped), NULL);
if (nRet == SOCKET_ERROR && (ERROR_IO_PENDING != WSAGetLastError()))
{
printf("WSARecv Failed: %d\n", WSAGetLastError());
CloseClient(lpPerSocketContext);
}
} //while
위의 WSARecv 함수 호출에서 6번째 인자를 눈여겨 보기 바란다. WSAOVERLAPPED 구조체의 변수를 지정하는데 PER_IO_CONTEXT의 Overlapped 필드를 넘기고 있다. 3>에서 설명한 것처럼 이는 사실 pIOContext의 주소를 넘기는 것과 동일한 효과를 갖는다.
아무튼 WSARecv로 인한 읽기 작업이 완료되면 이는 IOCP 큐에 들어간다. 이를 읽어들이는 작업은 앞에서 만든 스레드들에서 수행한다. 이 함수는 비동기 함수이기 때문에 바로 리턴하고 그리고나서 코드는 다시 while 루프로 선두로 가서 다른 클라이언트로부터의 연결을 대기한다.
while (g_bEndServer == FALSE)
{
// 클라이언트가 들어오기를 대기한다.
sdAccept = WSAAccept(g_sdListen, NULL, NULL, NULL, 0);
…
즉, main 함수는 초기화 작업을 하고 난 뒤부터는 클라이언트로부터의 소켓연결이 맺어지기를 기다렸다가 만들어지면 이를 IOCP와 연결한 뒤에 WSARecv를 한번 호출하는 일만 한다. 실제 작업은 모두 스레드에서 이루어진다.
참고로 WSASend와 WSARecv의 함수 원형을 살펴보자.
int WSARecv(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
int WSASend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent, DWORD dwFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
이 두 함수는 비슷한 인자를 많이 갖고 있다. 먼저 모두 첫번째 인자는 소켓 핸들이다. 두 번째 인자는 WSABUF라는 구조체에 대한 포인터로 보낼 데이터에 대한 정보이거나 데이터를 받을 버퍼에 대한 정보이다. WSABUF는 다음과 같이 버퍼의 시작 주소와 버퍼의 크기를 지정하는 두개의 필드로 구성되어 있다.
Typedef struct __WSABUF
{
u_long len; // 버퍼 크기
char FAR *buf; // 버퍼 시작 주소
} WSABUF, FAR *LPWASBUF;
이 두 번째 인자로는 WSABUF 배열의 주소를 지정할 수도 있다. 그 경우 차례로 여러 버퍼의 데이터를 전송하거나 (WSASend의 경우) 받은 데이터를 여러 버퍼로 옮기는 역할(WSARecv의 경우)을 한다. 세 번째 인자는 이 두 번째 인자가 가리키는 WSABUF 변수의 수를 나타낸다. 배열을 지정했을 경우에는 그 크기를 이 인자로 지정해주면 된다. 배열이 아니라면 그냥 1을 지정하면 된다. 여기서 한가지 알아야 할 점은 이 두 함수 모두 지정한 크기만큼 입출력이 종료된 다음에 리턴되는 것이 아니란 점이다. WSARecv 같은 경우에는 읽어올 데이터가 생기면 지정된 크기와 관계없이 바로 작업을 종료한다. WSASend의 경우에는 소켓 버퍼가 꽉 차서 데이터를 지정된 크기만큼 보낼 수 없으면 일단 보낼 수 있는 만큼 보내고 만다.
네 번째 인자는 각기 실제로 전송된 데이터(WSASend의 경우)와 실제로 읽어들인 데이터(WSARecv의 경우)의 크기가 들어간다. 그런데 이 함수들을 예제 프로그램에서처럼 비동기 모드로 사용할 경우에는 이 인자로 리턴되는 값은 함수 자체의 리턴값이 0인 경우에만 의미가 있다. 0인 경우는 바로 작업이 끝난 경우이다. 함수가 바로 끝나지 않을 경우에는 SOCKET_ERROR가 리턴되고 이 때 GetLastError 함수를 호출해보면 그 값이 WSA_IO_PENDING일 것이다.
다섯 번째 인자는 약간 복잡한데 일단 대부분 0이 리턴되거나 (WSARecv의 경우) 0이 지정(WSASend의 경우)된다고 알아두기 바란다. 여섯 번째 인자는 WSAOVERLAPPED 구조체에 대한 포인터를 지정하는 부분이다. IOCP를 사용하는 경우에는 hEvent 필드의 값은 반드시 NULL이 지정되어야 한다. 마지막 인자는 콜백함수를 지정하는데 사용된다. 이 콜백함수의 원형은 다음과 같다.
void CALLBACK CompletionROUTINE(DWORD dwError, DWORD cbTransferred,
LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags);
만일 여섯 번째 인자와 마지막 인자가 모두 NULL이면 이 함수들은 동기 모드로 동작한다. 여섯 번째 인자와 마지막 인자가 모두 지정되면 작업이 종료되었을 때 마지막 인자로 지정된 함수가 호출된다. 여섯 번째 인자만 지정되고 첫 번째 인자로 지정된 소켓이 IOCP와 연결되어 있으면 이 함수의 결과는 IOCP 큐에 들어간다. 사실 이 두 함수의 인자들을 제대로 이해할 수 있다면 윈도우 운영체제의 입출력 함수는 다 이해했다고 봐도 무방하다.
5> 비동기 I/O 결과 읽기
앞서 수행된 비동기 I/O의 결과를 읽어들이려면 GetQueuedCompletionPort라는 함수를 이용해야 한다. 이 함수 원형에 대한 설명은 참고 3에 있다. 이 함수는 IOCP 큐안에 읽어들일 비동기 I/O 결과가 있으면 이를 읽어가지고 리턴한다. 읽어올 것이 없으면 읽어올 것이 생길 때까지 리턴하지 않는다. 다음 코드처럼 이 함수는 무한루프안에서 계속적으로 호출되는 것이 일반적이다.
While (1)
{
GetQueuedCompletionStatus(…);
// 읽어들인 결과를 바탕으로 다음 일을 수행한다.
…
}
예제 프로그램과 같은 에코우 서버에서는 특정 소켓에 대해 읽기 작업이 완료된 결과를 읽어들였으면 이를 비동기로 쓰는 작업을 하고, 쓰기 작업이 완료된 결과를 읽어들였으면 다시 비동기로 읽기 작업을 수행한다. 앞서 이야기한 것처럼 GetQueuedCompletionPort 함수의 세 번째 인자로는 현재 이 소켓에 대해 따로 할당된PER_SOCKET_CONTEXT 구조체의 포인터가 리턴되고 이 것의 pIOContext 필드를 보면 현재 진행중인 작업의 상태를 알 수 있다. pIOContext의IOOperation 필드의 값이ClientIoRead이면 지금 큐에서 읽어온 작업이 읽기 작업의 결과인 것이고 ClientIoWrite이면 쓰기 작업인 것이다.
위의 코드를 좀더 예제 프로그램에 맞게 고쳐보면 다음과 같은 식이다.
While (1)
{
GetQueuedCompletionStatus(…);
// 읽어들인 결과를 바탕으로 다음 일을 수행한다.
만일 읽어들인 결과가 읽기 작업이면
읽어들인 데이터를 그대로 다시 서버로 보낸다 (물론 비동기 I/O)
만일 읽어들인 결과가 쓰기 작업이면
만일 앞서 쓰기 요청한 것이 다 전송되지 않았으면
전송안 된 부분만 다시 전송한다
다 전송되었으면
읽기 비동기 작업을 소켓에 수행한다.
}
참고 3. GetQueuedCompletionStatus
이 함수의 원형은 다음과 같다.
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,
LPDWORD lpNumberOfBytes,
PULONG_PTR lpCompletionKey,
LPOVERLAPPED *lpOverlapped,
DWORD dwMilliseconds);
첫 번째 인자인 CompletionPort로는 앞서 생성된 IOCP 객체의 핸들을 지정한다.
두 번째 인자로는 지금 읽어온 I/O 작업의 결과로 읽거나 쓴 데이터의 크기가 바이트 단위로 지정된다. 즉 이 인자의 값은 운영체제에서 지정한다.
세 번째 인자인 lpCompletionKey역시 운영체제에 의해 채워져 리턴되는 값이다. CreateIoCompletionPort 함수로 IOCP 객체를 생성할 때 세 번째 인자로 지정한 값이 여기로 리턴된다. 앞서 이야기한 것처럼 한 IOCP 객체로 둘 이상의 입출력 디바이스를 처리할 수 있기 때문에 이를 구분하는 값이 여기로 지정된다고 생각하면 된다.
네 번째 인자인 lpOverlapped 역시 운영체제에 의해 값이 지정되는데 이는 한 입출력 디바이스내에서 각각의 입출력 작업을 구별하는 역할을 한다. 이 값은 사실 앞서 비동기 작업에서 사용된 OVERLAPPED 구조체의 주소가 그대로 들어온다. 그렇기 때문에 비동기 I/O 작업시에 OVERLAPPED 구조체를 스택에 있는 것을 사용하면 안 되고 각 작업마다 서로 다른 OVERLAPPED 구조체가 사용되어야 하는 것이다.
마지막 인자인dwMilliseconds는 IOCP 큐에 결과가 없을 경우 얼마나 더 대기하다가 리턴할 것인지를 밀리세컨드 단위로 지정한다. 만일 타임아웃이 나서 리턴할 경우에는 GetQueuedCompletionStatus 함수의 리턴값은 FALSE가 되고 네 번째인자로는 NULL이 지정된다. 읽어올 것이 생길 때까지 대기하도록 하고 싶으면 이 인자로 INFINITE를 지정하면 된다.
위의 플로우를 염두에 두고 이제 예제 프로그램의 스레드 코드를 실제로 살펴보자. 주석을 자세히 달아놓았으므로 주석과 함께 코드를 살펴보기 바란다.
DWORD WINAPI EchoThread (LPVOID WorkThreadContext)
{
// 앞서 스레드 생성시 스레드 함수의 인자로 IOCP 핸들을 지정했었다.
// 인자를 IOCP 핸들로 캐스팅한다.
HANDLE hIOCP = (HANDLE)WorkThreadContext;
BOOL bSuccess = FALSE;
int nRet;
LPOVERLAPPED lpOverlapped = NULL;
PPER_SOCKET_CONTEXT lpPerSocketContext = NULL;
PPER_IO_CONTEXT lpIOContext = NULL;
WSABUF buffRecv;
WSABUF buffSend;
DWORD dwRecvNumBytes = 0;
DWORD dwSendNumBytes = 0;
DWORD dwFlags = 0;
DWORD dwIoSize;
while (TRUE)
{
// IOCP 큐에서 비동기 I/O 결과를 하나 읽어온다.
bSuccess = GetQueuedCompletionStatus(hIOCP, &dwIoSize,
(LPDWORD)&lpPerSocketContext, &lpOverlapped,INFINITE);
if (!bSuccess)
printf("GetQueuedCompletionStatus: %d\n", GetLastError());
// CleanUp 함수에 의해서 스레드의 강제 종료 명령이 내려지면..
if (lpPerSocketContext == NULL) return 0;
if (g_bEndServer) return 0;
// 클라이언트와의 소켓 연결이 끊어졌으면…
if (!bSuccess || (bSuccess && (0 == dwIoSize)))
{
// lpPerSocketContext를 메모리에서 제거한다.
CloseClient(lpPerSocketContext);
continue;
}
/* 앞서 WSASend와 WSARecv에 의해 I/O 작업을 할 때 넘겼던 WSAOVERLAPPED
타입의 변수가 사실은 PER_IO_CONTEXT 타입의 시작이기도 하므로 이를 캐스팅하
여 사용가능하다. */
lpIOContext = (PPER_IO_CONTEXT)lpOverlapped;
switch (lpIOContext->IOOperation) // 끝난 작업 종류가 무엇인가 ?
{
case ClientIoRead: // 읽기 작업인가 ?
// --------------------------------------------
// 받은 것을 그대로 보낸다. 즉, 다음 작업은 쓰기 작업이다.
// --------------------------------------------
printf("%s를 받았고 이를 재전송합니다.\n.", lpIOContext->wsabuf.buf);
lpIOContext->IOOperation = ClientIoWrite; // 이제 쓰기 작업이 진행됨을 표시
// 얼마큼 전송할 것인지 명시한다. 받은 만큼 보낸다. 이는 상태를 기록하기
// 위함이지 WSASend 함수와는 관련없다.
lpIOContext->nTotalBytes = dwIoSize;
// 전송된 데이터 크기. 아직 보낸 것이 없으므로 0
lpIOContext->nSentBytes = 0;
// WSASend에게 보낼 데이터의 포인터와 크기를 지정한다.
// 받은 데이터가 이미 lpIOContext->wsabuf.buf에 있다.
lpIOContext->wsabuf.len = dwIoSize; // 크기 지정
dwFlags = 0;
nRet = WSASend(lpPerSocketContext->Socket,
&lpIOContext->wsabuf, 1, &dwSendNumBytes,
dwFlags, &(lpIOContext->Overlapped), NULL);
if (SOCKET_ERROR == nRet && (ERROR_IO_PENDING != WSAGetLastError()))
{
printf("WSASend: %d\n", WSAGetLastError());
CloseClient(lpPerSocketContext);
}
break;
case ClientIoWrite: // 쓰기 작업인가 ?
// ----------------------------------------------------
// 전송이 다 되었는지 확인한다. 다 전송되지 않았으면 아직 전송되지
// 않은 데이터를 다시 보낸다. 다 전송되었으면 WSARecv를 호출해서
// 다시 받기 모드로 진입한다.
// --------------------------------------------
lpIOContext->nSentBytes += dwIoSize; // 전송된 데이터 크기 업데이트
dwFlags = 0;
if (lpIOContext->nSentBytesnTotalBytes) // 다 전송되지 않았으면
{
// 마저 전송해야 하므로 아직 보내기모드
lpIOContext->IOOperation = ClientIoWrite;
// -----------------------
// 전송되지 않은 부분을 보낸다.
// -----------------------
// 버퍼 포인터를 업데이트하고
buffSend.buf = lpIOContext->Buffer + lpIOContext->nSentBytes;
// 보내야할 데이터의 크기를 남은 데이터의 크기만큼으로 줄인다.
buffSend.len = lpIOContext->nTotalBytes - lpIOContext->nSentBytes;
nRet = WSASend (lpPerSocketContext->Socket,
&buffSend, 1, &dwSendNumBytes,
dwFlags, &(lpIOContext->Overlapped), NULL);
// SOCKET_ERROR가 리턴된 경우에는 반드시 WSAGetLastError의 리턴값이
// ERROR_IO_PENDING이어야 한다.
if (SOCKET_ERROR == nRet && (ERROR_IO_PENDING != WSAGetLastError()))
{
printf ("WSASend: %d\n", WSAGetLastError());
CloseClient(lpPerSocketContext);
}
}
else // 데이터가 전부 전송된 경우
{
// 다시 이 소켓으로부터 데이터를 받기 위해 WSARecv를 호출한다.
lpIOContext->IOOperation = ClientIoRead;
dwRecvNumBytes = 0;
dwFlags = 0;
buffRecv.buf = lpIOContext->Buffer; // 수신버퍼 지정
// 읽어들일 데이터 크기 지정. 사실 이 크기만큼 데이터를 읽어들여야
// 그 결과가 IOCP큐에 들어가는 것은 아니다. 이 크기 이상 안
// 읽어들일 뿐이고 데이터가 이용가능한 만큼 IOCP큐에 넣는다.
buffRecv.len = MAX_BUFF_SIZE;
nRet = WSARecv(lpPerSocketContext->Socket,
&buffRecv, 1, &dwRecvNumBytes,
&dwFlags, &(lpIOContext->Overlapped), NULL);
// SOCKET_ERROR가 리턴된 경우에는 반드시 WSAGetLastError의 리턴값이
// ERROR_IO_PENDING이어야 한다.
if (SOCKET_ERROR == nRet && (ERROR_IO_PENDING != WSAGetLastError()))
{
printf ("WSARecv: %d\n", WSAGetLastError());
CloseClient(lpPerSocketContext);
}
}
break;
} //switch
} //while
return(0);
}
자 이상으로 IOCP가 어떤 식으로 동작하는지 알아보았다. 단계별로 설명과 코드를 잘 살펴보면 어떻게 동작하는지 더 쉽게 이해할 수 있을 것이다.
3. 예제 프로그램의 기타 코드 설명
예제 프로그램에서 설명이 안 된 코드는 서버와 연결된 클라이언트의 리스트를 관리하는 함수들(CtxtAllocate, CtxtListFree, CtxtListAddTo, CtxtListDeleteFrom)과 청소 함수(CleanUp, CloseClient), 대기 소켓 생성함수(CreateListenSocket)등이다. 대기 소켓 생성 함수는 이미 지난 연재에서 살펴본 내용(사실 socket 대신에 WSASocket을 호출하는 부분만 다르다)이기 때문에 여기서는 다른 함수들에 대해서만 알아보겠다.
클라이언트 리스트 관리 함수들
접속하는 클라이언트가 생길 때마다 이는g_CtxtList에 기록된다. 이는CptrList 타입의 링크드 리스트 클래스이고 이 변수로의 접근은 모두g_CriticalSection이란 크리티컬 섹션에 의해 한번에 한 스레드로 제한된다.
CtxtAllocate는 인자로 지정된 소켓에 PER_SOCKET_CONTEXT 구조체를 하나 할당하고 그 구조체를 초기화한 다음에 이를 리턴한다. 할당에 실패하면 NULL을 리턴한다. PER_SOCKET_CONTEXT 구조체의 IO_PER_CONTEXT 타입의 필드인 pIOContext의 필드를 초기화하는 부분을 눈여겨 봐두기 바란다.
PPER_SOCKET_CONTEXT CtxtAllocate(SOCKET sd, IO_OPERATION ClientIO)
{
PPER_SOCKET_CONTEXT lpPerSocCon;
lpPerSocCon = (PPER_SOCKET_CONTEXT)malloc(sizeof(PER_SOCKET_CONTEXT));
if (lpPerSocCon)
{
lpPerSocCon->pIOContext = (PPER_IO_CONTEXT)
malloc(sizeof(PER_IO_CONTEXT));
if (lpPerSocCon->pIOContext)
{
lpPerSocCon->Socket = sd;
memset(&lpPerSocCon->pIOContext->Overlapped,
0, sizeof(OVERLAPPED));
lpPerSocCon->pIOContext->IOOperation = ClientIO;
lpPerSocCon->pIOContext->nTotalBytes = 0;
lpPerSocCon->pIOContext->nSentBytes = 0;
lpPerSocCon->pIOContext->wsabuf.buf = lpPerSocCon->pIOContext->Buffer;
lpPerSocCon->pIOContext->wsabuf.len = MAX_BUFF_SIZE;
}
else
{
free(lpPerSocCon);
lpPerSocCon = NULL;
}
}
return(lpPerSocCon);
}
나머지 세 함수들은 간단하다. CptrList 클래스를 사용해본 이라면 이 함수들의 소스를 이해하기가 아주 쉬울 것이다. 여기서는 CtxtListAddTo와 CtxtListDeleteFrom 함수만 살펴보겠다.
// g_CtxtList에 lpPerSocketContext가 가리키는 항목을 추가한다
VOID CtxtListAddTo (PPER_SOCKET_CONTEXT lpPerSocketContext)
{
EnterCriticalSection(&g_CriticalSection);
g_CtxtList.AddTail(lpPerSocketContext); // 리스트의 끝에 붙인다.
LeaveCriticalSection(&g_CriticalSection);
return;
}
// g_CtxtList에서 lpPerSocketContext가 가리키는 항목을 제거한다.
VOID CtxtListDeleteFrom(PPER_SOCKET_CONTEXT lpPerSocketContext)
{
EnterCriticalSection(&g_CriticalSection);
if (lpPerSocketContext)
{
POSITION pos = g_CtxtList.Find(lpPerSocketContext);
if (pos)
{
g_CtxtList.RemoveAt(pos);
if (lpPerSocketContext->pIOContext)
free(lpPerSocketContext->pIOContext);
free(lpPerSocketContext);
}
}
LeaveCriticalSection(&g_CriticalSection);
return;
}
청소 함수들
여기서는 CleanUp 함수의 코드를 보기로 하겠다. 이 함수를 프로그램이 종료될 때 호출되는 함수로 모든 스레드가 종료되기를 기다렸다가 클라이언트 리스트에 할당되었던 자료구조들을 제거하고 최종적으로 IOCP와 대기 소켓을 제거하는 일을 수행한다.
void CleanUp()
{
if (g_hIOCP)
{
// 스레드를 강제 종료하도록 한다.
// 참고 4와 EchoThread의 if (lpPerSocketContext == NULL)를 같이 보기 바란다.
for (DWORD i = 0; i < g_dwThreadCount; i++)
PostQueuedCompletionStatus(g_hIOCP, 0, 0, NULL);
}
// 모든 스레드가 실행을 중지했는지 확인한다.
if (WAIT_OBJECT_0 != WaitForMultipleObjects( g_dwThreadCount, g_hThreads,
TRUE, 1000))
printf("WaitForMultipleObjects failed: %d\n", GetLastError());
else
for (DWORD i = 0; i < g_dwThreadCount; i++) // 스레드 핸들을 모두 닫는다.
{
if (g_hThreads[i] != INVALID_HANDLE_VALUE) CloseHandle(g_hThreads[i]);
g_hThreads[i] = INVALID_HANDLE_VALUE;
}
// g_CtxtList에 들어있는 클라이언트들을 모두 제거한다.
CtxtListFree();
// IOCP를 제거한다.
if (g_hIOCP)
{
CloseHandle(g_hIOCP);
g_hIOCP = NULL;
}
// 대기 소켓을 제거한다.
if (g_sdListen != INVALID_SOCKET)
{
closesocket(g_sdListen);
g_sdListen = INVALID_SOCKET;
}
DeleteCriticalSection(&g_CriticalSection); // 크리티컬 섹션을 제거한다.
WSACleanup(); // 윈속 라이브러리를 해제한다.
}
참고 4. PostQueuedCompletionPort
앞에서 설명한 것처럼 이 함수는 IOCP 큐에 마치 비동기 작업이 끝나서 그 결과가 큐에 들어가는 것처럼 흉내내는 기능을 한다. 그렇기 때문에 이 함수의 인자들을 보면 GetQueuedCompletionStatus 함수에 있는 것과 동일하다. 이 함수의 원형은 다음과 같다.
BOOL PostQueuedCompletionStatus(
HANDLE CompletionPort,
DWORD dwNumberOfBytesTransferred,
ULONG_PTR dwCompletionKey,
LPOVERLAPPED lpOverlapped);
첫 번째 인자인 CompletionPort로는 지금 만들어내는 I/O 작업의 결과가 들어갈 IOCP 객체의 핸들을 지정한다.
두 번째 인자인 dwNumberOfBytesTransferred는 GetQueuedCompletionStatus 함수의 두 번째 인자로 넘어갈 값을 지정한다.
세 번째 인자인 dwCompletionKey는 두 번째 인자와 마찬가지로 GetQueuedCompletionStatus 함수의lpCompletionKey 인자로 들어갈 값을 지정하는데 사용된다.
네 번째 인자인 lpOverlapped는 앞서 인자들과 마찬가지로 GetQueuedCompletionStatus 함수의 네 번째 인자로 들어갈 OVERLAPPED 구조체의 값을 넘긴다.
이 함수가 성공적으로 인자로 지정된 값들을 IOCP 큐에 넣으면 0이 아닌 값이 리턴된다. 실패시에는 0이 리턴되며 이 때는 GetLastError 함수를 호출해서 에러의 원인을 찾아볼 수 있다.
예제 프로그램의 실행 화면은 그림 2와 같다.
< 그림 2. 예제 프로그램의 실행화면 >
이 것으로 IOCP에 대한 장황한 설명을 마치겠다. 아마 이해하기가 그리 쉽지 않을 것이다. 필자의 경우에도 이를 이해하는데 상당한 시간을 소모했으며 위의 예제 프로그램을 바탕으로 실제 환경하에서 동작하는 프로그램을 만드는데도 상당한 시간을 보냈다. 이해하기는 어렵지만 IOCP는 스레드을 최대한으로 활용할 수 있도록 해주는 메커니즘이다. 특히 소켓으로 다중 사용자의 요구를 처리해야 하는 프로그램을 만들어야 한다면 IOCP는 최적의 솔루션이 아닌가 싶다.
참고문헌
1. INFO: Design Issues When Using IOCP in a Winsock Server (Q192800) - http://support.microsoft.com/default.aspx?scid=kb;EN-US;q192800
2. Programming Server-Side Applications for Microsoft Windows 2000, Chapter 2 Devico I/O and Interthreaded Communication
3. Writing Windows NT Server Applications in MFC Using I/O Completion Ports - http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpic/html/msdn_servrapp.asp
4. UNBUFCPY, SOCKSRV – Microsoft Platform SDK IOCP 윈속 예제 프로그램
5. Windows Sockets 2.0: Write Scalable Winsock Apps Using Completion Ports - http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnmag00/html/Winsock.asp