책정리/열혈 TCP,IP

21장 Asynchronous Notification IO모델

GONII 2015. 8. 5. 16:19
  • 비동기(Asynchronous) Notification IO 모델의 이해

    동기(Synchronous)와 비동기(Asynchronous)에 대한 이해

    비동기란 '일치하지 않음'을 뜻하는데, 이러한 비동기의 상황은 데이터의 입출력 과정에서도 매우 유용하게 활용된다. 지금까지는 send, recv 함수를 통해 동기화된 입출력을 진행하였다. send 함수가 호출되면 데이터의 전송이 완료된 후에야 반환이 이뤄지고 recv 함수가 호출되면 원하는 만큼 데이터를 읽어 들인 후에야 반환이 이뤄지기 때문에 동기화된 입출력을 진행한 셈이다.

    "send 함수가 호출되는 순간부터 데이터의 전송이 시작되고, send 함수의 호출이 완료(반환)되는 순간 데이터의 전송이 완료된다"

    "recv 함수가 호출되는 순간부터 데이터의 수신이 시작되고, recv 함수의 호출이 완료(반환)되는 순간 데이터의 수신이 완료된다"

    비동기 입출력이란 함수의 반환시점과 데이터의 송수신의 완료시점이 일치하지 않는 경우를 뜻한다.

    동기화된 입출력의 단점과 비동기의 해결책

    비동기 입출력은 동기 입출력의 단점을 극복하기 위해서 디자인 된 모델이다.

    동기호ㅘ 된 입출력의 단점은 "입출력이 진행되는 동안 호출된 함수가 반환을 하지 않으니, 다른 일을 할 수가 없다"이다.

    비동기 방식은 동기 방식에 비해 효율적으로 CPU를 활용하는 모델이 된다.

    비동기 Notification 입출력 모델에 대한 이해

    "입력버퍼에 데이터가 수신되어서 데이터의 수신이 필요하거나, 출력버퍼가 비어서 데이터의 전송이 가능한 상황의 알림"

    Notification IO란, IO와 관련해서 특정 상황이 발생했음을 알리는 것을 뜻한다. 가장 대표적인 Notification IO모델은 select모델이다.

    select 함수는 호출된 함수의 '반환'이라는 과정을 통해서 IO가 필요한, 또는 가능한 상황을 알린다. 그런데 이 알림이 이뤄지는 방식은 동기화되어 있다. 그리고 이렇게 말하는 이유는 다음과 같다

    "IO가 필요한, 또는 가능한 상황이 되는 시점이 select함수가 반환하는 시점과 일치한다"

    select 함수처럼 IO가 필요한, 또는 가능한 상황에서 반환이 이뤄지는 것이 아니라, IO의 상태에 상관없이 반환이 이뤄지는 방식이 '비동기 Notification IO'모델이다.

    비동기 Notification IO에서는 IO의 관찰을 명령하기 위한 함수호출과 실제로 상태의 변화가 있었는지 확인하기 위한 함수호출이 분리되어 있다. 때문에 IO의 관찰을 명령하고 나서 다른 일을 하다가 이후에 상태의 변화가 실제로 있었는지 확인하는 것이 가능하다.

  • 비동기(Asynchronous) Notification IO 모델의 이해와 구현

    비동기 Notification IO 모델의 구현 방법에는 두 가지가 존재한다. 하나는 WSAEventSelect 함수를 사용하는 방법이고, 다른 하나는 WSAAsyncSelect 함수를 사용하는 방법이다.

    WSAEventSelect 함수와 Notification

    WSAEventSelect 함수는 임의의 소켓을 대상으로 이벤트 발생여부의 관찰을 명령할 때 사용하는 함수이다.

#include <winsock2.h>

int WSAEventSelect(SOCKET s, WSAEVENT hEventObject, long lNetworkEvents);
// 성공 : 0
// 실패 : SOCKET_ERROR 반환

  • s

    관찰대상인 소켓의 핸들 전달

  • hEventObject

    이벤트 발생유무의 확인을 위한 Event 오브젝트의 핸들 전달

  • lNetworkEvents

    감시하고자 하는 이벤트의 유형 정보 전달

WSAEventSelect 함수는 매개변수 s에 전달된 핸들의 소켓에서 lNetworkEvents에 전달된 이벤트 중 하나가 발생하면, hEventObject에 전달된 핸들의 커널 오브젝트를 signaled 상태로 바꾸는 함수이다. 때문에 이 함수를 가리켜 다음과 같이 이야기 하기도 한다.

