본문 바로가기
Programming/열혈 TCP, IP 소켓 프로그래밍(저자 윤성우)

Ch 21. Asynchronous Notification IO 모델

by minjunkim.dev 2020. 8. 15.

모든 내용은 [윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!


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

- 지금까지 윈도우 기반 예제에서 send/recv 함수를 통해서 동기화된 입출력을 진행하였음

 

- 동기식 입출력 : 입출력 함수의 반환시점과 데이터 송수신의 완료시점이 일치하는 경우

1) send 함수를 호출하면 데이터 전송 시작

2) 출력버퍼로 데이터 전송이 완료되면 send 함수 반환

3) recv 함수를 호출하면 데이터 수신 시작

4) 원하는만큼 데이터 수신을 하면 recv 함수 반환

 

- 그렇다면 비동기 입출력이 의미하는 바는 무엇인가?

: 입출력 함수의 반환시점과 데이터 송수신의 완료시점이 일치하지 않는 경우

1) send 함수를 호출하면 데이터 전송이 시작되고, send 함수는 반환됨

2) 이후에 출력버퍼로 데이터 전송이 완료됨

3) recv 함수를 호출하면 데이터 수신이 시작되고, recv 함수는 반환됨

4) 이후에 원하는만큼 데이터 수신이 완료됨

 

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

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

- 비동기 입출력 : 데이터의 송수신 완료에 상관없이, 호출된 함수가 반환을 하므로 다른 일을 진행할 수 있음

- 비동기 방식은 동기 방식에 비해 보다 효율적으로 CPU를 활용하는 모델이 됨, 이를 가리켜 동기 방식의 단점을 극복한 모델이라 하는 것

 

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

- 그러나, 동기와 비동기는 입출력에 한정해서만 논의되는 것이 아니다.

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

- 대표적인 Notification IO : select 방식

"IO가 필요한, 또는 가능한 상황이 되는 시점이(간단히 말해서 IO관련 이벤트의 발생시점이) select 함수가 반환하는 시점과 일치함" => 동기 Notification IO

- 비동기 Notification IO : IO의 상태에 상관없이 반환이 이뤄지는 방식 e.g. WSAEventSelect 함수

IO의 관찰을 명령하기 위한 함수호출과 실제로 상태의 변화가 있었는지 확인하기 위한 함수호출이 분리되어 있음.

때문에 IO의 관찰을 명령하고 나서 다른 일을 열심히 하다가 이후에 상태의 변화가 실제로 있었는지 확인하는 것이 가능.

* 물론 select 함수도 타임아웃을 지정할 수 있다.

- select 함수도 타임아웃의 지정을 통해서 IO의 상태변화가 발생하지 않은 상황에서 블로킹 상태에 놓이지 않을 수 있다.(반환할 수 있다.) 따라서 비동기와 유사한 형태의 코드를 작성할 수는 있다. 그러나 이후에 IO의 상태 변화를 확인하기 위해서는 핸들(파일 디스크립터)을 다시 모아서 재차 select 함수를 호출해야 한다. 따라서 select 함수는 기본적으로 동기화 된 형태의 Notification IO 모델이다.


# 비동기 Notification IO 모델의 구현 방법에는

1) WSAEventSelect 함수를 사용

2) WSAAsyncSelect 함수를 사용 : 이 함수를 사용하기 위해서는 발생한 이벤트를 수신할 윈도우의 핸들을 지정해야 하기 때문에(UI와 관련된 내용) 이 책에서는 언급하지 않음

 

# WSAEventSelect 함수와 Notification

- IO의 상태변화가 의미하는 것

1) 소켓의 상태변화 : 소켓에 대한 IO의 상태변화

2) 소켓의 이벤트 발생 : 소켓에 대한 IO관련 이벤트 발생

=> 둘다 IO가 필요한, 또는 가능한 상황의 발생을 의미

 

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

#include <winsock2.h>

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

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

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

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

1) FD_READ : 수신할 데이터가 존재하는가?

2) FD_WRITE : 블로킹 없이 데이터 전송이 가능한가?

3) FD_OOB : Out-of-band 데이터가 수신되었는가?

4) FD_ACCEPT : 연결요청이 있었는가?

5) FD_CLOSE : 연결의 종료가 요청되었는가?

- 비트 OR 연산자(|)를 통해 둘 이상의 정보를 동시에 전달 가능

 

- 즉, WSAEventSelect 함수는 s 핸들의 소켓에서 lNetworkEvents에 해당하는 이벤트 중 하나가 발생하면, hEventObject 핸들의 Event 커널 오브젝트의 상태를 signaled 상태로 바꾸는 함수 => "Event 커널 오브젝트와 소켓을 연결하는 함수"라고 하기도 함

- 그리고 이벤트의 발생유무에 상관없이 바로 반환을 하는 함수이기 때문에 함수호출 이후에 다른 작업을 진행할 수 있음 : 비동기 Notification IO 방식

