티스토리 뷰


이번에는 소켓 프로그래밍을 이용하여 서버를 만들어 보도록 하겠다.


이전과 동일하게 프로젝트를 하나 생성한다.

소켓을 사용하는 것은 일반적인 활동이 아니기에 기본 헤더파일에 포함되어 있지 않다.

소켓 프로그래밍을 이전에 사용하기 위한 작업 2가지가 필요하다.


1. 헤더 파일 선언

2. 라이브러리 추가


먼저 헤더 파일 선언을 위해 stdafx.h에 가서 WindSock2.h 를 추가하자.


그리고 stdafx.cpp 로 가서 아래의 코드를 추가하다.


#pragma comment (lib, "ws2_32.lib")    

//32비트 어플리케이션을 만들기에 32소켓이 2.2.버전이라는 것을 의미한다.


우리는 만들려는 프로그램은 서버의 로그가 남을 수 있도록 이전에 배운 List Box를 사용할 것이다.



이 List Box의 ID를 IDC_EVENT_LIST로 해주고, 변수 추가를 통해 m_event_list를 추가해 준다.

그리고 이 리스트 박스에 로그들이 추가 되는 문장들이 시간 순서대로 즉 위에서부터 차곡차곡 쌓이기 위해 함수 하나를 만들어서 사용할 것이다. 



이 함수는 Dlg.h 파일에 추가 해주면 된다. 위의 그림에서 보는 것과 같이 AddEventString 이란 함수를 넣어주면, 초록색으로 물결 줄이 쳐지게 된다. 마우스 커서를 함수위에 올려주면 아래와 같은 그림이 나오게 된다.



여기서 정의 만들기를 선택해주면 된다.



위와 같은 화면이 나와 코드를 넣어 주면 된다.


.cpp 파일로 이동하여 InitInstance 함수의 필요없는 코드들을 지워준다.


BOOL CTestSVApp::InitInstance(){

CWinApp::InitInstance();


CTestSVDlg dlg;

m_pMainWnd = &dlg;

dlg.DoModal();


return FALSE;

}


지금부터 소켓 함수를 쓰겠다는 표시를 해줘야한다. 소켓 함수를 사용하는 곳은 .cpp 파일의 InitInstance 함수에 넣어 주면 된다.  

WSA(Windows Socket Api 의 약자)Startup 함수를 사용하여 통과를 해야 소켓이 동작을 한다.

끝날 때는 WSACleanup 을 적어준다. 이 함수는 더 이상 소켓을 쓰지 않겠다는 뜻이다.

윈도우즈는 핸들의 개념을 갖고 있는데, 최종적으로 사용하지 않을 때 종료한다는 것이다. 즉 종료한다고 말은 해줘도 진짜 사용하지 않을 때까지 종료되지 않는다.


BOOL CTestSVApp::InitInstance(){

CWinApp::InitInstance();


WSADATA temp;

WSAStartup(0x0202, &temp);


CTestSVDlg dlg;

m_pMainWnd = &dlg;

dlg.DoModal();


WSACleanup();


return FALSE;

}


WSAStartup(0x0202, &temp); 에서 첫번째 인자에 해당하는 0x0202은 현재 거의 고정이 되어 있다. 모든 운영체제의 소켓버전은 2.2이상이기 때문이다. 


소켓을 휴대폰에 비유하여 생각을 해보자. 먼저 핸드폰을 개통하기 위해서는 대리점에 가서 폰을 사는 것처럼 우선 통신할 수 있는 수단을 구해야 한다.


소켓을 사용하기 위해서 SOCKET 이라는 함수를 사용한다. 반환값은 부호없는 32비트 정수이다. 이 함수는 인자를 3개를 받는다. 첫번째는 어떤 네트워크 그룹에 따라 주소체계를 사용할 것인가 두번째는 어떤 방식(TCP or UDP)으로 사용할 것인가 세번째는 어떤 네트워크 프로토콜을 사용할 것인가에 대해서 이다. 


socket(AF_INET, SOCK_STREAM, 0 ) 


핸드폰에 비유하면 어떤 통신사를 선택할 것인지 결정하는 것처럼 AF_INET 주소체계를 쓰겠다고 명시해준다. 다음 인자는 우리가 TCP 방식으로 사용할 것인지 UDP 방식으로 사용할 것인지를 명시해주는 것이다. TCP면 SOCK_STRAM을 사용해주고, UDP면 SOCK_DIAFRAM을 사용해주면 된다. 

