"MFC에서 소켓을 이용한 파일 전송기 만들기(1장--개념잡기)" [開發] 네트워크

-----------------------------------------------
제1장  윈속, 소켓, 주소, 포트
-----------------------------------------------

이 강좌의 본론을 이야기 하기 전에, 재미없는 몇가지 사항에 대해 이야기 해야만 할 
것 같다.
윈속을 이용한 네트워킹 프로그래밍이나 인터넷 프로그래밍에 관해 이야기 하고 있는 
어느 서적에서나 그 서두 부분에서는 다음의 사항들에 관해 이야기한다.

인터넷의 개요, 역사, 그 현황
TCP/IP 프로토콜 및 기타 인터넷 프로토콜
소켓의 개요와 윈속

이 강좌를 읽고 있는 분이라면, (윈도우 소켓에 관심을 갖고 있을 것이므로) 대강 위
의 내용들에 대해서는 알고있거나, 이 내용들을 위해 참고할만한 다른 방법(서적이나 
즐겨찾아가는 인터넷 사이트)이 있다고 생각한다. 사실, 위의 내용들에 대한 지식은 
실제 코드의 구현에 별 도움이 안된다. 그럼에도 불구하고, 윈속을 주무르는 프로그래
머라면 위의 내용을 알고 있어야 모양새가 갖춰진다.(사실 이 말은 거짓말인지도 모른
다. 소켓통신 프로그래밍에 대해 더 깊이 들어갈려고 하면, 위의 지식들이 얼마나 도
움이 될 것인지 내가 아직 들어가 보지 않아서 경험해 보지 않았기 때문이다. 특히 인
터넷 프로그래밍을 위해 소켓을 공부하는 사람이라면 인터넷 프로토콜에 대해서 공부
하는 것은 필수적이다)
나는 단지 소켓이라는 개념이 무엇이며 인터넷 또는 다른 방법의 네트워크 환경에서 
통신한다는 것에 대해 전혀 까막눈인 사람들, 즉 소켓이라는 말을 처음 들어본 사람들
을 위해서 간단한 개념소개와 이해하기 좋도록 단순한 비유를 들어주고는 이 기나긴 
서두의 주제를 넘어갈 생각이다. 최소한 2장은 넘어가야 하지 않겠는가?

1. 소켓과 윈속
소켓 인터페이스는 TCP/IP 네트워크 스펙에 대한 API(Application Programming 
Interface)로, 네트워크 애플리케이션이 쉽게 작성될 수 있도록 해주는 운영체제 차원
에서 제공되는 인터페이스이다. 예전의 도스와 같은 운영체제 하에서는 아마도 API의 
도움없이도 순전히 프로그래머의 노력으로 네트워크 프로그래밍이 가능했을지도 모른
다. 왜냐하면, 도스는 프로그래머가 코드상에서 직접 인터럽트를 호출하는 것을 허용
하고, 프로그래머는 직접 하드웨어에 접근할 수 있었기 때문에 네트웍 카드를 제어하
는 것도 가능할 것이다.(물론, 현재 도스 하에서 네트워킹은 불가능하다. 앞의 말은 
운영체제의 성격을 이야기 하기 위한 것이다)
그러나 윈도우즈는 코드가 직접 하드웨어(이 경우 네트웍 카드)를 제어하는 것을 허용
하지 않는다. 항상 운영체제에게 해당작업을 요청하도록 되어 있다. 그래서 윈도우즈
가 네트워킹을 하고자 하는 프로그래머에게는 소켓 API를 제공하는 것이다.

원래 소켓이란 캘리포니아 대학 버클리 분교(U.C. Berckly)에서 유닉스 운영체제를 위
해서 최초로 개발된 것으로, 애플리케이션이 하드웨어의 디스크 상에 파일을 읽고 쓰
는 것처럼 네트워크 통신을 할 수 있도록 설계되었던 것인데, 윈도우즈 소켓(윈속)은 
이를 모방하여 윈도우즈 운영체제에서 네트워킹이 구동되도록 만들어진 것이다. 초창
기의 유닉스 소켓을 버클리소켓이라고 하는 것은 이런 역사적인 배경이 있다.

여기까지 읽은 분들중 어떤 분들은 "그러니까 소켓이 머냐고요??"하는 분들이 있을지 
모르겠다. 헉.....

