본문 바로가기

MFC

[MFC] CSocket Timeout 구현하기

출처 : http://blog.daum.net/art_code/677984


타 임아웃 처리가 된 클라이언트 프로그램을 만들어 보도록 하겠다. 이 장을 제대로 이해 하기 위해서는 C++의 클래스 상속 및 재정의(overriding)에 대한 이해가 필수적이다. 이것은 C++의 기초에 해당하는 것이므로 여기서는 별도로 설명하지 않겠다. 

이전에 만들어본 프로그램은 정말로 가장 간단한 형태를 구현했기 때문에 CSocket의 인스턴스를 직접 생성해서 사용했지만, 타임 아웃 처리 등을 하기 위해서는 그렇게 해서는 안된다. CSocket 클래스를 상속받아 새로운 클래스를 만들고 이 클래스의 몇가지 함수를 overriding(재정의)해야 한다. 그리고 이 새로운 클래스의 인스턴스를 생성시켜서 사용해야 한다. 

CSocket 클래스는 전에도 얘기했지만 동기화 소켓이고 Connect, Send, Receive 함수 등을 호 출했을 때 바로 처리가 되지 않으면 멈춰진 상태(블록된 상태)로 있게 된다. 그래서 CSocket에는 블록된 상태에서 어떤 메시지가 발생하면 이를 감지하고 OnMessagePending이라는 함수를 호 출해 주는 기능이 있다. 그런데 우리가 이 OnMessagePending 함수 내에서 어떤 처리를 해주기 위해서는 이 함수를 overriding해야 하고, 그러기 위해서는 CSocket 클래스를 상속받아 자신만 의 클래스를 새로 만들어야 하는 것이다. 

함수를 호출했을 때 블록될 수 있는 함수는 Connect, Send, Receive 정도이다. 먼저 어떤 경우에 타임 아웃이 필요한지 실제로 예를 들어 보여주도록 하겠다. 

만약 야후 서버로 접속하되 실수로 1000번 포트로 접속을 시도했다고 가정하자. 지금 바로 도스 창이나 터미널창을 열어서 “telnet kr.yahoo.com 1000”을 입력하고 엔터를 쳐보자. “Trying 211.32.119.135…”이라고 나오더니 아무리 시간이 지나도 아무런 응답이 없을 것이다1
Ctrl-C 를 눌러 그냥 종료하도록 하자. 물론 1~2분 정도가 지나면 텔넷 프로그램이 타임아웃을 걸어서 종료될 수도 있지만 그것은 텔넷 프로그램에서 접속을 포기하고 타임아웃 처리를 한 것이다. 실제 우리가 만든 프로그램에서는 뭔가가 잘못되었을 때 영영 블록된 상태에서 빠져나올 수가 없다. 

예 를 들면 Connect 함수를 호출하여 서버로 접속을 시도했는데, 성공도 실패도 아닌 아무런 응답도 없는 경우에 블록될 수 있다. 또한 서버는 아무런 데이터도 보내줄 생각을 안하는데 클라이언트에 서 Receive 함수를 호출한 경우에도 블록된다. Send 함수 호출 역시 블록될 수 있다. 정말 무한 정 대기 상태에 빠지게 된다. 

이런 경우에 대한 해결책으로 타임아웃을 처리하는 예제를 만들어 보기로 한다. 우리가 제일 먼저 만들어볼 프로그램은 야후에 1000번 포트로 접속하되 5초 동안 Connect 함수가 반환되지 않는 다면 강제로 종료시켜 버리는 것이다. 방금 테스트해보아서 알겠지만 야후 웹서버로 1000번 포트 에 접속하면 아무런 응답이 없이 블록될 것이다. 이 자리를 빌어 좋은 테스트 환경을 제공해준 야 후 코리아 측에도 감사를 표하는 바이다. 

Visual C++을 이용해서 새로운 프로젝트를 만들되 Dialog based(대화상자 기반)으로 하고 역시 버튼 하나만 위치시키도록 하자. 필자는 프로젝트 이름을 TimeOut이라고 하였다. 혼동을 피하기 위해 되도록이면 필자와 같은 이름을 사용하도록 하자. 물론 처음 프로젝트를 생성할 때 “Windows 소켓 ”에 체크하는 것을 잊지 말자. 다음과 같이 각자 재량에 따라 만들면 되겠다.

clip_image007

역시 정말 간단한 프로그램이다. “접속 시도 ” 버튼을 누르면 야후 웹서버의 1000번 포트로 접속을 시도하고 5초가 지나면 타임아웃이 되는 프로그램이다. 그런데 실제 코드를 작성하기전에 해야할 일이 있는데 CSocket 클래스를 상속하여 새로운 소켓을 하나 만드는 일이다. 
필자는 CSocket을 야후에 1000번 포트로 접속했을 때 아무런 응답이 없는 이유는 야후 측의 방화벽 설정 때문이다. 야후 웹서버의 80번 포트를 제외한 포트는 보안을 위해 모두 접속을 막아버렸고, 이로 인해 아무런 응답이 없다. 방화벽 설정은 보통 거부와 무시가 있는데 이 경우는 무시이다. 더 자세한 것은 방화벽 관련 문서(iptables 등)를 참조하기 바란다. 

