C++/소켓통신

MFC에서 소켓을 이용한 파일전송기 만들기(5장-업그레이드버전)

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




이제 5장이 정리되어 올리게 된 것을 매우 기쁘게 생각합니다..
글 올릴려고 데브피아 왔다가 아래의 몇몇 글을 읽고 저는 매우 분개하였습니다. 이 
글을 작성하는 데 시간이 얼마나 걸릴 것 같습니까? 다 강좌를 시작한 제 잘못이지
요. 일단은 저질러 놓은 일은 책임을 지겠습니다마는, 정말 힘빠지는 글을 보고야 말
았습니다.(허접이는 그 양반에게 공개적인 사과를 강력히 요구한당!!)

------------------------------------------------------
제5장 파일전송기 - 그 두번째 버전
------------------------------------------------------

음...
먼저, 김현승 시삽님의 요청을 도저히 모른체 할 수 없어 OnMessagePending함수에 
대한 이야기를 조금 하고 시작하도록 하겠다.

--------- OnMessagePending 함수 -------------------
여러분은 소켓의 동작은 기본적으로 블로킹모드로 수행된다는 것을 알 것이다. 블로
킹모드란 무엇인고 하니, 소켓이 동작하는 동안은 그 동작이 완료되기 까지 프로세스
는 다른 일을 아무것도 할 수 없다는 말이다. 만일, 소켓의 동작이 무엇인가 잘못되
어 무한루프를 돌게 된다면, 그 시스템은 죽은 거나 다름 없게 된다. 즉, 이 동안에 
프로세스는 아무런 메시지도 접수하지 못하며, 이를 처리하지도 못하는 것은 물론이
다. 이런 일을 예방하기 위하여 OnMessagePending이라는 함수가 사용된다. 이 함수
는 동기소켓인 CSocket의 멤버함수이며, 비동기 소켓인 CAsyncSocket에서는 호출
할 수 없다.

OnMessagePending함수는 블로킹모드의 소켓동작이 진행중인 동안에도 메시지 큐로
부터 메시지를 검사하고, 이를 처리할 수 있도록 해주는 동기소켓이 제공하는 매우 
중요한 인터페이스이다. 즉, 블로킹 동작의 수행중에도 다른 강제적인 메시지 펌핑 
수단을 쓰지 않더라도 이 함수를 오버라이드 함으로써, 우리가 블로킹 동작중에 처리
하고 싶은 어떤 윈도우즈 메시지라도 처리할 수가 있는 것이다.
예를들어, 우리가 만일 블로킹 동작중에 윈도우즈 메시지 중에 WM_PAINT 메시지를 
처리하기를 원한다고 해보자.(물론, 다른 어떤 윈도우즈 메시지도 처리해 줄 수 있
다. 방법은 똑 같다).
그러면, OnMessagePending함수를 오버라이드해서, 이 함수 내에서 WM_PAINT 메시
지를 기다리고, 이 메시지가 도달하면, 원하는 일을 하도록 해 놓으면 된다. 코드는 
다음과 같이 된다.

BOOL CSocket::OnMessagePending()
{  
     MSG msg;
// WM_PAINT 메시지를 찾는다.
     if(::PeekMessage(&msg, NULL, WM_PAINT, WM_PAINT, PM_NOREMOVE))
  
         // 메시지가 발견되면 메시지 큐로부터 메시지를 제거하고, 이를 위한 동
작을 수행한다.
         ::PeekMessage(&msg, NULL, WM_PAINT, WM_PAINT, PM_REMOVE);
        
     // 여기에 메시지 핸들러를 넣어준다.

         return FALSE; 

     return CSocket::OnMessagePending();
 }

