본문 바로가기

프로그래밍/API

IOCP의 AcceptEx 모델~

IOCP에는 기본적인 accept() 또는 connect()의 확장 모델이 있다. 이 모델은 비동기 형태의 처리를 하게 하여,
IOCP의 사용시 일관된 처리를 유지할수 있게 만들어 준다.
 -- 굳이 확장함수가 아니라도 WSAAccept()와 같은 함수만 사용해서도 구현이 가능하다.

여기에서는 IOCP에 어떻게 AcceptEx() 함수를 적용시키는지 정리해보고자 한다.

1. AcceptEx()함수를 사용하기 위해

#pragma comment(lib,"ws2_32.lib")
#pragma comment(lib,"mswsock.lib")  // AcceptEx()


   가 추가되어야 한다. 이 명령은 자동적으로 라이브러리를 링크하도록 하는 명령으로, AcceptEx() 함수는
   winsock 라이브러리가 아닌 ms winsock라이브러리에 포함되어 있다.

2. listen 소켓을 생성하고 IOCP에 연결한다.
   이 과정은 일반적으로 소켓을 개방하고, listen() 을 설정하는 과정까지는 동일하다.
   여기에서 중요한 것은, listen() 을 하기전,

// AcceptEx()를 사용할 경우, listen() 에서 자동적으로 accept를 받지 못하도록 한다.
BOOL on = TRUE;

if (setsockopt( fd, SOL_SOCKET,  SO_CONDITIONAL_ACCEPT, (char *)&on, sizeof(on)))
    return -1;
...
return listen( fd, 0);


을 수행하는 것이다. 전에도 한번 이야기 했지만, 이 처리를 수행하지 않을 경우, AcceptEx() 가 동작중이지 않아도 자동적으로 listen() 이 발생되게 된다.

3. AcceptEx()를 통해 listen pool을 생성한다.
   간단히 정리하면, listen을 받을 backlog 수 만큼 AcceptEx() 를 생성해 주면 된다.

DWORD __len;

if (AcceptEx( ud->fd, ud->u.a.fd, (LPVOID)ud->u.a.buff, 0,
     sizeof(SOCKADDR_STORAGE) + 16, sizeof(SOCKADDR_STORAGE) + 16, &__len, __o) == FALSE)
    switch (WSAGetLastError())
    {
     case ERROR_IO_PENDING: break;
     default              : acp->do_cancel();
     {
      ::closesocket( ud->u.a.fd);
     } return FALSE;
    }

    여기에서 ud->fd는 listen()된 소켓 번호를, ud->u.a.fd는 accept()시 연결할 새로운 소켓번호를, 그리고
    ud->u.a.buff는 연결되는 소켓 정보를 저장할 버퍼를 명시하고 있다. -- 데이터를 받을 공간은 0로 명시!
    여기에서 ud->u.a.buff는

   struct _accept {
    SOCKET fd;
    BYTE buff[(sizeof(SOCKADDR_STORAGE) + 16) * 2];
   } a;

    과 같이 정의되어 있다. 그리고, __o는 OVERLAPPED 포인터이다.

    위의 연결 과정을 받고싶은 backlog 수 만큼 반복적으로 실행하면 된다. 여기서 새로 생성되는 ud->u.a.fd는 IOCP에 연결되어 있지 않아야 한다.

4. 연결이 발생되면 실제 accept과정을 수행한다.
    IOCP를 통해 새로운 연결이 발생되었다고 통보를 받으면 다음과 같은 처리를 수행해 새로운 연결을 구성한다.

  SOCKET l_fd = ud->fd;
  BOOL r = TRUE;

  /* Accept된 소켓의 Context를 Update시킨다. (대기소켓에서 활성 소켓으로 변환) */
  if ((__nil == (DWORD)-1) ||
    (setsockopt( ud->u.a.fd, SOL_SOCKET,
        SO_UPDATE_ACCEPT_CONTEXT, (char *)&l_fd, sizeof(SOCKET)) < 0)) ///// 첫번째
   acp->do_cancel();
  else {
   if (!(__w = WSASetSocket( ud->u.a.fd, NULL))) ///// 두번째
    ;
   else {
    SOCKADDR *l_addr = NULL, *r_addr = NULL;
    int       l_len = 0, r_len = 0;

    /* 접속된 클라이언트 정보를 받는다. */
    GetAcceptExSockaddrs( ud->u.a.buff, 0, sizeof(SOCKADDR_STORAGE) + 16,
      sizeof(SOCKADDR_STORAGE) + 16, &l_addr, &l_len, &r_addr, &r_len); { /// 세번째
     r = acp->do_accept( __w, r_addr); /// 네번째
    } goto _gD;
   }
  } ::closesocket( ud->u.a.fd);
_gD: return (r == TRUE) ? CALLBACK_START(ACCEPT)( acp, ud, __o): FALSE; /// 마지막으로


    여기에서 ud->fd는 listen() 소켓을 나타내며, __nil 값은 GetQueuedCompletionStatus() 함수를 통해 받은
    오류 여부를 의미한다.

    순서대로 정리하면,

     첫번째로 연결된 소켓의 Context정보를 갱신하기 위해 SO_UPDATE_ACCEPT_CONTEXT를 수행하고,
     두번째로 새로운 소켓의 관리 포인트를 생성시킨다. (이 부분은 관리 테이블이라 보면 된다)
     세번째로 GetAcceptExSockaddrs()를 통해 연결된 클라이언트의 정보를 얻어 온다.
                  여기에 저장되는 주소는, 앞에서 버퍼로 넘겨준 포인터가 저장된다.
     네번째로 연결된 소켓을 IOCP에 등록하는 처리를 수행한다.

    마지막으로 계속 listen 받기를 원한다면 3. 과정을 다시 수행하도록 한다.

이러한 AcceptEx()의 최대 장점은 서버의 최대 수용 클라이언트를 접속에서 미리 차단할수 있다는 것으로,
다양한 응용이 가능하다.  -- 즉, 더이상 AcceptEx()를 하지 않는다면 새로운 연결은 자동 발생되 않으며
이 상황에서 클라이언트가 서버로 접속을 요구하면 클라이언트는 서버 포트가 닫힌 것으로 인식이 된다.

참고로, 이렇게 listen된 소켓은 netstat로 보더라도 LISTEN정보가 나타나지 않으므로, 불안해 하지 않아도 된다.

다음에는 ConnectEx()를 사용하는 방법에 대해서도 정리해볼 생각이다... 조만간에^^;