본문 바로가기

프로그래밍/API

IOCP의 ConnectEx 모델...

IOCP의 AcceptEx() 다음으로, ConnectEx() 모델이 있다. 이 모델 또한, AcceptEx() 와 마찬가지로 비동기로
처리할수 있는 함수로 connect()시 non-block상태로 처리할수 있는 장점이 있다.

WSAConnect()대신 굳이 ConnectEx()모델이 필요한 이유는 서버의 프로세싱 과정의 block을 제거해 서버의
처리를 지속하기 위해서 이다. -- 별도의 쓰레드를 사용해 connect() 를 할수 있지만 이 방식 보다는 좀더 유용할듯 하다.

그럼, 설명을 해보겠다.

1. ConnectEx 함수 포인터를 얻을 필요가 있다.
   AcceptEx()는 별도의 함수 포인터를 얻지 않아도 바로 접근 처리할수 있지만, ConnectEx()는 얻어서 처리하였다.
   함수의 정의에 문제가 있는지 AcceptEx() 처럼 바로 접근이 안되는 문제가 있었다~

    SOCKET fd;

    if ((fd = WSASocket( AF_INET, SOCK_STREAM, 0, NULL, NULL, WSA_FLAG_OVERLAPPED)) < 0)
     ;
    else {
     GUID  guid = WSAID_CONNECTEX;
     DWORD bytes = 0;

     if (::WSAIoctl( fd, SIO_GET_EXTENSION_FUNCTION_POINTER,
         (LPVOID)&guid, sizeof(guid), (LPVOID)&ESYS->lpfnConnectEx,
         sizeof(ESYS->lpfnConnectEx), &bytes, NULL, NULL))
      ;

     :: closesocket( fd);
    }

    처럼, 임의로 소켓을 생성한후, ConnectEx() 함수를 얻을수 있다.

2. ConnectEx()를 IOCP와 연결하여 함수를 호출한다.
   AcceptEx()와 틀리게, ConnectEx()는 연결하고자 하는 소켓을 생성후 IOCP에 등록한후 사용해야 한다.

   그리고,

      SOCKADDR_STORAGE __nil;

      memset( &__nil, 0, sizeof(__nil)); __nil.ss_family = ud->u.c.addr.ss_family;
      if ((::bind( ud->fd, (LPSOCKADDR)&__nil, ud->u.c.len) < 0) || //// 첫번째
        (ESYS->lpfnConnectEx( ud->fd, (LPSOCKADDR) //// 두번쨰
           &ud->u.c.addr, ud->u.c.len, NULL, 0, NULL, __o) == FALSE))
       switch (WSAGetLastError())
       {
        case ERROR_IO_PENDING: break;
        default              : cop->do_cancel( (LPSOCKADDR)&ud->u.c.addr);
        {
         ;
        } goto _gC;
       }

    처리를 설명하면,

       첫번째로 bind() 처리가 중요한데, connect()는 서로 쌍으로 연결되어 지는 처리 이므로 자신의 연결을
                    생성하기 위해 bind()를 수행하여야 한다. -- 이때, ss_family를 제외하고 모두 0으로 초기화
                    한다.
                    -- 즉, listen이 가능한 소켓으로, listen 받을 포트는 커널이 결정하도록 한다는 의미를 갖는다.
       두번째로 ConnectEx() 함수를 호출해 실제 연결하고자 하는 주소(ud->u.c.addr)에 연결을 시도한다.

    여기에서 사용되는 소켓 주소는 다음과 같이 정의하고 있다. 이 부분은 개발자의 사용 환경에 맞춰 조정할수 있다.

   struct _connect {
    SOCKADDR_STORAGE addr; // 모든 sockaddr을 포함한다.
    int len;  // 저장 addr의 길이
   } c;

     나는 범용으로 사용하기 위해 SOCKADDR_STORAGE를 사용하였지만, 실제로는 사용하는 SOCKADDR_IN을
     사용해도 무방하다.

     이로서 기본 연결에 필요한 처리를 완료 하였다. 다음으로 IOCP로 부터 통보를 받기를 기다리면 된다.

3. 연결이 완료되면..
    IOCP로 부터 연결이 완료되었다고 통보를 받으면,

    물론, 여기에서도 __nil은 소켓에 오류를 판단하기 위해 GetQueuedCompletionStatus() 함수에서 반환되는
    오류값이다.

   if ((__nil == (DWORD)-1) ||
     (setsockopt( ud->fd, SOL_SOCKET, SO_UPDATE_CONNECT_CONTEXT, NULL, 0) < 0))
    cdp->do_cancel( (LPSOCKADDR)&ud->u.c.addr); // 연결을 페쇄 한다.
   else {
       ...
   }

   처럼, 연결된 fd의 Context를 갱신하는 것 만으로 연결이 완료 되는 것이다.

여기서 ConnectEx()의 처리 방식을 Key-point를 정리하자면 ConnectEx() 함수 호출전 미리 사용할 소켓을
bind()시켜야 한다는 것이다.

나도, 이 부분 때문에 고생을 좀 했다^^;