이를 이용하면, 소켓에 타임아웃을 설정해 줄 수 있다.(주의 - 이후의 이야기는 
VC++ 2.2 이후의 버전에서만 사용할 수 있다) 
이를테면, 소켓에 타임아웃을 설정하는 함수를 만든다(단순히 SetTimer를 호출하면 
된다). 그리고 타이머를 해제하는 함수를 만든다(역시 KillTimer를 호출한다). 그
런 후, 블로킹 동작 바로 앞에서 타이머를 세팅해 놓고, 동작의 직후에 타이머를 해
제시킨다. 그리고 나서 OnMessagePending함수를 오버라이드해서 WM_TIMER 메시지
를 기다리고, 메시지가 발견되면 CancelBlockingCall함수를 호출해 버리면 블로킹 
동작을 강제로 종료시키게 된다. 만일 타임아웃 시간이 되기 이전에 블로킹동작이 리
턴하면 강제종료는 일어나지 않는다. 타임아웃이 되도록 블로킹 동작이 리턴하지 않
고 있다면 당연히 강제 종료될 것이다. 이처럼 CancelBlockingCall함수에 의해 소
켓 동작이 종료되면, 종료된 그 함수는 에러를 리턴하고, GetLastError함수에 의해
서는 WSAEINTR가 리턴되는데 이것은 "어떤놈이 방해를 해서 작업을 완료하지 못했음
(ㅡ,.ㅡ;)"이란 뜻이다. 

이 함수의 동작을 이해하기는 매우 쉬워보인다. 그런데, 실제 구현할 때에는 좀더 깊
이 생각해 보아야 한다. 예를 들어, 우리의 예제에서 Send와 Receive는 루프를 돌
면서 파일을 전송하는 데, 타임아웃 처리를 위해서 OnMessagePending을 오버라이드
했다고 하면,  루프의 중간에 강제로 종료될 수 있다. 그리고 그 다음의 상황은 나
는 장담할 수 없다.

만일 여러분의 소켓에 타임아웃을 넣어주고 싶다면, Connect함수나, Accept 함수
에 대해서 사용하도록 하자. Send나 Receive함수에 대해서 타임아웃을 걸려면, 그 
함수들의 동작을 세심히 고려해 보고 나서 타임아웃을 걸도록 하자. 이경우 강제 종
료되면, 반대편 소켓도 블로킹 동작이 리턴하지 않고 있을 수 있으므로(사실은 어느 
한쪽에 문제가 있어서 타임아웃이 걸렸겠지만), 이 쪽도 타임아웃 처리를 해 주어야 
한다.

이제 대충은 OnMessagePending함수에 대해 알았을 것이다. 더 알고 싶다면, 직접 
공부하라.

이제, 본론으로 들어가서, 우리가 4장에서 만든 파일전송기를 업그레이드 시켜보자.

1. CWinThread 상속 클래스를 프로젝트에 추가한다.
위저드를 사용해서 CWinThread의 상속 클래스를 추가한다. 

class CSocThread : public CWinThread

그리고는 이 클래스에 접속된 소켓객체를 물려둘 수 있도록 두개의 멤버변수를 추가해
야 한다. 하나는 소켓의 핸들이고, 하나는 소켓 객체의 인스턴스이다. 다음과 같이 
이들을 추가한다.

SOCKET m_hSocket;  // 소켓의 핸들
CClntSocket m_ClntSocket;  // 소켓객체의 인스턴스

그리고 CClntSocket(억셉트 소켓객체)에 스레드객체를 멤버로 추가하는 데, 이것
은 스레드와 소켓을 바인드하는 용도이다. 다음과 같이 스레드 객체를 CClntSocket
에 추가한다.

CWinThread * m_pThread; // 스레드 객체의 포인터

2. 리슨소켓을 업그레이드 한다.
이제, 4장에서 만들어 보았던 허접버전의 리슨소켓(CServSocket)의 OnAccept 함수
를 고치자. 먼저 이 함수에서 메인 다이얼로그에 보냈던 메시지를 완전히 삭제한다
(이젠 필요가 없다)
그리고 함수를 다음과 같이 고친다.