정리를 해보자. 소켓이란 운영체제에서 제공되는 API들 중에서 특별히 네트워킹을 위
한 API들에서 사용하는 것으로, 그 많고 많은 핸들 중의 하나이다.
만일 누군가가 파일 입출력을 구현할려고 한다면 그는 파일객체를 만들고 그 객체의 
핸들이 해당파일을 가리키게 해야 한다. 그리고 그 핸들을 사용해서 파일 IO에 관련
된 일을 하고, 작업이 끝나면 그 핸들을 닫는다. 소켓도 마찬가지로, 누군가가 네트워
크 통신을 하고자 한다면 맨 처음 소켓 객체를 만들고 그 핸들을 얻어야 한다. 통신
이 끝나면 그 핸들을 닫아 주어야 하는 것이다. 그러니까 소켓은 파일 디스크립터
(descriptor)와 유사하다고 볼 수 있다.
우리가 일상생활에서 편지를 주고 받는 것에 비유를 해보자. 편지를 보내기 위해서는 
편지를 쓰고, 우표를 붙인다음 우체통에 넣는다. 그러면 그 편지가 도착하기 까지의 
과정은 우체국에서 다 해주어야만 한다. 편지를 보내는 사람은 단지 봉투에 정확한 주
소와 이름을 적어서 우체통에 넣어주면 되는 것이다.
이 과정에서 소켓이란 우체통에 해당하며, 편지는 소켓을 통해 전송하고자 하는 데이
터에 해당한다고 볼 수 있다. 우체국은 운영체제와 인터넷서비스망에 비유할 수 있겠
다.

2. 데이터그램과 스트림
소켓은 두가지의 대표적인 형태를 갖는데, 바로 데이터그램과 스트림이다. 이것은 소
켓객체가 처음 생성될때 지정되는 것으로, 한번 지정되면 그 객체가 소멸할 때까지 바
꿀 수 없다.

데이터그램 소켓은 UDP 데이터그램을 사용해 데이터를 전송한다. 이 데이터 전송방식
은 자기 딴에는 최선을 다해서 데이터의 정확성을 보장할려고 하지만, 모든 경우에 대
해서 보장하지 못한다. 데이터그램은 전송하는 데이터가 분실될 수도 있으며(즉, 발생
한 데이터 분실에 대해 경고를 해주지 않는다는 것이다), 데이터의 순서가 뒤바뀔 수
도 있다. 또한, 데이터의 전송을 위해 두 터미널이 연결되어 있을 필요는 없다. 데이
터 전송명령이 주어지면 무조건 보내고자 하는 컴퓨터의 IP주소를 향해 데이터를 날리
는 것이다. 그리고 나서는 보낸 데이터에 대해 더이상 신경쓰지 않는다.
헉... 이 글을 읽어보니 데이터그램 소켓은 네트워킹의 암적인 존재인것 처럼 들린다.
그러나 데이터그램 소켓은 전송속도에서 이득을 얻는다. 따라서 데이터의 전송과 수신
측에서 적당히 데이터의 순서를 정렬할 수 있는 방법을 구현해 주면, 작은 데이터 흐
름을 사용하고, 전송속도가 제일 큰 이슈가 되는 경우에는 효과적으로 사용될 수 있
다.

스트림 소켓은 데이터 전송을 위해 TCP 연결을 사용한다. TCP는 기록된 데이터에 에러
가 없도록 하고, 전송 순서에 맞추어 정확히 전송되는 것을 보장한다. 만약 데이터 스
트림의 일부를 운반하는 개개의 IP패킷이 운송중에 파손되거나 순서에 어긋나거나 분
실되면, TCP는 문제가 발생한 것을 인식하고 데이터의 재전송이나 재정렬을 통해 적절
히 보상해 준다. 즉, 데이터의 정확한 전송을 보장하기 위해 속도를 희생하는 것이다.
스트림 소켓은 연결지향의 소켓으로, 전송측과 수신측이 연결되어 있지 않으면 동작하
지 않는다. 우리가 사용할 MFC 소켓클래스는 두 형식의 소켓을 사용할 수 있고, 디폴
트로 스트림 소켓을 생성하도록 되어 있다.

