본문 바로가기
컴퓨터/Server

Window I/O 통지모델 : WSAAsyncSelect , WSAEventSelect, Overlapped I/O

by 김짱쌤 2015. 3. 29.

WSAAsyncSelect



비동기 Select

WSAAsyncSelect(이하 WAS)는 윈도우 운영체제에서 제공하는 socket용 통지모델이다. 대놓고 Async를 표방한만큼 지금까지 앞에서 언급했던 동기형 통지모델과 다른 방식으로 통지를 해준다. 사용자가 커널의 상황을 지속적으로 확인하며 통지를 받는 것이 아니라, 특정 상황이 되면 통지를 주도록 예약을 하는 것이다. 그야말로 비동기 통지방식이다. 기존 Select가 확인하던 I/O 상태변화에 대해서 소켓별로 WAS를 사용하여 등록을 하면 윈도우 메시지를 통해서 통지가 된다. 그래서 둘이 합쳐 Async Select이다. 통지 방식으로 윈도우 메시지를 사용하는 만큼 WAS는 윈도우 프로시저에서만 사용할 수 있다.

리눅스의 동기 형식의 통지방식은 다수의 fd(소켓)에 대해서 동시에 체크 할 수 있었지만, 윈도우의 비동기 통지방식은 소켓별로 따로 통지한다. 쉽게 생각하면 동기 형식이 더 좋은게 아니냐하고 반문할 수 있겠지만, 동기형식도 FD_SET에 새로운 FD들을 등록해야하는것은 매한가지이고, 비동기 통지의 경우 한번 등록하면 계속해서 호출할 필요가 없기 때문에, 부하가 적다는 이점이 있다. 내부적으로 따로 체크하는 것이 아니라 운영체제가 I/O 상황이 될때 인터럽트를 사용하는 방식으로 구현되기 때문에 운영체제 수준에서도 연산량이 많이 줄어든다. 대신 다른 운영체제에서 지원하지 않는 기능이기 때문에, 다른 구동환경에서 같은 프로세스를 사용할 수가 없다는 단점은 있다.


WSAAsyncSelect 사용법

WAS의 시그니처는 다음과 같다.


int WSAAsyncSelect(
   SOCKET socket, // I/O 상황을 체크할 소켓
   HWND hWnd, //메시지 통지를 받을 윈도우 핸들
   unsigned int wMsg, //이벤트가 발생하면 보낼 메시지
   long lEvent //통지받을 네트워크 이벤트 비트마스크
);
//성공시 0을 반환, 실패시 SOCKET_ERROR 반환
//네트워크 이벤트
enum NetworkEvent
{
   FD_ACCEPT, //클라이언트가 접속했을 때
   FD_READ,   //수신 가능할 때
   FD_WRITE,  //송신 가능할 때
   FD_CLOSE,  //접속 종료할 때
   FD_CONNECT,//접속 완료될 때
   FD_OOB,    //OOB 데이터 도착할 때
};


원하는 이벤트를 비트마스킹해서 4번째 인자에 넣어주기만 하면 쉽게 사용가능하다.( ex: FD_READ | FD_WRITE ) 실제 예를 보면서 이해하는 것이 쉽다.