void CServSocket::OnAccept(int nErrorCode) 
{
    // TODO: Add your specialized code here and/or call the base 
class
    CSocket soc;
    Accept(soc);
    
    // 접속을 처리할 스레드를 생성한다
    CSocThread *pThread = (CSocThread *)AfxBeginThread
(RUNTIME_CLASS(CSocThread),
        THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED);
    if(!pThread)
    {
        soc.Close();
        TRACE("Thread could not be created.\n");
        return;
    }

    // 접속한 소켓을 분리해서 소켓의 핸들을 스레드에게 넘겨준다
    pThread->m_hSocket = soc.Detach();

    // 생성한 스레드를 실행시킨다
    pThread->ResumeThread();

    CSocket::OnAccept(nErrorCode);
}

위의 코드에서는 접속요청이 들어오면, 접속할 소켓을 생성하고 접속을 받아들인 후
에, 스레드를 생성하는데, 이 스레드를 서스펜디드 모드로 생성하였다. 여기서, 3장
에서 이야기 한대로, 첫번째 인자는 우리가 만든 스레드 객체의 런타임클래스로 캐스
트한 포인터를 전달하였다. AfxBeginThread함수의 두번째 인자는 스레드의 프라이
어리티를 설정하는 것인데, 이것은 CPU타임을 다른 스레드와 나누어 쓸 때에, 어느정
도의 우선순위를 갖고 나누어 쓰는 가를 지정해 주는 것이다. 즉, CPU타임을 동일한 
간격으로 나누어 쓸 것인지, 다른 스레드보다 더 많은 시간을 할당받도록 할 것인지
(혹은 그 반대)를 설정할 수 있는 것인데, 위에서 전달한 인자 
THREAD_PRIORITY_NORMAL는 메인 프로세스와 같은 우선순위로 CPU타임을 나누게 한
다는 뜻이다. 일반적으로, 스레드의 우선순위를 지정하는 것은 나누어 쓰는 CPU타임
의 간격을 지정할 수는 있지만, 그 사용순서의 우선순위를 지정하지는 못한다. 즉, 
스레드는 실행순서에 대해서는 보장할 수 없다는 말이다. 어떤 스레드를 다른 스레드
보다 반드시 먼저 실행되게 해야할 필요가 있다면, 그 스레드가 작업하는 동안 다른 
스레드를 최면 상태로 만들어 줄 필요가 있다.
스레드 구동함수의 3번째 인자는 스레드가 사용할 스텍사이즈를 바이트 단위로 지정
한다. 3장에서, 프로시저(main 함수)가 구동될 때에, 운영체제가 프로시저를 위한 
스텍을 할당한다고 이야기 했었을 것이다. 이미 알고 있는 바와 같이 스레드는 또 하
나의 main함수 이므로, 이 스레드를 위해서도 스텍이 할당되어야 한다. 이 인자로 0
을 전달하면, 이 스레드를 구동시킨 상위 스레드와 같은 사이즈의 스텍이 할당된다.
(특별한 일이 없는한, 이렇게 사용한다)
네번째 인자는 크리에이트플래그 라는 것인데, 스레드를 어떤 상태로 생성할 것인지
를 지정한다. 디폴트는 0인데, 이것은 생성과 동시에 스레드를 구동하라는 것이 된
다. 우리의 코드에서는 CREATE_SUSPENDED를 전달하였는데, 이것은 생성하자마자 바
로 구동하지 말고 좀 기다리라는 뜻이다. 우리는 스레드를 구동하기 전에 스레드의 
멤버를 초기화 해 주기 위해 서스펜디드 모드로 생성하였다. 이렇게 생성된 스레드객
체의 포인터는 pThread 에 의해 참조된다.
그리고 나서는 스레드 구동함수의 에러를 체크하고 있는데, 이 부분은 대충 이해하리
라 믿는다.
함수의 마지막 부분에서는 접속을 받아들여 연결된 소켓의 핸들을 분리하여, 스레드
에게 넘겨주고 있다. (이 부분이 중요하다)
소켓객체의 멤버함수인 Detach 함수는 소켓객체로 부터 소켓 핸들을 분리하고, 그 
핸들을 리턴한다. 이 리턴된 핸들을 스레드의 멤버변수에 넣고 있다. 이제, 연결된 
소켓은 리슨소켓으로부터 스레드로 옮겨졌다.
그리고 나서 스레드를 구동시키기 위해 ResumThread함수를 호출하고 있다.

