C++/소켓통신

MFC에서 소켓을 이용한 파일전송기 만들기(2장-MFC소켓클래스)

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




헉....오늘아침에 제 강좌의 조회수를 보고 전 놀라고 말았습니다.
이 분야에 관심들이 정말 대단하군요...
더 공부한 뒤에 시작할걸... 허접이는 클났군.... 이마에 땀이 나더군요....
음.... 부디 여러분을 실망시키지 말아야 할텐데... 하늘이여 내게 힘을 주소서..

다시 또 대강 2장이 정리되었습니다. 원래는 이번 장에서 더 많은 이야기를 할려고 
했지만.. 천시(퇴근시간)와 지리(사무실)와 인화(눈치보임)가 도와주질 않는군요.. 
타이핑하느라 손도 넘 아픕니다. 죄성..

-----------------------------
제2장  MFC 소켓지원 클래스
-----------------------------

이제부터는 본격적으로 소켓을 사용하는 방법에 대해 이야기 해야할 때가 된것같다. 
1장의 내용을 별 무리 없이 이해했다면, 어디가서 "소켓이란 무엇인가?-.-?" 에 관
한 주제로 이야기 할 수 있을 것이다. 여기에 이번장에서 다룰 내용을 다시 무리없
이 이해한다면(사실 어려운거 하나도 없다), 이번에는 어디가서 "난 소켓을 프로그래
밍할 줄 안다 -,.-" 하고 이야기 할 수 있을 것이다. 

1. MFC 소켓 클래스와 멤버들
먼저 MFC에서 소켓 API들을 캡슐화 해 놓은 클래스들에 대해서 알아보자. 이들 소
켓 클래스는 CAsyncSocket 과 CSocket 클래스 인데, CSocket은 CAsyncSocket으
로 부터 상속된 클래스로, 개발자가 좀더 편리하게 사용할 수 있도록 해 놓은 것이
다.

CAsyncSocket 객체는 비동기 소켓객체인데, 이말은 소켓에서 발생하는 모든 이벤트
를 알아내기위해 프로그래머가 전부 코딩으로 처리하는 것이 아니라 윈속이 제공하는 
인터페이스를 사용하는 소켓객체라는 말이다. 이것은 내부적인 이야기 이다. 우리는 
윈속과 윈속 API에 대해서 연구논문을 쓰고자 하는 것이 아니기 때문에, 이 말을 단
지 이렇게 이해하도록 하자.

"CAsyncSocket은 비동기 소켓을 표현하는 객체로서, 소켓의 이벤트는 객체에서 제
공하는 이벤트핸들러를 오버라이드해서 적절히 사용할 수 있다"

CSocket 객체는 비동기 소켓의 확장판으로, 동기소켓 객체이다. 비동기 소켓이 이벤
트 중심으로 프로그래머가 해당 이벤트에 대한 직접적인 제어를 수행하는 반면, 동기
소켓은 비동기 소켓의 이벤트 처리 모듈을 기본적으로 내장하고 있다고 보면 된다.

예를 들어서, 비동기 소켓으로 소켓을 생성한 다음 서버로 연결하기 위해 프로그래머
는 CAsyncSocket의 Connect() 함수를 호출한다. Connect() 함수는 비동기 소켓이
기 때문에 아직 연결되지 않았음을 나타내는 FALSE 값을 바로 리턴한다. 그리고 
OnConnect() 가상 함수가 호출될 때까지 대기 상태에 있게 되지만, 동기 소켓은 
ConnectHelper()라는 함수를 제공해 소켓 연결이 완료될 때까지 리턴하지 않는다. 
따라서 ConnectHelper() 함수의 리턴 값이 FALSE라는 의미는 소켓 연결 실패를 가
리킨다.
즉, 비동기 소켓을 쓸때에는, Connect()함수를 호출한 후 그 결과를 처리하기 위해
서 OnConnect 메시지 핸들러를 오버라이드 해 주어야 하는데, 동기 소켓을 사용한다
면, Connect동작(서버에 접속하기 위한 동작)이 완료된 후(객체가 내부적으로 처리
를 수행한 후)에 함수가 리턴하므로, 바로 그 함수의 리턴값을 조사하면, 함수의 실
행 결과를 얻게 된다는 것이다.
MSDN에 보면, Important 라고 강조된 글로 CSocket 객체에서는 OnConnect가 결
코 호출되지 않는다고 적어놓은 부분을 볼 수 있을 것이다.