"Event 오브젝트와 소켓을 연결하는 함수"

WSAEventSelect함수는 이벤트의 발생유무에 상관없이 바로 반환을 하는 함수이기 때문에 함수호출 이후에 다른 작업을 진행할 수 있다. 즉, 이 함수는 비동기 Notification 방식을 취하고 있다.

위 함수에 세 번째 인자로 전달될 수 있는 이벤트의 종류에는 다음과 같다. 이들은 비트 OR연산자를 통해 둘 이상의 정보를 동시에 전달할 수 있다.

FD_READ

수신할 데이터가 존재하는가?

FD_WRITE

블로킹 없이 데이터 전송이 가능한가?

FD_OOB

Out-of-band 데이터가 수신되었는가?

FD_ACCEPT

연결요청이 있었는가?

FD_CLOSE

연결의 종료가 요청되었는가?

간단히 WSAEventSelect 함수에 대해서 알아보았는데 이 함수만 봐도 다음의 내용을 추가로 알아야 함을 알 수 있다.

  • WSAEventSelect 함수호출 이후의 이벤트 발생 확인방법
  • 이벤트 발생이 확인된 경우, 발생된 이벤트의 유형을 확인하는 방법

manual-reset 모드 Event 오브젝트의 또다른 생성방법

#include <winsock2.h>

WSAEVENT WSACreateEvent(void);
// 성공 : Event 오브젝트 핸들
// 실패 : WSA_INVALID_EVENT 반환

위 함수를 통해 생성된 Event오브젝트의 종료를 위한 함수는 다음과 같이 별도로 마련되어 있다.

#include <winsock2.h>

BOOL WSACloseEvent(WSAEVENT hEvent);
// 성공 : TRUE
// 실패 : FALSE

이벤트 발생 유무의 확인

이벤트 발생유무의 확인을 위해서는 Event오브젝트를 확인해야 한다.

#include <winsock2.h>

 

DWORD WSAWaitForMultipleEvents(

DWORD cEvents,

const WSAEVENT* lphEvents,

BOOL fWaitAll,

DWORD dwTimeout,

BOOL fAlertable

);

  • DWORD cEvents

    signaled 상태로의 전이여부를 확인할 Event오브젝트의 개수 정보 전달

  • const WSAEVENT* lphEvents

    Event 오브젝트의 핸들을 저장하고 있는 배열의 주소 값 전달

  • BOOL fWaitAll

    TRUE 전달 시 모든 Event 오브젝트가 signaled 상태일 때 반환

    FALSE 전달 시 하나만 signaled 상태가 되어도 반환

  • DWORD dwTimeout

    1/1000초 단위로 타임아웃 지정

    WSA_INFINITE 전달시 signaled 상태가 될 때까지 반환하지 않는다

  • BOOL fAlertable

    TRUE 전달 시, alertable wait 상태로의 진입

  • 반환값

    반환된 정수 값에서 상수 값 WSA_WAIT_EVENT_0를 빼면, 두 번째 매개변수로 전달된 배열을 기준으로 signaled 상태가 된 Event 오브젝트의 핸들이 저장된 인덱스가 계산된다. 만약 둘 이상의 Event 오브젝트가 signaled 상태로 전이 되었다면 그 중 작은 인덱스 값이 계산된다. 그리고 타움아웃이 발생하면 WAIT_TIMEOUT이 반환된다.

이는 소켓의 이벤트 발생에 의해서 Event 오브젝트가 signaled 상태가 되어야 반환하는 함수이므로 소켓의 이벤트 발생여부를 확인하기에 좋은 함수이다. 단, 전달할 수 있는 최대 Event오브젝트의 핸들 수가 64개로 제한되어 있으니, 이 이상의 핸들을 관찰해야 한다면 쓰레드의 생성을 통한 확장을 시도하거나 핸들을 저장하고 있는 배열을 구분해서 위 함수를 두 번 이상 호출하는 방법을 고민해야 한다.

이 함수가 반환하는 정보를 통해서 알 수 있는 것은 signaled 상태로 전이된 Event 오브젝트의 첫 번째 인덱스 값이 전부이다. 하지만 여기서 생성하는 Event오브젝트가 manual-reset 모드이기 때문에 다음과 같은 방식으로 signaled 상태가 된 Event 오브젝트를 모두 확인할 수 있다.