- 비동기 IO의 구현에서, select 함수는 반환되고 나면 이벤트의 발생확인을 위해서 또 다시 모든 핸들(파일 디스크립터)을 대상으로 재호출해야 한다.(관찰 대상의 정보를 매번 다시 전달해야 함) 그러나 WSAEventSelect 함수는 호출을 통해 전달된 소켓의 정보가 운영체제에 등록이 되기 때문에, 이 소켓에 대해서는 WSAEventSelect 함수를 재호출 할 필요가 없다.(리눅스의 epoll과 동일)

 

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

- CreateEvent 함수를 이용해서 Event 커널 오브젝트를 생성하되, auto-reset 모드와 manual-reset 모드 중 하나를 선택하고, signaled 상태와 nonsignaled 중 하나를 선택하여 생성할 수 있었음

- manual-reset 모드와 non-signaled 상태의 Event 커널 오브젝트가 필요할 때는, 다음 함수를 이용해서 Event 커널 오브젝트를 생성하는 것이 편리함

#include <winsock2.h>
// #define WSAEVENT HANDLE => 이미 이렇게 정의되어 있음 

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

- 위의 함수를 통해서 생성된 Event 커널 오브젝트의 종료를 위한 함수는 다음과 같음

#include <winsock2.h>

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

 

 

# 이벤트 발생유무의 확인

- WSAEventSelect 함수호출(이벤트 발생유무의 관찰을 명령) 이후에, 이벤트 발생유무의 확인을 위해서는 Event 커널 오브젝트를 확인해야 함

- 이 때 사용하는 함수는 다음과 같으며, 이는 매개변수가 하나 더 많다는 것을 제외하면, WaitForMultipleObjects 함수와 동일

#include <winsock2.h>

DWORD WSAWaitForMultipleEvents(
DWORD cEvents, const WSAEVENT* lphEvents, BOOL fWaitAll,
DWORD dwTimeout, BOOL fAlertable);
// 성공시 이벤트 발생 커널 오브젝트 관련정보, 실패시 WSA_INVALID_EVENT 반환

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

(최대로 가능한 핸들 수는 매크로 형태로 정의되어 있는 상수 WSA_MAXIMUM_WAIT_EVENTS를 확인하면 알 수 있음.

그 이상이 필요하다면 쓰레드의 생성을 통한 확장이나,

핸들을 저장하고 있는 배열을 구분해서 위 함수를 두 번 이상 호출하는 방법을 시도해야 함)

- 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이 반환된다.

 

- 위 함수는 단 한번의 호출로 signaled 상태로 전이된 Event 커널 오브젝트의 "핸들정보 모두"를 알 수 있는 것이 아니다.(반환값의 특징을 보면 하나 밖에 알 수 없다.) 그러나 여기서 생성하는 Event 커널 오브젝트가 manual-reset 모드라는 사실을 참고하여 다음과 같은 방식을 통해 모두 알 수 있다.

int posInfo, startIdx, i;
....

/* signaled 상태가 된 첫번째 Event 커널 오브젝트의 관련 정보 반환*/
posInfo=WSAWaitForMultipleEvents(numOfSock,hEventArray,FALSE,WSA_INFINITE,FALSE);

startIdx=posInfo-WSA_WAIT_EVENT_0; // 이를 통해 인덱스 계산

/*
찾은 Event 커널 오브젝트에서 마지막 Event 커널 오브젝트까지
signaled 상태인지 아닌지 확인
*/
for(i=startIdx;i<numOfSock;++i)
{	
	/*
    Event 커널 오브젝트 하나하나씩 signaled 상태인지 확인 
    타임아웃 시간이 0이므로 호출하자마자 반환
    signaled 상태라면 Event 커널 오브젝트의 관련 정보를,
    아니라면 WAIT_TIMEOUT을 반환할 것임
    */
    int sigEventIdx=WSAWaitForMultipleEvents(1,&hEventArray[i],TRUE,FALSE);
    ....
}

- Event 커널 오브젝트가 manual-reset 모드(자동으로 다시 non-signaled 상태로 돌아가지 않으므로)라서 위와 같이 확인이 가능한 것임 => 비동기 Notification IO 모델에서 Event 오브젝트가 manual-reset 모드이어야 하는 이유


# 이벤트 종류의 구분

- 이벤트 발생유무의 확인하였다면, 이후에는 어떤 이벤트가 발생하여 signaled 상태가 되었는지 원인을 파악하여야 하는데, 이 때 아래 함수를 호출함

- 아래 함수의 호출을 위해서는

1) signaled 상태의 Event 커널 오브젝트의 핸들과

2) WSAEventSelect 함수호출에 의해 연결된, 이벤트 발생의 주체가 되는 소켓의 핸들

둘 다 필요함