3. 스레드 객체에서 소켓을 받는다.
이제, 스레드에서 전달받은 소켓객체를 자신의 멤버객체에 붙여야 한다. 스레드 객체
의 InitInstance 함수를 다음과 같이 오버라이드한다.

BOOL CSocThread::InitInstance()
{
    // TODO:  perform and per-thread initialization here
    // 리슨 소켓으로부터 넘겨받은 소켓을 자신의 멤버 소켓에 붙인다.
    m_ClntSocket.Attach(m_hSocket);
    m_ClntSocket.m_pThread = this;

    return TRUE;
}

위의 함수를 보면, 리슨소켓으로 부터 전달받은 접속속켓을 Attach함수를 사용해서 
자신의 멤버에 붙이고 있다. 이렇게 해 주어야만 스레드내에서 소켓이 독립된 객체로
서 동작하게 된다. 이를 테면, 메인 프로세스에서 소켓을 생성하고, 접속받은 다음
에 스레드를 생성한 후 스레드내에서 이 소켓의 멤버함수를 호출하더라도, 결국에 
그 동작은 메인 프로세스에서 하는 것이 되기 때문에, 아무 소용이 없는 것이다. 소
켓을 스레드로 분리해 내기 위해서는 꼭 이와 같이 Detach와 Attach함수를 사용해야
만 한다.

4. 스레드 동기화를 위한 멤버를 추가한다.
위와 같이 하였다면, 리슨소켓은 항상 접속요청이 있을 때마다, 새 소켓을 생성해서 
접속을 억셉트한 뒤, 새 스레드를 생성하고 접속된 소켓을 스레드에 넘긴 후 스레드
를 구동한다. 즉, 소켓의 통신은 전적으로 새로이 생성된 스레드에서 전담하게 되는 
것이다. 그런데, 스레드마다 파일을 열고 전송해야 하므로, 스레드를 동기화 해 주어
야 한다. 이를 위해서, 메인 다이얼로그에 동기화 객체 크리티컬섹션을 추가한다.

CCriticalSection m_CriticalSection;  // 퍼블릭 멤버이다.(다 알고 있겠지
만)

그리고, 억셉트용 소켓객체에 크리티칼섹션 객체의 포인터를 멤버로 추가한다. 즉, 
CClntSocket에 다음의 멤버를 추가하자.

CCriticalSection * m_pCriticalSection;

이제, 다이얼로그의 동기화 객체를 접속된 소켓이 참조하도록 해 주어야 하는데, 이
것은 리슨소켓에서 스레드를 생성하기 전에 해 주는 것이 가장 좋겠다. 그러자면, 리
슨 소켓이 다이얼로그를 참조할 수 있어야 한다. 이를 위해서 CServSocket에 CWin 
* 형 멤버를 추가한다.
CWin * m_pWnd;
그리고, 이 변수를 초기화 할 퍼블릭함수를 추가한다.

void CServSocket::SetDlg(CWnd *pWnd)
{
    m_pWnd = pWnd;
}

그리고 나서 OnAccept함수의 스레드를 구동하기 전에 다음의 구문을 추가한다.

CFTPServerDlg *pDlg = (CFTPServerDlg *)m_pWnd;
pThread->m_ClntSocket.m_pCriticalSection = &pDlg->m_CriticalSection;

이제, 우리의 접속소켓은 주 프로세스의 크리티칼섹션 객체를 참조할 수 있으며, 리
슨소켓에 의해 생성된 모든 접속 소켓은 이 동기화 객체를 참조하게 될 것이므로, 스
레드간 동기화가 이루어 질 수 있는 것이다. 우리는 위에서 리슨소켓의 멤버를 추가
하였으므로, 이에 적당한 값을 전달하는 코드를 메인 다이얼로그에 추가해야 한다. 
메인 다이얼로그의 OnInitDialog함수에 다음의 코드를 추가한다.