상속한 클래스를 CDataSocket이라는 이름으로 만들었다. 그러면 CDataSocket 클래스의 선언 및 구현 파일인 DataSocket.h와 DataSocket.cpp 파일이 생겼을 것이다. 

우리가 CSocket 클래스를 상속한 이유는 OnMessagePending 함수를 overriding하기 위해서 이다. 그 함수를 다음과 같이 재정의하자. 

BOOL CDataSocket::OnMessagePending() 

    MSG Message; 
    if (::PeekMessage(&Message, NULL, WM_TIMER, WM_TIMER, PM_NOREMOVE)) 
    { 
       if (Message.wParam == 10) 
      { 
         ::PeekMessage(&Message, NULL, WM_TIMER, WM_TIMER, PM_REMOVE); 
         CancelBlockingCall(); 
         Close(); 
      }
    } 
    return CSocket::OnMessagePending(); 
}

만약 여러분이 Win32 API 프로그래밍에 익숙하다면 위의 코드를 아무런 부담없이 이해할 것이지만 MFC로만 프로그래밍하던 분이라면 이해하기 좀 힘들지도 모르겠다. 만약 위의 코드가 잘 이해가 되지 않는다면 김상형씨의 “윈도우 API 정복” 책의 윈도우, 메시지 파트 부분을 참고하기 바란다. 참 잘 나와있는 책이다. 한마디 덧붙이자면 아무리 MFC로 프로그래밍을 하더라도 Win32 API를 제대로 모르고서는 프로그래밍을 잘 할 수가 없다. 방금 소개한 김상형씨의 “윈도우 API 정복 ”은 초/중급 수준에서 필자가 추천하는 책이다. Win32 API를 처음 공부해보겠다면 그 책을 보는 것도 괜찮을 것이다.
 
OnMessagePending 함수는 Connect, Send, Receive 등의 함수 호출로 블록된 도중에 어떤 메시지가 발생하면 자동으로 호출되는 함수이다. 이것은 CSocket 클래스에 의해 제공되는 기능이다. 

OnMessagePending 함수 안에 overriding한 코드를 간략하게 설명해 보자면 PeekMessage(&Message, NULL, WM_TIMER, WM_TIMER, PM_NOREMOVE)함 수는 현재 메시지 큐에 WM_TIMER 이벤트가 있는지 확인하는 것이다. 확인만 하고 WM_TIMER 메시지는 메시지 큐에 그대로 남겨둔다. 만약 WM_TIMER 메시지가 메시지 큐에 있다면 다시 이 메시지의 WPARAM값이 10인지 확인한다. 

WM_TIMER가 발생했을 때 WPARAM에는 첫번째 인자는 메시지 구조체를 입력받을 변수의 포인터, 두번째 인자는 메시지를 받을 윈도우의 핸 들인데 NULL을 지정하면 현재 쓰레드의 모든 메시지를 대상으로 한다. 그리고 세번째와 네번째는 어 떤 메시지를 체크할 것인지에 대한 일종의 범위를 지정한 것이다. 

WM_TIMER부터 WM_TIMER까 지로 정의했으니 이것은 WM_TIMER 메시지만 정확하게 체크하라는 것이다. 마지막 PM_NOREMOVE는 메시지 확인만 하고 메시지 큐에서 메시지를 지우지는 말라는 의미이다. 자세한 것은 MSDN을 참고하기 바란다. 

타이머의 ID가 들어간다. 즉 타이머의 ID가 10번인지 확인하는 것이다. 만약 타이머의 ID가 10이 맞으면 메시지 큐에서 WM_TIMER 메시지를 삭제하고 CancelBlockingCall과 Close 함수를 호출한다. CancelBlockingCall은 현재 블록된 상태로 있는 함수를 취소하는 함수이고 Close 함수는 소켓을 닫아버리는 것이다. 즉 타임아웃이 되면 블록된 함수를 취소하고 접속을 끊어버리는 것이다. 

이렇게 OnMessagePending 함수를 재정의한 후에 실제로 타임아웃을 이용한 프로그램을 작성해 보도록 하자. 우선 TimeOutDlg.cpp(혹은 여러분의 다이얼로그 클래스 구현 파일)에 #include “DataSocket.h”를 써줘야 한다. CSocket의 인스턴스를 생성할 것이 아니라 CSocket을 상속받은 CDataSocket의 인스턴스를 만들 것이기 때문에 이 클래스의 선언 파일을 포함시켜줘야 한다. 이제 “접속 시도 ” 버튼에 대한 이벤트 핸들러를 만들고 다음과 같이 코드를 입력한다. 

void CTimeOutDlg::OnBnClickedButtonConnect() 

    CDataSocket socket;

    if (!socket.Create(0)) 
    { 
        MessageBox(”소켓 생성에 실패했습니다.”, “에러”, MB_OK | MB_ICONWARNING); 
        return; 
    }

    SetTimer(10, 5000, NULL);

    if (!socket.Connect(”kr.yahoo.com”, 1000)) 
   { 
       KillTimer(10); 
       socket.Close();

       MessageBox(”타임 아웃되었습니다.”, “에러”, MB_OK | MB_ICONWARNING);

       return; 
   }

   KillTimer(10);

   MessageBox(”서버에 접속되었습니다.”, “알림”, MB_OK);

   socket.Close(); 


