C++/소켓통신

MFC에서 소켓을 이용한 파일전송기 만들기(4장-허접버전스타트)

gandus 2011. 5. 16. 17:11
출처 : 데브피아 - 최훈익 님의 강좌

http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=51&MAEULNo=20&no=1455&ref=653



이제 4장이 정리되어 올립니다. 4장에서는 가장 원초적인 모습의 파일전송기를 만들
어 보게 됩니다. 예제소스를 첨부하여 올렸습니다.
많은 분 들이 소스를 달라고 요청들을 하셔서... 물론 이해는 하지만, 전 강좌를 만
드는 것 외에도 할일이 있답니다. 지금은 거의 노이로제에 걸릴 지경입니다. 마음이 
급하시더라도 강좌의 단계를 따라 기다려 주시면 정말 감사하겠습니다.

------------------------------------------
제4장 파일전송기-그 첫번째 버전
------------------------------------------

이제 우리는 파일전송기의 코딩에 들어간다. 이번강좌에서는 매우 허접한 파일전송
기 프로그램을 하나 만들어 볼 것이다. 강좌의 단계를 마추기 위해서 원래 목적하고 
있는 애플리케이션을 내가 얼마나 허접한 버전으로 바꾸어 놓았는지 잠시 설명하겠
다.
이 프로그램은 서버측에서 파일다이얼로그를 열고 전송할 파일 하나를 선택한다. 그
리고 전송대기 버튼을 눌러서 서버소켓이 Listen하도록 한다. 이 시점에서 클라이언
트측에서 전송시작버튼을 눌러서 파일전송이 시작된다. 하나의 파일을 전송하고 나
면 애플리케이션의 동작은 끝난다. 이 시점에서 서버측에서는 대기상태를 해제하고, 
다른 파일을 선택하고 다시 전송대기할 수 있다. 그러면 클라이언트는 다시 전송시작
을 눌러서 다시 전송을 시작할 수 있다.

이 프로그램은 다중접속을 지원하지 않는 것은 물론, 한번에 한개의 파일만 전송할 
수 있다. 하지만, 훌륭하게 자신의 임무를 수행한다. 파일전송기의 윤곽을 잡아볼 
수 있을 것이다.
이 강좌에서 코딩의 모든 과정을 이야기 하는 것은 무리이다. 예제를 참고하여 소스
를 분석해보기 바란다. 강좌에서는 중요한 부분만 설명할 것이다.

1. 서버측의 코딩
새 프로젝트를 시작한다. 1단계에서 다이얼로그 베이스드를 선택하고, 2단계에서 액
티브x 관련항목의 체크를 해제한 뒤 피니쉬한다. 프로젝트의 이름은 FTPServer라고 
하였는데, 여기서의 FTP는 File Transfer Program의 약자이다(결코 FTP 프로토콜
을 이야기 하는 것이 아니므로, 혼동하지 말자)

소스의 리소스 항목을 참고하여 메인 다이얼로그의 템플릿을 디자인 한다. 그리고 프
로젝트에 두개의 CSocket 상속클래스를 추가하는데, 하나는 리슨소켓을 위한 것이
고, 하나는 억셉트소켓을 위한 것이다.(소켓 클래스의 이름은 여러분이 자유로이 설
정할 수 있다. 자신이 코드를 살펴볼 때, 잘 알아볼 수 있는 이름이면 된다)

리슨 소켓에는 멤버변수 2개를 추가한다. 하나는 메인다이얼로그의 핸들을 저장할 
HWND형의 변수이고, 하나는 억셉트 소켓의 인스턴스로, 접속요청을 받았을 때 억셉
트하기 위한 것이다.
메인다이얼로그의 핸들은 소켓객체가 작업하는 과정에서 메인다이얼로그로 메시지를 
전달하기 위해서 필요하다. 각 변수의 선언은 다음과 같다.

public:
  HWND m_hWnd;
  CClntSocket m_ChildSock;

이제, 멤버에 억세스하는 함수를 추가한다. 먼저, 윈도우핸들 멤버변수에 값을 할당
해 주는 함수로, void SetWnd(HWND hWnd);와 같은 퍼블릭함수를 추가한다.