m_pServerSocket->SetDlg(this);

이제 어느정도 준비가 되었다. 이제, 스레드를 추가하였고, 애플리케이션은 접속요청
이 있을 때 마다 새 접속을 처리할 스레드를 생성하고 이를 구동할 것이다. 따라서, 
다중접속을 지원하게 된 것이다.
(위의 설명에서 헤더파일의 적절한 인클루드에 관한 설명은 빠져있다. 코드를 작업하
면서, 이 부분은 여러분이 직접 하길 바란다. 예를들어, 동기화객체를 위해서는 
afxmt.h를 인클루드하여야 하고, 각 객체를 사용할 때 적절한 헤더를 인클루드하는 
것을 빠뜨리지 않기를 바란다)
스레드를 시작하기 전에, 클라이언트에게 억셉트하였다는 메시지를 전달하도록 하
자. 이 부분에서 서버는 메시지를 전달하고, 클라이언트로부터의 확인 응답 메시지
(acknowledgement Message)를 요구한다. 이것은 대부분의 연결지향의 프로토콜에
서 사용하는 수순인데, 우리도 이것을 함 해보자는 것이다. 스레드를 구동하기 전 부
분에 다음의 코드를 추가한다.

int nFlag;
nFlag = 1000;
soc.Send(&nFlag, 4);

위의 코드는 1000이라는 값을 가진 INT 값을 전송하는데, 클라이언트가 자신이 받
은 값이 1000 이면, 서버가 억셉트했다는 신호를 보낸것으로 약속을 하는 것이다.

5. 파일 전송부의 코딩
이제 가장 핵심적인 부분이 남았다. 바로 파일을 전송해야 하는 부분이다. 리슨소켓
이 접속을 대기하다가, 접속요청을 억셉트하고 나면, 소켓 통신은 억셉트된 소켓의 
소관이다. 따라서, 파일전송부분은 CClntSocket에서 코딩해야 한다. 스레드가 생성
되는 시점을 생각해 보자. 이것은 클라이언트가 접속을 요청한 다음의 시점이다. 스
레드가 생성되고 나서 어느시점에서 파일전송을 시작할 것인가?
이것은 여러가지 방법을 생각할 수 있지만, 우리는 클라이언트가 접속요청을 하고, 
접속이 억셉트되었다는 것을 알고나면, 파일을 보내달라는 신호를 서버에 전달하고, 
서버는 이를 받았을 때에 파일전송을 시작하도록 하겠다.
이를 위해서 CClntSocket의 OnReceive함수를 오버라이드한다.

void CClntSocket::OnReceive(int nErrorCode) 
{
    // TODO: Add your specialized code here and/or call the base 
class
    int nFlag;
    Receive(&nFlag, 4);

    if(nFlag == 1000)    // 파일을 전송해 달라는 요청이다
    {
        nFlag = 1500;
        Send(&nFlag, 4);    // 파일을 전송할 것을 알려준다
        
        int NameLength;
        int nTotalLen;
        CString strFileName;
        CString strFilePath;
        CFile sendFile;

        m_pCriticalSection->Lock();
        strFilePath = (LPCTSTR)*m_pFilePath;
         sendFile.Open(strFilePath, 
CFile::modeRead|CFile::typeBinary);
        nTotalLen = sendFile.GetLength();

        NameLength = m_pFilePath->GetLength();
        m_pCriticalSection->Unlock();

        Send(&NameLength, 4);    // 파일이름의 길이를 전송
     
        char * strName = new char[NameLength];
        byte* data = new byte[4096];
 
        m_pCriticalSection->Lock();
        strFileName = sendFile.GetFileName();
        m_pCriticalSection->Unlock();

        strName = strFileName.GetBuffer(NameLength);
        Send(strName, NameLength);    // 파일의 이름을 전송
        Send(&nTotalLen, 4);    // 전체 파일길이를 전송

        DWORD dwRead;

        m_pCriticalSection->Lock();
        do
        {
            dwRead = sendFile.Read(data, 4096);
            Send(data, dwRead);
            
        } while (dwRead > 0);
        m_pCriticalSection->Unlock();

         sendFile.Close();
        strFileName.ReleaseBuffer(-1);

        strName = NULL;
        delete strName;
         delete data;
    }

    CSocket::OnReceive(nErrorCode);
}