#include <winsock2.h>

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


#include <winsock2.h>

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

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

- hEventObject : 소켓과 연결된(WSAEventSelcect 함수호출에 의해), signaled 상태인 Event 커널 오브젝트의 핸들 전달

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

 

- 위 함수는 manual-reset 모드의 Event 커널 오브젝트를 non-signaled 상태로 되돌리므로, 발생한 이벤트의 유형을 확인후 별도로 ResetEvent 함수를 호출할 필요가 없음

 

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

- 구조체 멤버 lNetworkEvents에는 발생한 이벤트의 정보가 담김

(WSAEventSelect 함수의 세번째 인자로 전달되는 상수와 동일)

- 발생한 이벤트의 종류를 확인하는 방법의 예시

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_XXX 관련 오류가 발생하면 iErrorCode[FD_XXX_BIT]에 0 이외의 값 저장"

- 오류검사를 진행하는 방법의 예시

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

if(netEvents.iErrorCode[FD_READ_BIT]!=0)
{
    // FD_READ 이벤트 관련 오류발생
}

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

 

AsyncNotiEchoServ_win.c

#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) // 실행파일의 경로/PORT번호 를 입력으로 받아야 함
    {
    	printf("Usage: %s <port> \n",argv[0]);
    	exit(EXIT_FAILURE);
    }
    
    /* 소켓 버전 2.2로 윈속관련 라이브러리의 초기화 */
    if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0)
        ErrorHandling("WSAStartup() error!");
    
    /* TCP 소켓 생성 */
    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");
    
    /* manual-reset/non-signaled Event 오브젝트 생성 */
    newEvent=WSACreateEvent();
    
    /*
    서버소켓의 이벤트 발생여부의 관찰을 명령
    이벤트는 FD_ACCEPT(연결요청이 있었는가?)
    이벤트가 발생하면, 커널 오브젝트는 signaled 상태로 변경됨
    */
    if(WSAEventSelect(hServSock,newEvent,FD_ACCEPT)==SOCKET_ERROR)
        ErrorHandling("WSAEventSelect() error");
        
    /* 관찰대상 소켓의 핸들 저장 */
    hSockArr[numOfClntSock]=hServSock;
    
    /* 관찰대상 소켓과 연결된 커널 오브젝트 핸들 저장 */
    hEventArr[numOfClntSock]=newEvent;
    
    /* 이벤트 발생 관찰을 명령한 소켓의 개수 증가 */
    ++numOfClntSock;
    
    while(1)
    {
    	/*
        첫번째로 이벤트가 발생한(signaled 상태가 된)
        Event 커널 오브젝트 관련정보 반환
        */
        posInfo=WSAWaitForMultipleEvents(
        numOfClntSock,hEvenrArr,FALSE,WSA_INIFINITE,FALSE);
        
        startIdx=posInfo-WSA_WAIT_EVENT_0; // 인덱스 계산
        
        for(i=startIdx;i<numOfClntSock;++i)
        {
        	/*
            각 Event 커널 오브젝트에 대해
            이벤트가 발생했는지(signaled 상태인지) 확인
            */
            int sigEventIdx=WSAWaitForMultipleEvents(1,&hEventArr[i],TRUE,0,FALSE);
            
            if((sigEventIdx==WAS_WAIT_FAILED || sigEventIdx==WAS_WAIT_TIMEOUT))
                continue;
            else // 이벤트가 발생했다면
            {
                sigEventIdx=i; // 해당되는 소켓과 커널 오브젝트의 핸들을 얻기 위한 인덱스
                
                /* signaled 상태가 된 원인(이벤트의 종류)을 확인 */
                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);
                     
                     /* manual-reset/non-signaled Event 오브젝트 생성 */
                     newEvent=WSACreateEvent();
                     
                     /*
                     생성된 소켓의 이벤트 발생여부의 관찰을 명령
                     이벤트는 수신할 데이터가 존재 or 연결의 종료가 요청됨
                     이벤트가 발생하면, 커널 오브젝트는 signaled 상태로 변경됨
                     */
                     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;
                    }
                    
                    /* 클라이언트와의 송수신을 위해 생성된 소켓과 연결된 Event 커널 오브젝트 소멸 */
                    WSACloseEvent(hEventArr[sigEventIdx]);
                    
                    /* 클라이언트와의 송수신을 위해 생성된 소켓의 커널 오브젝트 소멸 */
                    closesocket(hSockArr[sigEventIdx]);
                    
                    /* 이벤트 발생 관찰대상인 소켓의 개수 감소 */
                    --numOfClntSock;
                    
                    /*
                    관찰대상에서 제외된 소켓,
                    이와 연결된 Event 커널 오브젝트의 핸들을
                    배열에서 삭제
                    */
                    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,stdout);
    fputc('\n',stdout);
    exit(EXIT_FAILURE);
}

[출처] : 윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어