어느 것이 더 마음에 드는가? 
어떤 사람은 비동기 소켓이 프로그램상 더 많은 융통성을 제공하기 때문에 이를 선호
한다. 물론....나도 마찬가지 이다. -.-;
눈치 겁나게 빠른 사람은 1장에서 예시한 코드를 상기하면서, 우리는 이 두 소켓객체
를 적절히 혼합하여 사용할 것이란 것을 벌써 눈치 챘을 지도 모른다. 또는 위에서 
언급한 내용 때문에 엄청 고민하고 있는 분도 계실지 모르겠다. 사실, 위의 내용은 
이해하지 못해도 코딩하는데 지장이 없다(휴... 다행이로군..) 어쩌면, 소켓객체들
의 세부적인 성격들에 관해서는 소켓 프로그래밍에 대해서 연습하고 계속 공부하는 동
안에 자연히 알게 될 지도 모른다. 또한 CSocket이 CAsyncSocket으로 부터 상속받
았다는 것을 상기해야 한다. 둘은 근본이 같다.

이제, 소켓객체의 멤버함수와 이들을 사용하게 되는 일반적인 플로우에 대해 알아보
자.

소켓을 사용하기 위해서는 맨 처음 소켓을 초기화 해야 한다. 이 과정은 단순히 함수
하나를 호출하면 끝난다.
AfxSocketInit(NULL);
MSDN에서는 이 함수를 애플리케이션 객체(CWinApp)의 InitInstance함수에서 호출
하라고 되어 있다. 어디서 호출하든, 소켓을 사용하기 전에 소켓이 초기화 되어야 한
다.

--------------- Create함수 --------------------------------
애플리케이션에서 소켓을 초기화 했으면, 이제 소켓을 사용할 수 있다. 소켓을 사용
하기 위해서는 소켓 객체의 인스턴스를 생성하고 이 인스턴스의 멤버인 Create함수
를 호출한다.
CAsyncSocket mySoc;
mySoc.Create(UINT nPort);
위의 예는 서버측에서 클라이언트의 접속요청을 기다릴 소켓을 생성하는 예이다.
Create함수는 윈도우즈 소켓을 생성하고, 이를 호출한 인스턴스에 생성된 소켓을 붙
여준다.
이 함수는 원래 4개의 인자를 받는데, 모두 다 디폴트 값이 지정되어 있다. 원형은 
다음과 같은 형태이다.

CAsyncSocket::Create(UINT nSocketPort, int nSocketType, long lEvent, 
LPCTSTR lpszSocketAddress)

첫번째 인자 nSocketPort는 소켓이 사용할 포트이다. 디폴트는 0인데, 이 값에 0
을 전달하면, 포트의 번호를 소켓이 자동으로 할당해서 사용하게 한다.
두번째 인자 nSocketType은 소켓의 형태를 지정하는 상수로, 디폴트는 
SOCK_STREAM인데, 이것은 생성되는 소켓을 스트림 소켓으로 생성하도록 한다. 만
일 이 값으로 SOCK_DGRAM을 전달하면, UDP 프로토콜을 사용하는 데이터그램 소켓
을 생성한다.
세번째 인자 lEvent는 비트마스크로, 소켓에서 처리하고자 하는 메시지를 소켓에게 
알려준다. 디폴트값은 소켓에서 발생하는 모든 메시지를 처리하는 것인데, 보통 이대
로 사용한다.
네번째 인자 nSocketAddress는 접속되는 소켓의 IP어드레스를 가진 문자열상수의 
포인터인데 디폴트는 NULL이며, 보통 이대로 사용한다. 즉, 임의의 클라이언트로 부
터의 접속요청을 받는다.