위의 함수 첫부분에서는 클라이언트의 응답을 먼저 확인하고 있다. 이 약속에 맞도
록 클라이언트측에서 전송을 해 주어야만 파일을 전송받게 된다.
위의 코드에서 우리는 원래 다이얼로그에 있던 FileSend함수를 접속된 소켓에다가 
옮겨놓았다. 여기에 전체 파일의 크기를 전송하는 부분이 하나 추가되어 있다. 이 부
분은 클라이언트에서 파일을 전송받는 부분을 살펴보면 이유를 알게 된다. 
그런데, 파일의 경로를 가지고 있는 문자열이 필요하다. 따라서, CClntSocket에다
가 CString형 포인터를 멤버변수(m_pFilePath)로 추가하고, CServSocket에서, 스
레드를 구동하기 전에, 다음의 구문을 추가한다.

pThread->m_ClntSocket.m_pFilePath = &pDlg->m_strTransFile;

이 포인터는 사실 다이얼로그의 멤버변수를 가리키고 있으므로, 이 멤버를 참조할 때
에도 동기화 객체로 둘러싸 주어야 한다는 것에 주의한다.

이제 서버측의 코딩이 끝났는가? 아니당.. 소켓이 접속을 끝낼때 생성한 스레드를 종
료시켜야 하는 일이 남았다. CClntSocket의 OnClose함수를 오버라이딩하자.

void CClntSocket::OnClose(int nErrorCode) 
{
    // TODO: Add your specialized code here and/or call the base 
class
    m_pThread->PostThreadMessage(WM_QUIT, 0, 0);

    CSocket::OnClose(nErrorCode);
}

OnClose함수는 소켓의 연결이 종료되었을 때에 프레임 웍에 의해 호출된다. 따라
서, 접속이 종료되면, 이 함수 내에서 통신을 위해 생성되었던 스레드를 종료하고, 
스레드가 사용하던 리소스를 시스템에 반환하도록 해 주어야 한다. 우리가 생성한 스
레드의 포인터를 스레드의 멤버인 억셉트 소켓이 가지고 있다. 이를 이용해서 스레드
를 종료시키는 것이다.
이제 서버는 다중접속을 지원하므로, 파일을 전송하고 있는 상태라는 것을 사용자에
게 알리는 것은 의미가 없다.

이제 필요한 것은 무엇인가? 이제, 서버측의 통신부분이 수정되었으므로, 클라이언트
측의 통신부분도 수정되어야 한다. 맨 먼저, 서버에게 파일을 전송해 달라는 요청을 
해야한다.

전송시작 버튼의 핸들러를 수정하여 접속요청한 후 접속에 성공하면, 파일전송요청
을 보내도록 하자. 말은 그럴듯하지만, 사실은 1000이라는 int값을 전송하면 된다. 
우리는 이 1000 이라는 값을 파일전송요청으로 미리 예약해 놓은 것이다(서버측에서 
이야기 했듯이). 일종의 우리들만의 프로토콜이 되는 셈이다. 서버는 이 값을 전달받
으면, 파일을 보내달라는 말로 인식하고 파일을 전송할 것이다.
그리고, 클라이언트도 ReceiveFile함수를 삭제하고, 소켓객체의 OnReceive함수를 
오버라이드하여 파일을 전송받는 코드를 소켓으로 옮긴다.
파일전송부분은 서버측에서 수정된 것과 정확히 대응되도록 수정되어져야 한다. 이 
과정에서 클라이언트측에 두개의 사용자 메시지 펌핑을 추가하였다. 위의 설명을 참
고하면, 클라이언트측 소스를 살펴보는데 무리는 없을 것이다. 주목할 것은 파일을 
전송받는 루프가 