#define WM_SOCKET (WM_USER + 1)
int main(){
HWND hWnd = MakeWindow();
...
   WSAAsyncSelect( socket, hWnd, WM_SOCKET, FD_READ | FD_WRITE | FD_CLOSE ); 
  //소켓에 AsyncSelect 이벤트 등록
...
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
...
switch(uMsg)
{
   case WM_SOCKET:
   {
      SOCKET selectedSocket = wParam; //socket번호는 wParam으로 전달
      int event = WSAGETSELECTEVENT(lParam); //이벤트 정보는 lParam에서 추출
      switch(event)
      {
         //I/O 이벤트별 대응코드
      }
   }
}


WSAAsyncSelect 의 정체성

앞에서 먼저 언급했지만 WAS는 그 이름이 말하는 대로 비동기 통지방식이다. 커널에게 미리 등록만 해두면 유저는 따로 커널에게 확인하여 동기화하지 않더라도 알아서 메시지가 날아온다. Sync보다 상당히 편하고, 매 프레임마다 체크를 하지 않아도 되니 부담도 적다. 그리고 WAS 함수자체에서 I/O상황을 리턴받는 것이 아니고 등록만 하는 것이다 보니 Block이 걸릴 소지가 없다. 따라서 Non-Block 방식이다.


WSAEventSelect



윈도우 메시지 대신 이벤트 오브젝트를 사용

WSAAsyncSelect의 경우 윈도우 메시지를 통지방식으로 사용하기 때문에 윈도우 프로시저에서만 사용할 수 있다는 제한이 있었다. 하지만 WSAEventSelect는 윈도우 메시지 대신 이벤트 오브젝트에 시그널을 변경시키는 방식으로 통지를 준다. 이벤트 객체를 사용하기 때문에, Wait 함수를 사용하여 signal 대기하는 방식으로 구현해야한다.  


WSAEventSelect 사용법

켓을 생성할 때 마다 WSACreateEvent() 함수를 사용하여 이벤트 객체를 생성한다. 굳이 CreateEvent()가 아닌 다른 함수를 사용하는 이유는 Event의 reset모드가 auto-reset이 되면 제대로 signal을 확인 할 수 없기 때문이다. WSACreateEvent() 함수를 통해 만들어지는 이벤트는 manual-reset 모드로만 생성된다. 따라서 이 함수를 사용하는 것이 편리하다.


WSAEVENT WSACreateEvent(); 
// 이벤트 오브젝트를 생성 
// 성공시 Event핸들 반환, 실패시 WSA_INVALID_EVENT 반환
BOOL WSACloseEvent(WSAEVENT hEvent); 
// 생성하면 해제하는것은 기본.
// 이벤트 오브젝트 해제 성공시 TRUE, 실패시 FALSE


WSAEventSelect() 함수를 사용하여 소켓과 이벤트를 연결하고, 처리할 이벤트를 등록한다.


int WSAEventSelect (
   SOCKET socket, // 설정 소켓
   WSAEVENT hEventObject, // 연결할 이벤트
   long lNetworkEvents // 네트워크 이벤트
);
// 반환값 : 성공시 0 반환, 실패시 SOCKET_ERROR 반환


WSAWaitForMultipleEvent() 함수를 호출하여 이벤트를 받기 위한 대기상태로 만든다.


DWORD WSAWaitForMUltipleEvents( DWORD cEvents, // 대기할 이벤트 오브젝트 갯수 (최대 64개) const WSAEVENT* lphEvents, // 대기할 이벤트 오브젝트 핸들의 배열 BOOL fWaitALL, // TRUE면 모든 이벤트가 signaled될 때까지 대기, // FALSE면 한 이벤트라도 signaled이면 리턴 DWORD dwTimeout, // epoll의 timeout과 동일 WSA_INFINITE이면 무한 대기 BOOL fAlertable // TRUE전달시 alertable wait 상태로 진입 ); // 반환값: 타임아웃시 WAIT_TIMEOUT, 아니면 signaled 상태가 된 이벤트 오브젝트를 찾을 수 있다.


반환 값에서 상수 값 WSA_WAIT_EVENT_0을 빼면 2번째 인자 배열 기준 인덱스를 얻을 수 있다. 여러개가 signaled되면 더 작은 인덱스 값이 반환된다. 동시에 여러 이벤트 오브젝트가 signaled 되면 그 시점에서는 알 수 없지만, manual-reset모드이므로 이벤트 오브젝트의 signal이 유지된 상태이기 때문에 다시 한번 WSAWaitForMultipleEvents로 0 딜레이 체크를 해주면 된다. 


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


WSAEnumNetworkEvents() 함수를 호출하여 발생한 네트워크 이벤트를 알아내고 적절한 소켓함수를 찾아 처리한다. 


int WSAEnumNetworkEvents(
   SOCKET socket, // 이벤트 발생한 소켓
   WSAEVENT hEventObject, // signaled된 이벤트 오브젝트
   LPWSANETWORKEVENTS lpNetworkEvents //발생할 이벤트의 정보를 얻을 구조체 주소
);
// 반환값 : 성공시 0 반환, 실패시 SOCKET_ERROR 반환

typedef struct _WSANETWORKEVENTS {
   long lNetworkEvents;  //발생한 이벤트의 정보 (FD_READ...)
   int iErrorCode[FD_MAX_EVENTS]; //오류 코드, 여러개 일 수 있으니 배열로 저장
}WSANETWORKEVENTS, *LPWSANETWORKEVENTS;


서버에서 WSAEventSelect를 사용한 예


...
WSAEVENT hEventArr[WSA_MAXIMUM_WAIT_EVENTS];
SOCKET hSockArr[WSA_MAXIMUM_WAIT_EVENTS];
WSANETWORKEVENTS netEvents;
int numOfClntSock = 0;
...
WSAEVENT newEvent = WSACreateEvent();
if(WSAEventSelect(server_Socket, newEvent, FD_ACCEPT) == SOCKET_ERROR)
   ErrorHandling("WSAEventSelect() Error");
hSockArr[numOfClntSock] = server_Socket;
hEventArr[numOfClntSock] = newEvent;
++numOfClntSock;
// 서버 소켓에 ACCEPT를 select함
while(TRUE)
{
   posInfo = WSAWaitForMultipleEvents( 
             numOfClntSock, hEventArr, FALSE, WSA_INFINITE, FALSE);
   startIdx = posInfo - WSA_WAIT_EVENT_0;
   for( int idx = startIdx; idx < numOfClntSock; ++i)
   {
      int sigEventIdx = 
          WSAWaitForMultipleEvents(1, &hEventArr[i], TRUE, 0, FALSE);
      // signaled 이벤트 오브젝트들 체크하기
      if((sigEventIdx == WSA_WAIT_FAILED || sigEventIdx == WSA_WAIT_TIMEOUT))
         continue;

      sigEventIdx = idx;
      WSAEnumNetworkEvents(
         hSockArr[sigEventIdx], hEventArr[sigEventIdx], &netEvent);

      if(netEvents.lNetworkEvents & FD_ACCPET){
         //accept 처리
         newEvent = WSACreateEvent();
         WSAEventSelect(hClntSock, newEvent, FD_READ|FD_WRITE|FD_CLOSE);
         hEventArr[numOfClntSock] = newEvent;
         hSockArr[numOfClntSock] = hClntSock;
         ++numOfClntSock;
      }
      if(netEvents.lNetworkEvents & FD_READ){
         //read 처리
      }
      if(netEvents.lNetworkEvents & FD_WRITE){
         //write 처리
      } 
      if(netEvents.lNetworkEvents & FD_CLOSE){
         WSACloseEvent(hEventArr[sigEventIdx]);
         closesocket(hSockArr[sigEventIdx]);
         --numOfClntSock;
         // 배열의 빈칸을 메꾸는 작업 필요
      }     
      ...
}


WSAEventSelect 의 정체성

이 통지방식은 다소 애매한 구석이 있다. 우선 이벤트 오브젝트를 사용하여 signal을 체크한다는 점이 동기랑 비슷한 점이 있어보인다. 그리고 wait함수로 이벤트가 발생할 때까지 대기한다는점이 blocking같기도 하다. 하지만 select나 epoll처럼 유저가 리소스를 사용하여 체크하는 것이 아니라 Wait함수를 사용하여 이벤트가 발생할 때 활성화 된다는 점이 비동기 방식에 가깝다고 생각한다. 

WSAWaitForMultipleEvent() 함수에서 timeout 옵션이 있기 때문에 select나 epoll 처럼 어떻게 사용하느냐에 따라 blocking 방식으로 사용할 수도 non-blocking 방식으로 사용할 수도 있다. 하지만 다른 점이 있다면, blocking 방식을 사용해도 멀티플렉싱이 가능하다는 점이다. 하나의 Wait에서 여러개의 I/O를 동시에 감지하고 있기 때문에, 쓰레드 하나만 wait를 통해 blocking한 상태에서 대기시키면 멀티플렉싱이 가능하다. 결과적으로는 Blocking 이면서 Non-Blocking이기도 하다.


Overlapped I/O



지금까지 미적지근한 비동기 통지방식들...

앞서 말한 윈도우의 Select형식들은 어딘가 비동기라고 하기엔 허전한 느낌이 있다. 분명 유저와 커널의 관계는 비동기 형태인데 구동방식은 여전히 동기형인 Select의 구조를 따라가고 있기 때문이다. Select를 사용한  I/O의 구조를 요약하면 선체크 후I/O이다. 먼저 I/O가 가능한 상황인지 보고, 그다음에 I/O 동작을 하는 거다. WSA***Select 시리즈들은 통지 방식이 다를 뿐이지, 실제 I/O하는 코드를 보면 이 동작 구조를 그대로 가져온다. 비동기에서 굳이 이 형식을 따라갈 필요가 있을까? 비동기란 자고로 일단 시키고 끝나면 연락받으면 되는거 아닌가? 

WSAAsyncSelect도 그렇고 WSAEventSelect도 그렇고 지나치게 Select의 구현에 급급한 나머지 정작 중요한 비동기의 미덕을 잃은건 아닌가하는 생각이 든다. 그래서 만들어졌는지는 모르겠지만 Overlapped I/O는 정말 비동기스럽다. 일단 I/O명령을 날리고, 끝나면 signal을 받는다. 뿐만 아니라 끝났을때 특정 함수를 실행하게 할 수도 있다. 마치 엄마한테 "7시에 깨워줘, 그리고 운 다음에 밥도 해줘"라고 말하는 것과 같다. 유저는 신경쓰지 않아도 알아서 동작하는 그야말로 비동기의 진정한 모습이라 할 수 있다.


Overlapped I/O 사용법

위에서 말한 것처럼 Overlapped I/O는 따로 select없이 Send와 Recv만으로 이전 통지모델들이 하는 일을 할 수 있다. 따라서 이전과는 다른 Send와 Recv함수, WSASend와 WSARecv를 사용한다. 이전에 EventSelect에서 사용했던 이벤트 오브젝트와 유사한 Overlapped 오브젝트의 주소를 WSASend와 WSARecv를 호출할때 인자로 넘겨서 완료상황에 대한 통지를 그 Overlapped 오브젝트의 signal로 받는 것이다. 그리고 overlapped I/O 기능을 수행할 수 있는 소켓도 따로 만들어 주어야한다. 그래서 전체적인 순서를 말하자면 아래와 같다.


Overlapped I/O 가능한 소켓을 만든다.


SOCKET WSASocket(int af, int type, int protocol, //기존 socket과 똑같음 LPWSAPROTOCOL_INFO lpProtocolInfo,//생성되는 소켓의 특성정보 구조체 주소 GROUP group, //함수의 확장을 위해 예약된 매개변수, 보통 0 을 전달 DWORD dwFlags //소켓의 속성정보 전달, Overlapped I/O라면 WSA_FLAG_OVERLAPPED ); //성공시 소켓 핸들 실패시 INVALID_SOCKET 반환


네번째 다섯번째 인자는 아직 몰라도 좋으니 NULL과 0을 넣어주자. overlapped I/O용 소켓이라면 마지막 인자에 WSA_FLAG_OVERLAPPED 를 넣어주면 준비 완료.

Overlapped I/O 함수를 호출한다. WSASend나 WSARecv나 전체 구조는 유사하니 WSASend만 진행한다.

int WSASend( SOCKET s, // 소켓 LPWSABUF lpBuffer, // WSA버퍼 구조체 배열 or 주소 DWORD dwBufferCount, // 배열 길이정보 LPDWORD lpNumberOfBytesSent, //전송된 바이트 수를 받을 주소 DWORD dwFlags, // 함수의 데이터 전송 특성 설정값, 잘모르면 0 LPWSAOVERLAPPED lpOverlapped, // 위에서 언급한 Overlapped 구조체 주소 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;


새로운 구조체가 많이 등장해서 당황할 수 있지만 별거 없다. 함수 아래에 명시한 것 처럼, WSABUF는 버퍼에 길이까지 포함한 단순한 구조체이고, WSAOVERLAPPED에서 1번째~4번째 변수는 운영체제가 쓰거나 다른 곳에서 예약한 변수들이고 우리가 실제 사용하는것은 WSAEventSelect에서 익숙해진 WSAEVENT 밖에 없다. 이벤트 객체를 통해서 통지를 받는 것이므로 WSASend의 6번째 즉 overlapped 구조체의 주소를 넣어주지 않으면 일반 블로킹 함수처럼 동작하게 되므로 주의해야한다. 

어차피 사용되는 이벤트 구조체는 WSAEventSelect와 같으니 signal을 확인하는 방법에 대해서는 더 이상의 설명은 생략한다. 이제 신기술 Complete Routine에 대해 이야기 해보자. 앞서 설명한대로 요청한 I/O가 끝나면 호출되는 함수를 미리 집어 넣는 방법이다. 항상 아래의 시그니처로 구현해야한다. 


void CALLBACK CompRoutine(DWORD dwError, //에러 정보
                          DWORD szRecvBytes, //전달받은 바이트수
                          LPWSAOVERLAPPED lpOverlapped, //이벤트를 담은 overlapped 구조체
                          DWORD flags //플래그 값
                          );


인자들을 활용해 끝난 후의 동작을 정의하면 알아서 I/O 끝나면 이 함수를 호출한다. 예를 들면 상대방의 입력을 계속 받고 싶으면 완료루틴에서 다시 WSARecv를 호출해 주면 된다. 개인적으로 Wait하고 시그널 까는 앞선 방법보다 쿨한 방법이라고 생각한다. 그런데 다른 작업을 진행중인데 갑자기 다른 함수가 호출되어 스택이 넘어가는것은 부적절하다. 따라서 Complete Routine은 정해진 상황, Alertable wait 상태에서만 호출된다. 그래서 다음단계가 필요하다.


대기 함수를 호출하여 Alertable 상태로 대기한다. Alertable wait 상태로 만드는 함수들은 WaitForSingleObjectEx, WaitForMultipleObjectEx, WSAWaitForMultipleEvents, SleepEx 이다. 구체적인 사용방법은 직접 찾아보는 것을 추천한다. 어쨋든 WSA가 붙는 대기함수의 마지막 매개변수에 TRUE를 붙여줘서 호출하거나 Ex가 붙는 대기함수를 호출하면 함수가 블로킹 되면서 Alertable Wait 상태가 된다. 

해당 함수에서 Alertable Wait하는 동안 미리 시켜놓은 Overlapped I/O가 종료되면 Complete Routine에 넣어둔 콜백함수가 실행된다. 그리고 Wait상태로 Blocking하던 대기함수들은 WAIT_IO_COMPLETION을 반환한다. Wait함수들은 보통 timeout 설정도 가능하니, 영원한 블로킹에 빠질 염려는 하지 않아도 좋다.


서버에서 Overlapped I/O 사용 예

... server_socket = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED); // overlapped I/O 전용 소켓 생성 ... WSADATA dataBuf; char buf[BUF_SIZE] = {0,}; WSAOVERLAPPED overlapped; //완료루틴으로 통지받으면 hEvent 안넣어줘도 알아서 들어간다. WSAEVENT evObj = WSACreateEvent(); //완료루틴으로만 통지받으면 별로 쓸모 없다. 더미 dataBuf.len = BUF_SIZE; dataBuf.buf = buf; if(WSARecv(client_socket, &dataVuf, 1, &recvBytes, &flags, &overlapped, CompRoutine) == SOCKET_ERROR) // Overlapped I/O 요청 { //에러 처리 } ... int idx = WSAWaitForMultipleEvents(1, &evObj, FALSE, WSA_INFINITE, TRUE);//alertable wait ... void CALLBACK CompRoutine( DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags) { if(dwError != 0) { //에러 처리 } else { //완료 후 처리 } }


Overlapped I/O의 정체성

이미 너무 많은 것을 말해버린듯 하다. 비동기 형태로 작동한다. 그리고 WSAEventSelect와 동일하게 Wait하는 방법에 따라서 Blocking도 가능하고 Non-Blocking도 가능하다. 완료루틴을 사용한다면 루프마다 잠시 SleepEx등을 사용하는 방법을 생각해 볼 수 있다. 


'컴퓨터 > Server' 카테고리의 다른 글

Reactor / Proactor 패턴  (0) 2015.04.12
windows IOCP 기초  (3) 2015.04.06
select 와 epoll  (0) 2015.03.28
I/O 모델 : blocking, non-blocking, 동기, 비동기  (10) 2015.03.27
TCP의 연결 종료  (0) 2015.03.27