그 다음 인자에는 어떤 네트워크 프로토콜을 사용할 것인지를 알려주는 것인데, 보통 앞의 인자를 통해서 사용할 네트워크 프로토콜을 알 수 있기 떄문에 0이라 넣어주면 된다.


먼저 Dlg.h 파일에 변수 하나를 선언해 준다. 



그리고 Dlg.cpp 파일의 OnInitDialog()로 가서 아래의 코드를 추가해 주자. 이 코드는 휴대폰을 살 때 신청서라고 생각하면 된다.



mh_listen_socket = socket(AF_INET,SOCK_STREAM ,0 );


위의 코드는 위에서 설명했으니, 생략하겠다.



if (SOCKET_ERROR != mh_listen_socket) {

sockaddr_in srv_addr;

srv_addr.sin_family = AF_INET;

//sin_famliy는 어떤 주소체계를 사용할지 적어주는 역할을 한다.

srv_addr.sin_addr.s_addr = inet_addr(ADDR_ANY);

//sin_addr.s_addr은 내가 서비스할 주소를 명시하는 것이다.

srv_addr.sin_port = htons(18000);

//sin_port는 고유 포트 번호를 적어주면 된다.

}


AddEventString(L"서비스를 시작합니다......");


여기서 포트에 대해서 알아보자.

시스템 내부에서 네트워크를 지원하는 프로그램이 많이 있다. 그래서 내 컴퓨터에서 어떤 프로그램과 통신할 것인지 결정해주는 역할을 하는 것이 바로 포트이다. 이때 포트 번호는 0 ~ 1023번 까지는 잘 알려진 포트로 이러한 포트들을 제외한 포트를 명시해주면 된다.


htons 함수를 사용했다. OS 마다 데이터 Ordering이 다른데, (윈도우는 리틀 엔디안, 리눅스 계열은 빅 엔디안을 사용한다.) 이때 단순히 포트 값을 정수로 표현하면, OS가 다른 경우에는 인식하면 방식이 달라 동작하지 않을 수 있다. 접속 단계에서 포트를 사용하기 때문에 어떤 OS인지 알 수가 없다. 그래서 접속 전에 host가 이 방식으로 정렬해서 정수 값을 보내기로 약속한다. 그리고 받아서 사용시에 자신의 로컬 타입으로 바꾸어 사용한다. 그래서 서로의 오더링이 달라도 받아서 사용 가능하게 된다.


이제 핸드폰을 개통하도록 하자.

이때 사용되는 함수는 bind() 이다. 