do
{          
    dwRead = Receive(data, 4096);
    destFile.Write(data, dwRead);
    nReceiveLen += dwRead;
    if(nReceiveLen >= nTotalLen)
        break;
} while (dwRead > 0);

와 같이 전송받은 길이를 체크해서, 다 받은게 확실하다면 루프를 탈출하도록 해 놓
은 것이다. 이렇게 해 주어야 코드가 안정된다.

이제 다 되었다...
코딩이 끝났다면 컴파일하고 실행해보자. 동작하는 모습은 이전 장에서와 다름 없어 
보이지만, 중요한 차이를 발견할 수 있을 것이다.

클라이언트를 여러개 동시에 실행시키고, 이전장에서 해 보았듯이 동시에 파일을 전
송받도록 해보자.
아마도, 실행 중간에 웬 알수없는 (보기만 해도 겁나는) 메시지 박스를 띄우면서 애
플리케이션들이 우수수 전사할 것이다. 여기서 퀴즈!!! --왜 그런지 이유를 말해보시
라...(헉, 결코 내가 실수한 것은 아니당..)
바로 그렇다. 이번의 서버는 다중접속을 지원하고 있는 것이다. 클라이언트들은 동시
에 파일을 전송받고 있으며, 또한 동시에 전송받은 파일을 쓰고 있다. 여기서 충돌
이 일어나는 것이다. 4장의 예제에서는 결코 이런일이 없었다. 이 문제를 해결하기 
위해서는 클라이언트들이 서로 다른 경로에서 실행되어야 한다.

두번째 테스트로, 좀 큰 파일을 전송시켜놓고, 전송하는 동안 서버 애플리케이션의 
윈도우를 옮겨보자. 부드럽게 움직일 것이다. 서버는 소켓통신을 새로 생성한 스레드
에게 전적으로 일임하고 있으므로, 주 프로세스는 자신에게 전달되는 메시지를 여유
있게 처리하고 있는 것이다.
그러나 아직 슬픈 사실이 남아있다. 클라이언트는 여전히 전송받는 도중에는 잠들어 
있다. 왜냐하면, 클라이언트에서는 소켓이 스레드로 분리되지 않았기 때문이다.(만
일 클라이언트도 잠에서 깨어나게 하고 싶다면, 직접 해보시길.)

우리가 위에서 구현한 다중접속은, 주지하다시피 접속을 처리하기 위해 스레드를 생
성한다. 시스템에서 동시에 구동시킬 수 있는 스레드의 수에는 분명제한이 있으므로
(이론상은 그렇지 않다고 할지라도), 이 서버는 동시에 매우 많은 접속을 처리하는 
대용량의 서버가 될 수 없다. 작은 소그룹에서의 서버로 적당하다. 서버측에서 다중
접속을 처리하는 방식에는 여러가지가 있다. 물론 이 강좌에서는 다 이야기 하지 않
는다.(ㅡ.ㅡ;) 스스로 공부하는 기쁨을 얻으시길...

이제, 이번 장까지 해서 우리는 다중접속까지 구현해 보았다. 이쯤하면, 왠만한 분이
라면 다음 장은 읽을 필요가 없을 지도 모른다. 그러나 나는 초급자 분들을 위해서, 
다음장에서 또 다시 코드를 추가하여 서버가 여러개의 파일을 전송대기하고, 클라이
언트는 이들 중에서 자신이 원하는 파일을 골라서 다운 받도록 해 보겠다.(ㅡ,.ㅡ y)

그럼 다음장을 기약하자....(또 아래와 같은 글이 보일 시에는 담장 안올린당)