int posInfo, startIdx, i;
.....
posInfo = WSAWaitForMultipleEvents(numOfSock, hEventArray, FALSE, WSA_INFINITE, FALSE);

startID = posInfo - WSA_WAIT_EVENT_0;
...

for( i = startIdx ; i < numOfSock ; i++ )

{

int sigEventIdx = WSAWaitForMultipleEvents(1, &hEventArray[i], TRUE, 0, FALSE);

...

}

signaled 상태에 놓인 첫 번째 Event 오브젝트에서부터 마지막 Event 오브젝트까지 순서대로 하나씩, signaled 상태로의 전이여부를 확인하고 있음을 알 수 있다. 이는 Event 오브젝트가 manual-reset 모드이기 때문에 가능한 일이다. 즉 비동기 Notification IO모델에서 Event 오브젝트가 manual-reset 모드이어야 하는 이유를 위의 코드에서 보이고 있다.

이벤트 종류의 구분

#include <winsock2.h>

int WSAEnumNetworkEvents(SOCKET s, WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvents);
// 성공 : 0
// 실패 : SOCKET_ERROR 반환

  • s

    이벤트가 발생한 소켓의 핸들 전달

  • hEventObject

    소켓과 연결된, signaled 상태인 Event 오브젝트의 핸들 전달

  • lpNetworkEvents

    발생한 이벤트의 유형정보와 오류정보로 채워질 WSANETWORKEVENTS 구조체 변수의 주소 값 전달

위 함수는 manural-reset 모드의 Event 오브젝트를 non-signaled 상태로 되돌리기 대문에 ResetEvent함수를 호출할 필요가 없다.

typedef struct _WSANETWORKEVENTS
{

long lNetworkEvents;

int iErrorCode[FD_MAX_EVENTS];

}WSANETWORKEVENTS, *LPWSANETWORKEVENTS;

위의 구조체 멤버 lNetworkEvents에는 발생한 이벤트의 정보가 담긴다. 다음과 같은 방식으로 발생한 이벤트의 종류를 확인할 수 있다.

WSANETWORKEVENTS netEvents;
....
WSAEnumNetworkEvents(hSock, hEvent, &netEvents);

if( netEvents.lNetworkEvents & FD_ACCEPT )

{

// FD_ACCEPT 이벤트 발생에 대한 처리

}

if( netEvents.lNetworkEvents & FD_READ )

{

// FD_READ 이벤트 발생에 대한 처리

}

if( netEvents.lNetworkEvents & FD_CLOSE )

{

// FD_CLOSE 이벤트 발생에 대한 처리

}

오류 발생에 대한 정보는 구조체 멤버로 선언된 배열 iErrorCode에 담긴다(오류 발생의 원인이 둘 이상 될 수 있기 때문에 배열로 선언되었다) 확인 방법을 정리하면 다음과 같다

  • 이벤트 FD_READ관련 오류가 발생하면 iErrorCode[FD_READ_BIT]에 0 이외의 값 저장
  • 이벤트 FD_WRITE 관련 오류가 발생하면 iErrorCode[FD_WRITE_BIT]에 0 이외의 값 저장

즉, 이를 다음과 같이 일반화해서 이해하면 된다

"이벤트 FD_XXX 관련 오류가 발생하면 iErrorCode[FD_XXX_BIT]에 0 이외의 값 저장"

따라서 다음과 같은 형태로 오류 검사를 진행하면 된다.

WSANETWORKEVENTS netEvents;
....
WSAEnumNetworkEvents(hSock, hEvent, &netEvents);
...
if(netEvents.iErrorCode[FD_READ_BIT] != 0 )
{

// FD_READ 이벤트 관련 오류 발생

}

비동기 Notification IO모델의 에코 서버 구현

  • AsynNotiEchoServ_win.c

#include <stdio.h>

#include <string.h>

#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

   

#define BUF_SIZE 100

   

void CompressSockets(SOCKET hSockArr[], int idx, int total);

void CompressEvents(WSAEVENT hEventArr[], int idx, int total);

void error_handling(char* msg);

   

int main(int argc, char* argv[])