void CServSocket::SetWnd(HWND hWnd)
{
    m_hWnd = hWnd;
}

그리고, 억셉트된 소켓을 리턴하는 함수를 추가한다.

CClntSocket * CServSocket::GetAcceptSocket()
{
    return &m_ChildSock;
}

위의 코딩은 여러분의 구미와 필요에 맞추어 추가하거나 삭제하는 것이 가능하다. 예
를 들어, 메인 윈도우의 핸들을 저장할 필요가 없다면(메인윈도우와 교통하기 위해
서 꼭 위에서 처럼 핸들을 저장하고 있을 필요는 없다. 다른 방법도 있다) m_hWnd
와 SetWnd(...)는 필요없는 것이 된다.
이제, 서버의 리슨 소켓에서 해줄 일은 하나가 남았는데, 이것이 가장 중요하다. 바
로 OnAccept 소켓메시지 핸들러의 오버라이딩이다. OnAccept는 리슨소켓이 Accept
함수를 호출하여 접속요청을 받아들일 수 있다는 것을 알리기 위해 프레임웍에 의해 
호출된다. 즉, 클라이언트측으로부터의 Connect 요청이 왔다는 것을 알려주는 메시
지의 핸들러 인 것이다.(통상 그 이름때문에, Accept가 호출된 후에 호출되는 메시
지핸들러로 착각하기 쉽다) 따라서, 클라이언트측에서 Connect 요청이 들어왔을 때
의 처리를 이 함수를 오버라이딩해서 작성해 놓으면 된다.

void CServSocket::OnAccept(int nErrorCode) 
{
    // TODO: Add your specialized code here and/or call the base 
class
    Accept(m_ChildSock);

    SendMessage(m_hWnd, WM_ACCEPT_SOCKET, 0, 0);

    CSocket::OnAccept(nErrorCode);
}

위의 코드에서는 맨 처음 억셉트할 소켓의 인스턴스를 사용해 접속요청을 받고, 메인
다이얼로그에 접속을 받았다는 메시지를 날려주는 것으로 동작이 끝난다. 그러면, 메
시지를 전달받은 메인다이얼로그에서 나머지 처리를 해주는 것이다. 여기서 전달하
고 있는 메시지 WM_ACCEPT_SOCKET는 윈도우 운영체제의 메시지가 아니라, 우리가 
특별히 정의한 메시지이다. 즉, 사용자 정의 메시지이다. 이와 같이 사용하기 위해서
는 먼저, 리슨소켓의 헤더파일에서,
#define WM_ACCEPT_SOCKET WM_USER+1
과 같이 이것이 유저 메시지라는 것을 정의해 놓아야 한다. WM_USER뒤에 +와 숫자
를 써서 여러 사용자 메시지들을 구분하는데, 뒤에 붙이는 숫자는 마음대로 지정할 
수 있다. 하지만, 자신이 알아보기 쉬운 규칙을 부여하는 것이 좋다. 이를테면, 리
슨소켓이 날리는 메시지는 100단위, 억셉트 소켓이 날리는 메시지는 200단위, 등
등... 물론 우리의 예제는 그럴 필요가 없다.

위와 같이 사용자 메시지를 정의해 놓았으면, 이제 메시지 전송함수를 써서 정의된 
메시지를 날려주면 된다. 메시지 전송함수는 두가지가 있는데, 이것을 알아보자.
위에서 사용한 SendMessage함수는 매우 유용한 함수이다. 첫번째 인자는 메시지를 
받을 윈도우의 핸들, 두번째 인자는 전달할 메시지, 세번째와 네번째 인자는 WPARAM
과 LPARAM 인데, 이들은 부메시지들로서 데이터형은 각각 UINT와 LONG이다.
SendMessage함수는 메시지를 전달하고 나서 그 메시지를 받는 윈도우가 메시지를 처
리할 때까지 리턴하지 않는다. 즉, 메시지를 날리고 나서 잠시동안 대기상태에 들어
간다는 것이다. 이 특성은 우리가 사용자 인터페이스를 위해서 나중의 장에서 유용하
게 사용할 것이다.
이 함수 외에 PostMessage라는 함수가 있다. 이 함수는 SendMessage함수와  용법
이 같다고 생각하면 된다. 중요한 차이점은 메시지를 날리고 나서 바로 리턴한다는 
것이다. 즉, 메시지를 날리는 데에만 관심이 있지, 메시지가 어떻게 처리되는 가는 
관심 밖이다. 그래서 메시지를 전달하고, 바로 다음 단계를 수행할 필요가 있을 때 
이 함수를 사용한다. 상황에 따라, 적절한 메시지 함수를 사용하면 될 것이다.