이 강좌에서 언급하는 멤버함수들에 대해 더 자세히 알고 싶다면, 역시나 MSDN을 참
조하는 것이 최선이다. 이 강좌의 목적상, 여기서 언급하는 정도로만 알아두어도 코
딩에는 문제 없을 것이다. Create함수는 함수를 호출하는 소켓의 용도에 따라 다른 
구문으로 쓰인다. 이 함수설명의 첫부분에서 단지 포트번호 하나만을 전달하면서 호
출한 것은 서버측의 접속대기용 소켓에서 사용하는 방법이다. 우리는 항상 서버측의 
접속대기용 소켓(리슨소켓이라 한다)에서는 위와 같이 Create를 호출할 것이다.
반면, 클라이언트 측의 접속을 요청할 소켓에서는 아무런 인수도 전달하지 않고 이 
함수를 호출한다. 또한, 서버측에서 접속을 억셉트하는데 사용되는 소켓은 아예 이 
함수를 호출하지 않는다.(이 호출규약을 기억하기 바란다. 실제 코딩에서 어떤 경우
에는 Create함수의 잘못된 호출이 문제를 유발하기도 한다)

------------------ Listen() 함수 -----------------------------
이 함수는 서버측에서만 사용한다. 위에서 이야기한 리슨소켓이 Create를 호출한 
후 호출해야할 함수가 바로 이것이다. 이함수에는 정수형(int) 인자를 하나 전달하
는데, 이 것은 접속요청을 받아들일 수 있는 최대 접속요청 수이다. 최대값은 5개 까
지의 접속요청을 받는데 디폴트값 역시 5로 되어 있다. 5개 까지의 접속요청을 받는
다고 하니까, 어떤분은 
"오오... 소켓객체는 따로 아무런 처리를 해 주지 않아도 5명 까지는 다중접속을 지
원해 주는군.. 좋은데??....."
이런 일장춘몽에 젖어 있을지도 모르겠다....(꿈을 깨서 미안하다....) 여기서 5개 
까지의 접속요청을 받는다는 것은 그 보다는 훨씬 처참한 이야기이다. 접속요청이 들
어오면 소켓은 요청을 일종의 큐에 집어넣고, 먼저들어온 소켓부터 접속요청을 억셉
트하는데, 하나와의 통신이 끝나면, 다시 큐에 있는 가장 먼저 들어왔던 요청을 받는
다. 나중에 들어온 접속요청은 앞서 접속된 통신이 끝날때까지 기다려야만 한다. 만
일 동기 소켓인 CSocket을 사용해서 접속요청을 한 경우를 생각해 보자, 이 접속요
청이 큐에서 5번째 위치에 있다면.... 접속을 요청한 함수는 앞의 4개의 접속요청이 
끝날때까지 리턴하지 않을 것이다(얼마나 슬픈 이야기 인가??) 즉... 5개까지의 접
속요청을 받는 다는 것은 큐에 5개 까지의 접속요청을 넣어 두고 처리할 수 있다는 
말이다. 결코 다중접속이 아니다. 만일 하나의 클라이언트와 접속하고 난후 간략한 
데이터를 순식간에 전송하고는, 접속을 닫는 경우라면(예를 들어 단순한 메신저 같
은 프로그램), 이 것의 동작은 꼭 다중접속인 것처럼 보일 수 있다. 그러나, 파일
을, 그것도 그 수량에 제한없이 여러개의 파일을 연속적으로 전송할려고 하는 우리에
게는 어림도 없는 일이다. 따라서 다중접속을 지원하기 위해서는 반드시 이를 처리
해 주는 코드가 필요하다.
이 함수는 다음과 같이 호출한다.
mySoc.Listen();
이와 같이 하면 서버측의 소켓인 mySoc은 접속 대기 상태가 되고, 클라이언트의 접속
을 받아들일 준비가 되었다.