3. 주소
우리가 편지를 보낼때 받는 사람의 주소를 정확히 기재하지 않으면, 우체국에서는 이 
편지를 나에게 되돌려 보낸다. 즉, 배달이 불가능한 것이다.
소켓을 통해 데이터를 날릴때에도 마찬가지로, 통신하고자 하는 컴퓨터의 위치를 명시
해 주어야 한다. 이 위치는 IP Address라고 하는 주소를 통해서 지정하는데, 이에 대
해서는 잘 알고 있으리라 믿는다.
윈도우즈 소켓 API에서는 소켓을 생성하고 클라이언트 측에서 접속을 요청할 때에 서
버의 IP 주소를 명시해 주어야 한다. 서버측에서 접속요청을 받아들이면, 내부적으로 
클라이언트 측의 IP어드레스가 서버에게 전달된다.

4. 포트
위에서 소켓통신의 개념을 편지를 보내는 것에 비유했는데, 바로 이 포트를 설명하기 
위해서 이다.
어떤 사람이 편지를 보냈는데, 그 편지는 어떤 빌딩의 어느 층에 있는 어느 사무실로 
배달되는 것이라고 하자. 우편부는 봉투에 쓰여진 주소를 보고 어느 빌딩 어느 층 어
느 사무실 까지는 금방 찾아갈 것이다. 그런데 그 다음엔 어떻게 해야 하는가?? 등기 
우편이라면, 받는 사람에게 전달해 주고 그 사람의 사인을 받아야만 한다. 바로 받는 
사람의 이름에 해당하는 것이 포트이다.
우리가 소켓을 통해 데이터를 전송하면, IP 주소를 통해 해당되는 컴퓨터를 찾아갈 것
이다. 그런데, 그 컴퓨터에는 여러개의 프로그램이 실행중 일 수 있다. 그 프로그램
들 중 어느 프로그램에게 데이터를 전송해 주어야 하는가?? 이것을 지정하는 것이 포
트이다.
전송측과 수신측은 같은 포트번호를 가져야 서로 정확히 연결될 수 있고, 서로간에 대
화가 가능한 것이다.

헉......
허접하지만 어쨌든 1장을 마쳤다. 써놓고 보니 상당히 허접하다.. 문제는 이 허접한 
내용을 안 허접하도록 바꿀만한 재주가 나에게는 없다는 데에 있다.
이 글에는 내가 알고 있지 못한 수많은 오류가 포함되어 있을 지도 모르겠다...
어쨋건 1장에서는 별로 배울게 없다... 그러려니 하고 다음장으로 넘어가자...

-------------------------------------------------------------------------------
쩌업.. 처음 시작글과 이번 1장을 읽어 보신 분들중에서는 다분히 실망하신 분들이 계
실거라는 생각이 듭니다. 아직도 본론에 들어가지 않았기 때문입니다.
지금 당장 파일을 전송하고 싶은 분들을 위해서 간단히 파일을 전송하는 코드를 아래
에 적어봅니다만, 강좌가 초급자를 대상으로 하고 있는 관계로 강좌가 목표로 하는 애
플리케이션이 완성될려면 조금 더 기다리셔야 할 것 같습니다.

/**************************************************/
// 서버측에서 파일을 전송하는 코드부분
//*************************************************/

// 우리가 사용할 포트를 정의한다. 이 값은 맘대로 사용해도 되지만, 대개 1000 이상
의 포트 번호를 사용하자.
#define PORT 30000

// 맨처음 접속을 대기할 소켓을 생성한다.
AfxInitSocket(NULL);

CAsyncSocket listenSoc;
listenSoc.Create(PORT);

// 그리고는 접속요청을 기다리자.
listenSoc.Listen();

// 접속요청이 왔다면 받아들이자.
CSocket acceptSoc;  // 이 소켓은 Create를 호출하면 안된다.
listenSoc.Accept(&acceptSoc);

// 이제부터 클라이언트와 대화하는 것은 온전히 acceptSoc의 소관이다.
// 전송할 파일을 열자
CFile sourceFile;
sourceFile.Open((LPCTSTR)strFileName, CFile::modeRead | CFile::typeBinary);
// strFileName은 CString 객체로 파일의 이름을 가지고 있다.

// 파일을 전송하기 전에 파일이름을 클라이언트에게 알려줘야 한다. 
// 그래야 클라이언트는 전송받은 파일을 이름을 바꾸지 않고 정확히 쓸 것이다.
int nNameLen = strFileName.GetLength(); // 파일이름의 길이를 저장한다.