위와 같이 메시지를 날렸으면, 메시지를 받는 쪽에서 메시지를 처리해 주는 루틴이 
반드시 있어야만 메시지가 효력을 발휘한다. 이 방법을 알아보자. 이경우에는 메인 
다이얼로그가 메시지를 받았으므로, 메인다이얼로그 클래스에서 메시지를 처리해 주
어야 한다.
메인다이얼로그의 소스코드의 처음부분을 보면 
BEGIN_MESSAGE_MAP(CFTPServerDlg, CDialog) 이라고 된 부분을 볼 수 있을 것이
다. 그 아래에 //}}AFX_MSG_MAP 라고 된 부분의 아래에 다음과 같은 구문을 추가
해 준다.

..........
    //}}AFX_MSG_MAP
    ON_MESSAGE(WM_ACCEPT_SOCKET, OnAccept)
END_MESSAGE_MAP()

위의 구문은, WM_ACCEPT_SOCKET 이란 메시지(우리가 정의해둔 메시지)가 전달되면 
그 메시지를 처리하기 위해 OnAccept라는 멤버함수를 호출하라는 이야기 이다. 이처
럼 메시지와 그 핸들러 함수를 연결하는 것을 메시지 매핑이라고 한다.(여기서의 
OnAccept함수는 우리가 메인다이얼로그에 추가할 멤버함수이지, 소켓객체의 멤버함
수가 아니라는 것에 주의한다)
메시지 매핑을 정의했으면, 다이얼로그의 헤더파일로 가서,

    //{{AFX_MSG(CFTPServerDlg)
    virtual BOOL OnInitDialog();
    afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
    afx_msg void OnPaint();
    afx_msg HCURSOR OnQueryDragIcon();
    ......................
와 같은 부분을 찾는다. 다이얼로그가 함수를 호출할 수 있도록 여기에 메시지 핸들
러함수의 원형을 정의해 주어야 한다. 위와 같이 되 잇는 부분의 함수정의들이 적힌 
부분의 맨 아래에 다음의 함수 정의를 추가한다.

afx_msg LONG OnAccept(UINT wParam, LONG lParam);

위에서 afx_msg라는 구문은, 이 함수는 메시지핸들러로 사용할 함수라는 것을 컴파
일러에게 알려준다.

위의 작업을 했으면, 이제 메시지를 처리할 준비가 다 되었다. 이제 필요한 것은 이 
함수의 실제 정의를 해주는 것이다. 메인다이얼로그의 소스코드에 다음의 함수 정의
를 추가한다.(클래스위저드를 사용해서 멤버함수를 추가하지 말자. 위저드는 이 함수
가 헤더에 정의된 것을 보고, 이미 함수가 있다고 띵깡을 부릴것이다)

LONG CFTPServerDlg::OnAccept(UINT wParam, LONG lParam)
{
    m_pClientSocket = new CClntSocket;
    m_pClientSocket = m_pServerSocket->GetAcceptSocket();
    m_pClientSocket->SetWnd(this->m_hWnd);

    m_strStatus = "파일을 전송중입니다";
    UpdateData(FALSE);

    FileSend(m_pClientSocket, m_strTransFile);
    m_pClientSocket = NULL;
    delete m_pClientSocket;

    m_strStatus = "파일전송이 완료되었습니다";
    UpdateData(FALSE);

    return TRUE;
}