-------------------- Connect 함수 ---------------------------
서버측에서 Listen()을 호출하고 소켓이 대기 상태가 되면, 클라이언트 측에서는 
Connect()함수를 호출하여 접속을 요청할 수 있다.
이 함수는 두개의 버전이 있고, 두개의 버전 모두 두개의 인자를 받는다. 우리는 그
중의 첫번째 버전만 살펴볼 것이다. 나머지는 (무슨말을 할려는지 알고 있을 것이
다...) MSDN을 참조하기 바란다(..역시....-.-;).
함수에게 전달하는 첫번째 인자는 서버측 애플리케이션이 구동되고 있는 컴퓨터의 IP
주소를 가진 문자열의 포인터이다. 이것은 도트노테이션(숫자들 사이를 . 을 찍어 구
분한 표기법)이나 머신네임(msdn의 예를 인용하자면 "ftp.microsoft.com"과 같은 
표기법)을 쓸 수있다. 이 변수가 문자열의 포인터라고 하니까 헷갈릴 분들도 있을 
거 같다. 정확한 데이터형은 LPCTSTR이고, 이것은 char *와 호환된다. 직접 "문자
열" 과 같이 " "기호로 둘러싼 문자열을 써도 된다.
두번째 인자는 UINT형의 포트번호이다. 이것은 서버측의 Create함수를 설명하면서 
이야기 하였다. 당연히 서버 측 소켓에 전달한 것과 같은 포트번호이어야 한다.

-------------------- Accept 함수 ------------------------------
클라이언트 측에서 접속을 요청하면, (사실, 이때 서버측 소켓으로는 OnAccept를 구
동시킬 메시지가 전달된다) 서버측에서는 접속을 받아들이기 위해서 Accept함수를 
호출한다. 이 함수에 전달해야 하는 인수는 새로이 생성한 클라이언트측 소켓과 형
이 동일한 소켓 객체의 참조이다. 구문은 다음과 같다.

CSocket acceptSoc;
mySoc.Accept(acceptSoc);

주의할 점은 접속을 억셉트하기 위해 생성한 소켓은 Create를 호출하지 않는다. 이 
함수를 호출하면 리슨 소켓은 접속큐로부터 제일 오래 기다린 접속요청을 가져와서 접
속을 억셉트한다.
만일 큐에서 대기중인 접속요청이 없으면, 함수는 0(에러)을 리턴하고 억셉트를 위
해 생성된 소켓은 무용지물이 된다. 하지만, 리슨 소켓은 여전히 대기상태이다. 억셉
트 함수는 접속 큐가 다 비워질때까지 동작을 수행하려한다.

휴...........
이쯤에서 이번장을 마쳐야 할 것같다. 원래는 멤버함수에 대한 전반적인 사항과 다중
접속을 위해서 멀티스레딩에 대해 언급할려고 했었다. 그런데 겨우 5개의 함수에 관
해 이야기 하고 또 한 장을 접어야만 한다.....
이번장에서 소켓클래스의 전부를 이야기 하지 못했다. 또 언급된 함수들도 이것이 전
부다 이야기한것이 아니다. 특히, 메시지 핸들러에 대해서는 손도 대지 못했다. 메시
지 핸들러와 미처언급하지 못한 함수들에 대해서는 코딩하면서 이야기 하도록 하자.
다음장에서는 아마도 스레드에 대해 이야기 하면서 한장을 할애해야 할 것 같다.

P.S 이 장에서는 데이터의 전송에 직접적으로 관여하는 Send함수와 Receive함수에 대
한 설명은 의도적으로 제외되었다. 코드를 설명하면서 이 들 함수를 이야기 할 것이
다. 결코 손이 아파서 여기서 그만 두는 것이 아니다.....음......

이 시점에서 1장에서 예시한 코드를 다시한번 살펴봐 주기 바란다. 분명 달리 보일 
것이다.
그럼 다음 장을 기약하면서...........

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

저의 강의 첫번째 부분을 보신분들중 완성된 소스를 요청하신 분들이 몇분 계십니다.
제가 지금가지고 있는 소스는 제가 만들긴 했지만, 저에게 배포권이 없습니다. 이유
는 다 아시겠지요..ㅜ.ㅜ
저는 이보다 강좌의 목적에 맞도록 코드를 단순화 시켜서 강좌와 함께 완성시킬 생각
입니다. 맘이 급하시더라도 기다려 주시구요.. 강좌를 끝까지 지켜봐 주세요..그러
면 원하시는 것을 얻을 수 있을 겁니다.