Asynchronous Notification IO 모델

WSAEventSelect 함수와 Notification

#include <winsock2.h>

int WSAEventSelect(SOCKET s, WSAEVENT hEventObject, long lNetworkEvents);
// 성공 시 0, 실패 시 SOCKET_ERROR 반환
  • s: 관찰대상인 소켓의 핸들 전달
  • hEventObject: 이벤트 발생유무의 확인을 위한 Event 오브젝트의 핸들 전달.
  • lNetworkEvents: 감시하고자 하는 이벤트의 유형 정보전달.

즉, WSAEventSelect 함수는 매개변수 s에 전달된 핸들의 소켓에서 lNetworkEvents에 전달된 이벤트 중 하나가 발생하면, hEventObject에 전달된 핸들의 커널 오브젝트를 signaled 상태로 바꾸는 함수이다. 때문에 이 함수를 가리켜 “Event 오브젝트와 소켓을 연결하는 함수”라고 하기도 한다.

세번째 인자인 lNetworkEvents에 전달할 수 있는 이벤트의 종류는 다음과 같다.

  • FD_READ: 수신할 데이터가 존재하는가?
  • FD_WRITE: 블로킹 없이 데이터 전송이 가능한가?
  • FD_OOB: Out-of-Band 데이터가 수신되었는가?
  • FD_ACCEPT: 연결 요청이 있었는가?
  • FD_CLOSE: 연결의 종료가 요청되었는가?

select 함수는 여러 소켓을 대상으로 호출이 가능한데, WSAEventSelect 함수는 하나의 소켓을 대상으로만 호출이 가능하다.

select 함수는 반환되고 나면 이벤트의 발생확인을 위해서 또 다시 모든 핸들(파일 디스크립터)을 대상으로 재호출해야 되지만, WSAEventSelect 함수호출을 통해서 전달된 소켓의 정보는 운영체제에 등록이 되고, 이렇게 등록된 소켓에 대해서는 WSAEventSelect 함수의 재호출이 불필요하다.

다음의 내용을 추가로 알아야 한다.

  • WSAEventObject 함수의 두 번째 인자(WSAEVENT) 전달을 위한 오브젝트의 생성 방법
  • WSAEventObject 함수호출 이후의 이벤트 발생 확인 방법
  • 이벤트 발생이 확인된 경우, 발생된 이벤트의 유형을 확인하는 방법

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

이전에는 CreateEvent 함수를 이용해서 Event 오브젝트를 생성하였다. 바로 manual-reset 모드이면서 non-signaled 상태인 Event 오브젝트를 생성하려면 다음 함수를 이용하는 것이 편리하다.

#include <winsock2.h>

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

#define WSAEVENT HANDLE로, WSAEVENT는 HANDLE과 같다.

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

#include <winsock2.h>

BOOL WSACloseEvent(WSAEVENT hEvent);
// 성공 시 TRUE, 실패 시 FALSE 반환

이벤트 발생유무의 확인

WSACreateEvent 함수호출 이후를 고민할 차례이다. 이벤트 발생유무의 확인을 위해서는 Event 오브젝트를 확인해야 한다. 참고로 이는 매개변수가 하나 더 많다는 것을 제외하면, WaitForMultipleObjects 함수와 동일하다.

#include <winsock2.h>

DWORD WSAWaitForMultipleEvents(
    DWORD cEvents,
    const WSAEVENT* lphEvents,
    BOOL fWaitAll,
    DWORD dwTimeout,
    BOOL fAlertable
);
// 성공 시 이벤트 발생 오브젝트 관련 정보, 길패 시 WSA_INVALID_EVENT 반환
  • cEvents: signaled 상태로의 전이여부를 확인할 Event 오브젝트의 개수 정보 전달.
  • lphEvents: Event 오브젝트의 핸들을 저장하고 있는 배열의 주소 값 전달.
  • fWaitAll: TRUE 전달 시 모든 Event 오브젝트가 signaled 상태일 때 반환, FALSE 전달 시 하나만 signaled 상태가 되어도 반환.
  • dwTimeout: 1/1000초 단위로 타임아웃 지정, WSA_INFINITE 전달 시 signaled 상태가 될 때까지 반환하지 않는다.
  • fAlertable: TRUE 전달 시, alertable wait 상태로의 진입(이는 추후 설명한다.)
  • 반환 값: 반환된 정수 값에서 상수 값 WSA_WAIT_EVENT_0를 빼면, 두 번째 매개변수로 전달된 배열을 기준으로, signaled 상태가 된 Event 오브젝트의 핸들이 저장된 인덱스가 계산된다. 만약에 둘 이상의 Event 오브젝트가 signaled 상태로 전이되었다면, 그 중 작은 인덱스 값이 계산된다. 그리고 타임아웃이 발생하면 WAIT_TIMEOUT이 반환된다.

이는 소켓의 이벤트 발생에 의해서 Event 오브젝트가 signaled 상태가 되어야 반환하는 함수이므로 소켓의 이벤트 발생여부를 확인하기에 좋은 함수이다.