코드를 살펴보면, 이 함수가 무슨일을 하는지를 알 수 있을 것이다. 맨처음 억셉트
된 소켓을 리슨소켓으로 부터 (그 포인터를)전달받고, 사용자에게 이제 파일전송을 
시작한다는 것을 보여주고는 실제 파일을 전송할 함수 FileSend를 호출한다. 함수
가 리턴하면, 다시 사용자에게 파일전송이 끝났다는 것을 보여준다. 이제, 왜 소켓클
래스에서 메인 다이얼로그로 메시지를 전달했는지 알 것이다. 소켓이 억셉트한 시점
에서 다이얼로그의 멤버를 조작할 필요가 있었기 때문이다(물론, 이 방법말고 다른 
방법을 사용할 수 잇다. 어쩌면 우리는 다음 장들에서 그 다른 방법들을 보게 될 것
이다)
이제, 파일을 전송할 FileSend함수를 다이얼로그 클래스에 추가해 준다. 이때는 위
저드를 사용하는 것이 편리하다. 다음과 같은 함수를 추가하자.

void CFTPServerDlg::FileSend(CClntSocket *accSocket, CString 
strFilePath)
{
    CString strFileName;    // 파일이름을 저장할 임시 변수
    CFile sendFile;        // 전송할 파일의 핸들

    // 전송할 파일을 연다. 이때는 전체경로를 사용한 것에 주목하자.
     sendFile.Open(strFilePath, CFile::modeRead|CFile::typeBinary);
     // 파일이름의 길이를 저장한다
    int NameLength = strFilePath.GetLength();
    // 먼저, 파일이름의 길이를 전송한다
    accSocket->Send(&NameLength, 4);        
     
    char* strName = new char[NameLength];    // 파일이름을 담을 버퍼
를 할당
    byte* data = new byte[4096];            // 파일데이터
를 담을 버퍼를 할당
 
    strFileName = sendFile.GetFileName();        // 파일 이름을 
얻는다.
    strName = strFileName.GetBuffer(NameLength);    // 파일이름을 
버퍼로 옮긴다.
 
    accSocket->Send(strName, NameLength);    // 파일이름을 전송한다.

    DWORD dwRead;    // 이부분은 1장에서 설명하였다
    do
    {
        dwRead = sendFile.Read(data, 4096);
        accSocket->Send(data, dwRead);       
    } while (dwRead > 0);

 
     sendFile.Close();
    strFileName.ReleaseBuffer(-1);    // 파일이름 변수를 버퍼를 회수한
다.

    strName = NULL; // 이 포인터는 CString 객체의 문자열버퍼를 가리키
고 있으므로,
    delete strName;  // 바로 딜리트를 호출하면 안된다.
     delete data;

    accSocket->Close();
}

위의 소스를 살펴보면, 1장에서 예로든 루틴이 그대로 사용되고 있음을 알 수 있다. 
위에서는 파일이름을 위해서 따로 버퍼를 잡고, CString객체로 부터 버퍼의 포인터
를 얻어서 사용하였는데, 물론 1장의 예에서와 같이 이 부분을 단지 (LPCTSTR)로 캐
스트하여 사용할 수도 있다. 이렇게 하는 것이 가장 편할 것 같은데도 위와 같이 우
회하는 방법을 쓴 것은, 캐스트하여 전송한 파일이름에는 이상하게도 파일이름 뒤에 
알수없는 데이터가 추가로 붙어있는 것을 보았기 때문에(이유는 아직 알아내지 못했
다) 이를 해결할려고 한것이다. 이를 해결하는 방법은 또 있기는 한데, 위와 같이 하
는 것이 가장 좋은 듯 하다.

여기서, Send와 Receive함수의 설명이 부족하였던 것을 보충하자.
Send함수는 소켁객체의 멤버로, 세개의 인자를 받는데, 첫번째 인자는 전송할 데이
터를 담고있는 버퍼의 포인터, 두번째 인자는 버퍼의 길이를 바이트수로 나타낸 int 
값, 세번째 인자는 전송방식을 지시하는 플래그로 int 값인데 디폴트로 0이 지정되
어 있다.
Receive함수는 Send함수와 똑같은 인자를 받는다. 단지 버퍼가 전송받은 데이터를 
저장할 버퍼의 포인터라는 것만 다르다.
두 함수는 전송측과 수신측에서 정확히 1:1로 대응되어 호출되어야만 한다. 만일 전
송측에서 A만큼 크기의 B형의 데이터를 보냈다면, 수신측에서도 A만큼 크기의 버퍼
에 B형과 호환되는 타입으로 전송순서에 정확히 일치하도록 수신해야 하는 것이다.
아마도 여러분은 소켓프로그래밍을 할 때에는 두개의 비주얼 스튜디오를 띄워놓고 작
업해야 할 것이다. 하나는 전송측을 코딩하고, 다른 하나는 수신측을 코딩한다. 한쪽
에서 수정이 가해졌다면, 곧바로 다른 한 쪽도 대응되는 수정이 가해져야 한다.