{

WSADATA wsaData;

SOCKET hServSock, hClntSock;

SOCKADDR_IN servAdr, clntAdr;

   

SOCKET hSockArr[WSA_MAXIMUM_WAIT_EVENTS];

WSAEVENT hEventArr[WSA_MAXIMUM_WAIT_EVENTS];

WSAEVENT newEvent;

WSANETWORKEVENTS netEvents;

   

int numOfClntSock = 0;

int strLen, i;

int posInfo, startIdx;

int clntAdrLen;

char msg[BUF_SIZE];

   

if( argc != 2 )

{

printf("Usage : %s <port>\n", argv[0]);

exit(1);

}

   

if( WSAStartup(MAKEWORD(2,2), &wsaData) != 0 )

error_handling("WSASTartup() error");

   

hServSock = socket(PF_INET, SOCK_STREAM, 0);

memset(&servAdr, 0, sizeof(servAdr));

servAdr.sin_family = AF_INET;

servAdr.sin_addr.s_addr = htonl(INADDR_ANY);

servAdr.sin_port = htons(atoi(argv[1]));

   

if( bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR )

error_handling("bind() error");

   

if( listen(hServSock, 5) == SOCKET_ERROR )

error_handling("listen() error");

   

newEvent = WSACreateEvent();

if( WSAEventSelect(hServSock, newEvent, FD_ACCEPT) == SOCKET_ERROR )

error_handling("WSAEventSelect() error");

   

hSockArr[numOfClntSock] = hServSock;

hEventArr[numOfClntSock] = newEvent;

numOfClntSock++;

   

while(1)

{

posInfo = WSAWaitForMultipleEvents(numOfClntSock, hEventArr, FALSE, WSA_INFINITE, FALSE);

startIdx = posInfo - WSA_WAIT_EVENT_0;

   

for( i = startIdx ; i < numOfClntSock ; i++ )

{

int sigEventIdx = WSAWaitForMultipleEvents(1, &hEventArr[i], TRUE, 0, FALSE);

if( (sigEventIdx == WSA_WAIT_FAILED || sigEventIdx == WSA_WAIT_TIMEOUT) )

{

continue;

}

else

{

sigEventIdx = i;

WSAEnumNetworkEvents(hSockArr[sigEventIdx], hEventArr[sigEventIdx], &netEvents);

// 연결 요청 시

if( netEvents.lNetworkEvents & FD_ACCEPT )

{

if( netEvents.iErrorCode[FD_ACCEPT_BIT] != 0 )

{

puts("Accept Error");

break;

}

clntAdrLen = sizeof(clntAdr);

hClntSock = accept(hSockArr[sigEventIdx], (SOCKADDR*)&clntAdr, &clntAdrLen);

newEvent = WSACreateEvent();

WSAEventSelect(hClntSock, newEvent, FD_READ | FD_CLOSE);

   

hEventArr[numOfClntSock] = newEvent;

hSockArr[numOfClntSock] = hClntSock;

numOfClntSock++;

puts("connected new client...");

}

// 데이터 수신 시

if( netEvents.lNetworkEvents & FD_READ )

{

if( netEvents.iErrorCode[FD_READ_BIT] != 0 )

{

puts("Read Error");

break;

}

strLen = recv(hSockArr[sigEventIdx], msg, sizeof(msg), 0);

send(hSockArr[sigEventIdx], msg, strLen, 0);

}

// 종료 요청 시

if( netEvents.lNetworkEvents & FD_CLOSE )

{

if( netEvents.iErrorCode[FD_CLOSE_BIT] != 0 )

{

puts("Close Error");

break;

}

WSACloseEvent(hEventArr[sigEventIdx]);

closesocket(hSockArr[sigEventIdx]);

   

numOfClntSock--;

CompressSockets(hSockArr, sigEventIdx, numOfClntSock);

CompressEvents(hEventArr, sigEventIdx, numOfClntSock);

}

}

}

}

WSACleanup();

return 0;

}

void CompressSockets(SOCKET hSockArr[], int idx, int total)

{

int i;

   

for( i = idx; i < total ; i++ )

hSockArr[i] = hSockArr[i+1];

}

void CompressEvents(WSAEVENT hEventArr[], int idx, int total)

{

int i;

   

for( i = idx ; i < total ; i++ )

hEventArr[i] = hEventArr[i+1];

}

void error_handling(char* msg)

{

fputs(msg, stderr);

fputc('\n', stderr);

exit(1);

}

 

반응형