그러나 반환 값을 통해 알 수 있는 건 첫번째 핸들의 인덱스 뿐이므로, 다음과 같이 여러번의 반복문 호출을 통해서 모든 오브젝트의 핸들 정보를 알 수 있다. 반복 호출이 가능한 이유는 ‘manual-reset’이기 때문이다.

int posInfo, startIdx, i;
. . . . .
posInfo = WSAWaitForMultipleEvents(numOfSock, hEventArray, FALSE, WSA_INFINITE, FALSE);
startIdx = posInfo - WSA_WAIT_EVENT_0;
. . . . .
for(i = startIdx ; i < numOfSock ; i++)
{
    int sigEventIdx = WSAWaitForMultipleEvents(1, &hEventArray[i], TRUE, 0, FALSE);
    . . . . .
}

비동기 Notification IO 모델에서 Event 오브젝트가 ‘manual-reset’ 모드여야 하는 이유를 위에서 알 수 있다.

이벤트 종류의 구분

WSAWaitForMultipleEvents 함수를 통해서 signaled 항태로 전이된 Event 오브젝트까지 알아낼 수 있게 되었으니, 이제 마지막으로 해당 오브젝트가 signaled 상태가 된 원인을 확인해야 한다. 그리고 이를 위해서 다음 함수를 소개한다. 이 함수의 호출을 위해서는 signaled 상태의 Event 오브젝트 핸들뿐만 아니라, 이와 연결된(WSAEventSelect 함수 호출에 의해), 이벤트 발생의 주체가 되는 소켓의 핸들도 필요하다.

#include <winsock2.h>

int WSAEnumNetworkEvents(
    SOCKET s,
    WSAEVENT hEventObject,
    LPWSANETWORKEVENTS lpNetworkEvents
);
// 성공 시 0, 실패 시 SOCKET_ERROR 반환
  • s: 이벤트가 발생한 소켓의 핸들 전달.
  • hEventObject: 소켓과 연결된(WSAEventSelect 함수호출에 의해), signaled 상태인 Event 오브젝트의 핸들 전달.
  • lpNetworkEvents: 발생한 이벤트의 유형정보와 오류정보로 채워질 WSANETWORKEVENTS 구조체 변수의 주소 값 전달.

위 함수는 manual-reset 모드의 Event 오브젝트를 non-signaled 상태로 되돌리기까지 하니, 발생한 이벤트의 유형을 확인한 다음에, 별도로 ResetEvent 함수를 호출할 필요가 없다. 위 함수와 관련있는 구조체 WSANETWORKEVENTS를 소개하겠다.

typedef struct _WSANETWORKEVENTS
{
    long lNetworkEvents;
    int iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, *LPWSANETWORKEVENTS;

위의 구조체 멤버 lNetworkEvents에는 발생한 이벤트의 정보가 담긴다.
여기lNetworkEvents에 전달할 수 있는 이벤트의 종류 참조.

따라서 다음과 같은 방식으로 발생한 이벤트의 종류를 확인할 수 있다.

WSANETWORKEVENTS netEvents;
. . . . .
WSAEnumNetworkEvents(hSock, hEvent, &netEvents);
if(netEvents.lNetworkEvents & FD_ADDEPT)
{
    // 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 이외의 값 저장

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

WSANETWORKEVENTS netEvents;
. . . . .
WSAEnumNetworkEvents(hSock, hEvent, &netEvents);
. . . . .
if(netEvents.iErrorCode[FD_READ_BIT] != 0)
{
    // FD_READ 이벤트 관련 오류 발생
}

비동기 Notification I/O 모델 에코 서버 구현

#include <stdio.h>
#include <string.h>
#include <winsock2.h> 

#define BUF_SIZE 100 

void CompressSockets(SOCKET hSockArr[], int idx, int total);
void CompressEvents(WSAEVENT hEventArr[], int idx, int total);
void ErrorHandling(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)		
        ErrorHandling("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)		
        ErrorHandling("bind() error"); 	
    
    if(listen(hServSock, 5)==SOCKET_ERROR)		
        ErrorHandling("listen() error"); 	
        
    newEvent=WSACreateEvent();	
    if(WSAEventSelect(hServSock, newEvent, FD_ACCEPT)==SOCKET_ERROR)
        ErrorHandling("WSAEventSelect() error"); 	
    
    hSockArr[numOfClntSock]=hServSock;	
    hEventArr[numOfClntSock]=newEvent;	
    numOfClntSock++; 	
    
    while(1)	
    {		
        posInfo=WSAWaitForMultipleEvents( //첫번째 발생한 이벤트 index			
            numOfClntSock, hEventArr, FALSE, WSA_INFINITE, FALSE);		
        startIdx=posInfo-WSA_WAIT_EVENT_0; //상수값을 빼서 index를 구함 		
        
        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 ErrorHandling(char *msg){		
    fputs(msg, stderr);	
    fputc('\n', stderr);	
    exit(1);
}