위의 코드가 타임아웃을 구현한 간단한 예이다. 여기서는 Connect 함수가 블록된다는 가정하에 Connect 함수가 5초동안 반환하지 않으면 강제로 Connect 함수 호출을 취소하도록 한 것이다. 소켓을 생성(Create)하는 부분은 이전과 다를 것이 없다. 주의해서 볼 부분은 Connect 함수를 호출하는 전후이다. 즉 SetTimer 호출 부분부터 if 문 다음의 KillTimer 부분까지가 핵심이다. 

두가지 시나리오를 생각해 보자. 우선 Connect 함수가 성공적으로 호출되어 지체없이 TRUE를 반환하는 경우를 생각해 보자. 이 경우 SetTimer 함수로 ID 10번, 시간 간격 5초의 타이머를 설치하고, 바로 Connect 함수 를 호출하게 된다. 그리고 이 Connect 함수가 TRUE를 반환하기 때문에 if 문 안의 코드는 실행되지 않고 바로 if 문 다음으로 가서 ID 10번의 타이머를 파괴하고는 계속 다음의 작업을 계속 할 것이다. 이런 경우 Connect 함수가 5초 안에 충분히 끝날 것이므로 WM_TIMER 메시지는 발생하지 않는다 1

이번엔 Connect 함수를 호출했을 때 블록된다는 시나리오를 생각해 보자. 일단 SetTimer 함수 로 타이머 ID 10번, 시간 간격 5초 2의 타이머를 생성했다. 시간 간격을 5초로 했기 때문에 5초 뒤에 WM_TIMER 메시지가 발생할 것이다. 그리고 Connect 함수를 호출하였는데, 이 부분에서 블록된 상태로 있을 것이다. 

그래서 5초가 지나도 Connect 함수가 반환하지 않고 블록되어 있기 때문에 5초 뒤에 WM_TIMER 메시지가 발생하게 된다. 블록된 상태에서 이렇게 메시지가 발생하 면 OnMessagePending 함수가 바로 호출되게 된다. 그래서 그 메시지 안에서 WM_TIMER 메 시지가 발생했고 ID가 10번임을 확인하고는 CancelBlockingCall을 호출하여 Connect 함수를 취소하고 Close 함수를 호출하여 소켓을 닫아버리게 되는 것이다.

CancelBlockingCall을 호출 하면 블록되어 있던 Connect 함수는 FALSE를 반환하게 된다. 참고로 Send나 Receive 함수 의 경우엔 CancelBlockingCall을 호출하면 SOCKET_ERROR를 반환하게 된다. 어쨌든 이런 방식으로 동기화 소켓에서의 타임아웃을 처리하는 것이다. 

Connect 함수에 대한 타임아웃만 예로 보였지만 Send, Receive 함수 모두 같은 방식으로 처리 하면 된다. 

위의 프로그램을 컴파일해서 실행해 보면 다음과 같이 버튼을 누른뒤 5초 뒤에 타임아웃이 되는 것을 볼 수 있을 것이다.

clip_image008

참고로 버튼을 누르고 나서 바로 윈도우를 이동시키려 하면 움직여지지 않을 것이다. 이것은 Connect 함수에서 블록되어 있기 때문이다. 소켓을 통해 데이터 송수신을 바쁘게 하는 도중에도 SetTimer 함수로 타이머를 설치했을 때 첫 M_TIMER 메시지는 함수를 처음 호출했을 때가 아 니라, SetTimer를 호출하고 지정한 시간이 지난 다음에 발생한다. 타이머는 한번 설치해놓으면 지정 한 시간 간격으로 계속 WM_TIMER 메시지를 발생시키므로 사용했으면 바로 KillTimer 함수로 파 괴해주어야 한다.
 
SetTimer의 2번째 인자는 WM_TIMER 메시지가 발생할 시간 간격인데 단위가 ms(밀리 세컨드, 1000분의 1초)이다. 따라서 5초 간격의 WM_TIMER 메시지를 발생시키려면 5000을 넘겨주면 된 다. 참고로 윈도우 95/98/ME에서 최소 시간 간격은 55ms이고, 윈도우 NT/2k/XP에서는 10ms이 다. 시간 간격에 한계가 있음을 참고로 알아두자. 

윈도우 이동이라든가 최대/최소화 등이 부드럽게 되도록 하려면 데이터 송수신 작업을 쓰레드로 만들어야 한다. 이것은 나중에 다시 알아보도록 한다. 


'MFC' 카테고리의 다른 글

[MFC] 정적 DLL 만들기(스크랩)  (0) 2014.02.03
[MFC] CSocket(비동기) Error정리  (0) 2014.01.22
[MFC] HRESULT 반환값  (0) 2014.01.16
[MFC] Web Browser Control FAQ  (0) 2013.11.21
[MFC] MFC에서 메일보내기  (0) 2013.11.06