bind(mh_listen_socket, (SOCKADDDR *)&srv_addr, sizeof(srv_addr);


이 전에 사용되는 컴퓨터 용어중에 연결하다라는 의미를 가지는 단어들이 있다.


link, bind, embeded 이 세가지가 주로 사용되는 용어인데, 이 세 가지는 각각이 가지는 의미들이 다른다.


link 는 물리적으로 연결한다는 의미이다. 예를 들면 차에 타이어를 끼운다. 즉 이 물리적 연결을 떨어지면 의미가 없어지게 되는 것이다.


bind 는 인프라가 있고, 그 인프라에 해당하는 디바이스가 일시적으로 연결되는 행위에다. 즉 지하철 승강장에 지하철이 들어오는 것을 떠올리면 된다.


embeded 는 특정 시스템 내에서 동작하는 또 다른 시스템이라고 생각하면 된다. 즉 카오디오, 자동차와 카오디오는 별개인 것과 같이 이 오디오 시스템이 자동차에게 임베디드 되어 있다라고 말할 수 있다.


현재는 발신만 가능하다. 전화를 받을 수 있도록 listen을 추가하도록 하자. 클라이언트는 받으면 끝나기 때문에 listen 을 사용하지 않는다. 서버는 받아서 뭔가를 해야하기 때문에 listen을 통해 받는다. 마지막 인자는 동시에 받을 수 있는 콜 횟수를 의미한다. 1이상의 수여도 한번에 n명이 처리되는 것이 아니라 홀딩한 채로 1명씩 처리하는 것이기 떄문에 처리속도는 비슷하다. 


listen(mh_listen_socket, 1);


이제 클라이언트가 들어왔을 떄 접속을 받아줄 수 있도록 설정해야 한다. accept 함수를 사용할 경우 클라이언트가 들어왔을 때 접속을 받아줄 수 있지만 계속 접속을 기다리는 상태이기 대문에 프로그램이 응답없음 상태가 된다. 따라서 이 문제를 해결하는데 2가지 방식이 존재한다. 우리가 사용할 방법은 비동기 방식이다.

이 방식은 정확한 클라이언트의 접속 시간을 모를때, 윈도우 소켓 매니저라는 것이 있는데, 이 매니저에게 클라이언트가 들어오는지 감시해달라고 요청하는 것이다. 누군가 접속을 시도하면, 이 대화상자한테, 25001 메시지를 발생해달라고 설정하는 것이다.


WSAAsyncSelect(mh_listen_socket, m_hWnd, 25001, FD_ACCEPT);


이제 25001 메시지를 처리하도록 하자.

클래스 마법사에서 메시지탭에서 사용자 지정 메시지 추가를 눌러 주면된다.

그리고 아래의 그림과 같이 설정해주자.





만들어진 함수에 위의 코드를 넣어 주면 된다.


먼저 wParam에는 우리가 발생시킨 소켓의 핸들이 넘어오게 된다. listen 소켓을 하나만 사용했지만, 보통의 경우에는 많이 사용하기 때문에 메시지가 발생시 어떤 소켓에 의해 발생했는지 그 소켓에 대한 정보도 같이 넘겨준다.

accept 함수를 써 소켓을 만들어 준다. 새로운 소켓과 접속한 소켓이 연결될 수 있게 해준다. 


이제 클라이언트를 어떻게 관리할 것인가 에 대한 구조를 만들어보자.

클라이언트가 접속시 서버는 두가지를 기억해야한다. 

관리를 위해 IP와 소켓 핸들이 들어가 있는 구조체를 선언해주고, 서비스할 상수값을 정의해준다.



우리는 이제 100명이 사용할 수 있는 방을 만들어 준 것이다. 




빈 방을 찾고 찾은 방에 저장해야 하므로 새로운 소켓을 없애도 저장할 수 있는 코드를 수정한다.


25001메시지 경우 누군가 접속시 발생하고, 빈 방이라는 것이 확인되면 25002 메시지가 발생하게 된다. 25002 메시지는 클라이언트가 데이터를 전송 중이거나, 연결이 끊어졌을때, 현재 이 대화상자 윈도우로 발행될 때 사용된다. 이제 25001메시지 생성 방법과 동일하게 25002메시지도 생성해 보자. 



위에서 설명했듯이, wParam은 소켓의 핸들 값을 의미한다. 이번에는 lParam에 대해서 알아볼 것이다. 이 인자는 총 32비트로 이루어져 있고, 앞 16비트는 어떤 메시지 때문에 발생한 것인지에 대한 정보, 뒤 16비트는 에러 유무에 대한 정보가 담겨져 있다. 


SOCKET h_socket = (SOCKET)wParam;

if (WSAGETSELECTEVENT(lParam) == FD_CLOSE) {

closesocket(h_socket);

for (int i = 0; i < m_user_count; i++) {

if (m_user_list[i].h_socket == h_socket) {

m_user_count--; //맨 마지막 사용자면 카운터만 줄이기

if (i < m_user_count) {

memcpy(m_user_list + i, m_user_list + m_user_count, sizeof(UserData));

}

break;

}

}

}

else {//FD_READ

}


먼저 WSAGETSELECTEVENT는 IParam 인자가 FD_CLOSE를 의미하는지 아니면 FD_READ를 의미하는지 판별하는 함수이다. 그래서 만약 FD_CLOSE라면 소켓을 닫아주고, 반복문을 실행한다.


위에서 우리는 서버에 접속하는 클라이언트들을 배열에 저장하도록 했다. 새로운 클라이언트들이 접속하게 되면 배열 맨 아래에 클라이언트가 들어가게 된다. 이 때 배열 중간에 위치해 있는 클라이언트가 접속을 종료하게 되면 공간이 하나 나게된다. 지금은 배열의 크기가 적어서 새로운 클라이언트가 접속시 중간에 빈 공간을 찾아 그 곳에 넣을 수 있지만, 만약 배열의 크기가 십만 단위로 넘어가게 된다면 빈 공간을 찾는데 소비되는 자원들이 엄청나게 될것이다.


이를 방지하기 위해, 만약 중간에 위치한 클라이언트가 종료시, 현재 배열의 맨 아래에 위치한 클라이언트를 접속을 종료한 클라이언트 자리에 위치시켜주는 방식을 사용하면 된다. 







댓글