이상의 설명을 참고하면, 예제소스를 이해하는 데 아무 무리가 없을 것으로 사료된
다. 물론, 클라이언트 측에서 서버의 IP 주소를 지정하는 부분 - 그러니까 
Connect 함수를 호출하는 부분 - 에서의 IP 주소는 여러분의 환경에 맞추어서 바꾸
어 주어야 한다. 여러분이 직접 이와 비슷하게 코딩을 해보기를 바란다.
이제, 소스를 작성하는 것이 끝났다면, 컴파일하고, 실행해보자.

인터넷(혹은 다른 형태의 네트워크)에 연결되어 있다면, 서로 다른 컴퓨터간에 파일
전송이 이루어 지는 것을 목격하게 될 것이다.
혹시, 두 대 이상의 컴퓨터에서 코드를 테스트 할 수 없는 분은 그냥 자기의 컴퓨터
에서 서버와 클라이언트를 동시에 구동시켜놓고, 테스트 해 볼수 도 잇다. 이때는 네
트워크환경에서 TCP/IP의 등록정보에서 "고정된 IP주소사용"으로 설정해 놓은뒤에 
IP주소를 맘대로 설정해 준다.(각 항목은 0에서 255사이의 값으로 해야 한다.) 서브
넷마스크의 값은 대개 255.255.255.0 으로 한다. 그리고, 재부팅한 후 코드를 시험
해 볼 수 있다. 만일 어떤 인터넷 서비스(adsl이나 isdn과 같은)에 가입되어 잇는 
컴퓨터에서 위와 같이 테스트해 볼 경우에는 테스트 한 후에 이 설정을 원래대로 바꾸
어 놓아야만 인터넷 서비스가 문제없이 동작할 것이므로 주의한다. 

제대로 동작하는 지 시험하기 위해 .mp3파일을 혹시 가지고 잇다면 좋은 시험파일이
다. 이 파일을 전송하게 한 후에 전송받아서 새로 만들어진 파일을 실행해 보자. 파
일이 재생되는가?
그렇다면 해낸 것이다. 파일을 전송하고서, 데이터가 손상되지 않았다는 것을 확인
한 것이다.

두번째 테스트로, 사이지가 50MB 이상되는 동영상 파일같은 것을 한번 전송시켜 보
자. 전송하는데 시간이 좀 걸릴 것이다.(그렇다고 하더라도 50MB를 전송하는 속도 
치고는 우리의 애플리케이션은 매우 뛰어난 성능을 보이고 있다) 이렇게 시간이 걸리
면 전송하는 동안 사용자의 입력(예를들어 타이틀바를 클릭하고 드래그하여 윈도우
를 옮기는 것을 시도해 보자)에 대해 전혀 반응하지 않는다. 잠자는 숲속의 공주가 
된 것이다....

세번째 테스트로 두개이상의 클라이언트 애플리케이션을 동시에 실행시켜놓고, 서버
를 대기상태로 놓은 다음 재빨리 연속적으로 클라이언트들이 (가능한한 동시에)파일
을 전송받도록 해보자. 클라이언트윈도우의 텍스트는 분명 "파일을 전송받는 중입니
다"하고 쓰여 지겠지만, 맨 처음 전송받는 클라이언트를 제외하고는 모두 대기상태
로 있는 것이다. 파일의 전송은 순차적으로 끝나게 될 것이다. 즉, 다중접속은 지원
되고 있지 않다.

하지만, 이프로그램은 "파일을 전송한다"는 자신의 고유의 소임을 충실히 이행해 주
고 있다.
우리는 앞으로의 장에서, 이 소스를 고치고 코드를 추가하여 다중접속을 지원하게 하
고, 여러개의 파일을 보내도록 하고, 사용자 인터페이스를 개선할 것이다.