acceptSoc.Send(&nNameLen, 4); // 파일 이름의 길이를 전달한다.
acceptSoc.Send((LPCTSTR)strFileName, nNameLen); // 파일 이름을 전달한다.
// 데이터를 위한 임시 버퍼을 잡자.
byte * data = new byte[4096];
DWORD dwRead;
// 파일을 읽고 소켓으로 전송하자.
do
{
  dwRead = sourceFile.Read(data, 4096);
  acceptSoc.Send(data, dwRead);
}
while(dwRead > 0);
/* 위의 코드는 파일의 길이를 4k 바이트씩 잘라서 전송합니다. 소켓의 디폴트 버퍼크
기는 8k이지만, 파일의 전체를 한번에 읽어서 전송하면, 8k가 초과되는 용량에 대해서
는 데이터가 손상되지 않으리라 보장할 수 없습니다. CFile객체의 Read함수는 인수로 
지정된 바이트 수만큼 읽지 않을 수도 있습니다. 하지만 최대로 읽어들이는 양은 인수
로 전달된 바이트수를 초과하지는 않습니다. 따라서 읽어들인 양이 어느정도인지 
dwRead에 저장하고 그 크기를 소켓의 Send함수에 전달해 주어야 하는 것입니다. 소켓
의 Send함수 역시, 지정된 사이즈를 전부 한번에 전송하지 않을 수도 있습니다. 전송
당시의 TCP망이 속도가 저속이면 인수로 지정된 양보다 작은 양을 전송하게 되지만, 
그것은 TCP가 알아서 추가 전송을 할 것이므로 위 코드의 루프에서는 신경써주지 않아
도 됩니다. */

// 메모리를 해제하고 파일핸들을 닫는다
delete data;
sourceFile.Close();


/***************************************/
// 클라이언트에서 파일을 받는 부분
/***************************************/

// 포트를 정의하는데, 서버측과 같은 포트이어야 한다.
#define PORT 30000

// 소켓을 초기화 한다.
AfxInitSocket(NULL);

// 소켓을 만들자
CSocket connectSoc;
connectSoc.Create(); // 여기서는 인수를 전달하지 않는다

// 접속을 요청하자
connectSoc.Connect("210.120.150.111", PORT);
/* Connect함수의 첫번째 인자는 서버측 컴퓨터의 IP주소이다. 이것은 (.)을 찍는 방
식으로 써도 되고, 도메인네임으로 써도 된다.*/
if(connectSoc.GetLastError() == 0) // 접속에 실패하였다면..
{
  MessageBox("접속에 실패하였습니다");
  // 기타 접속실패 처리
  connectSoc.Close();
  return;
}

// 접속에 성공했다면 데이터를 받아 들이자
char *strFileName;
int nNameLen;

connectSoc.Receive(&nNameLen, 4);
connectSoc.Receive(strFileName, nNameLen);

CFile targetFile;
targetFile.Open(strFileName, CFile::modeCreate | CFile::modeWrite | 
CFile::typeBinary);

byte *data = new byte[4096];
DWORD dwRead;

do
{
  dwRead = connectSoc.Receive(data, 4096);
  targetFile.Write(data, dwRead);
}
while(dwRead > 0);

delete data;
targetFile.Close();

위의 코드는 가장 핵심적인 기능만 구현되어 있습니다. 실제로는 이 코드를 애플리케
이션에 붙이고 실행하면 CPU타임을 독차지 하게 되어 파일 전송이 완료되기 까지 사용
자에게 응답하지 않습니다. 용량이 큰 파일을 전송할 경우에는 전송시간이 걸릴 것이
므로, 사용자는 다운된 줄 알고 프로그램을 죽일 수도 있기 때문에 골칫거리가 아닐 
수 없습니다. 또, 전송받는 부분에서 상황에 따라 무한루프에 빠지는 경우도 있는데, 
전송받은 총량과 전송받아야 할 량을 비교해서 루프를 탈출하는 코드가 추가 되어야
만 완전해 집니다만... 이들에 대해서는 강좌에서 앞으로 이야기할 계획입니다.





-----------------------------------------------------------------------------------------

덧글

  • gkgk 2008/12/23 00:22 # 삭제 답글

    잘 읽었습니다 ^^ 지금 댓글 달고 있는 순간에도 Edit 박스에 입력하고 이걸 띄워준다고 생각하게 되니 재밌네요 ,;;ㅋㅋㅋ
  • lina 2017/11/14 13:40 # 삭제 답글

    설명되어있는거를 찾고있었는데 많은 도움이 되었어요!! 잘읽고갑니다~
댓글 입력 영역