현재강좌 : Winsock 프로그래밍 이전: 5.2 윈속의 동작 다음: 5.4 윈속 채팅 클라이언트


5.3 윈속 채팅 서버

▶ 여기서는 4.2절과 4.3절에서 소개한 UNIX용 채팅 프로그램을 PC에서 윈속을 사용하여 구현하는 것을 소개한다.

▶채팅 서버와 클라이언트의 연결관계 그리고 서버에서 사용하는 소켓의 종류(m_sAccept와 m_sClient[])를 그림 5-1에 나타냈다.

그림 5-1 윈속 채팅 서버와 윈속 채팅 클라이언트의 연결관계

(m_sAccept:초기소켓, m_sClient[]: 채팅 참가자의 소켓번호 배열)

▶ 채팅 서버는 클라이언트의 접속요청을 수락하는 일과 어떤 클라이언트가 보내온 메시지를 모든 클라이언트에게 방송하는 두 가지 일을 수행한다.

▶ 이를 위하여 서버는 두 가지 종류의 소켓을 필요로 하는데 첫번째는 접속요청을 기다리는 소켓(m_sAccept)이며, 두번째는 채팅에 참가하는 각 클라이언트들과 메시지를 주고받기 위한 소켓이다.

▶ 이를 위하여 소켓의 배열(m_sClient[])을 사용한다(이러한 동작 개념을 4.2절에서도 사용하였음).

▶ 서버는 먼저 초기소켓(m_sAccept)을 생성하고 여기에 accept()를 호출해 둔다.

▶ 이 소켓으로 어떤 클라이언트가 채팅에 참가하기를 신청하면 accept()가 처리되고, accept()가 리턴한 새로운 소켓번호를 배열 m_sClient[]에 차례로 저장하게 된다.

▶ 그림 5-2는 서버 프로그램의 실행 예이다. 서버 프로그램 WChat_Server.c의 전체 리스트는 5.3.2절에 수록되어 있으며 먼저 프로그램의 주요부분을 설명하겠다.

그림 5-2 윈속 서버 프로그램 실행 화면

5.3.1 프로그램 주요부분 설명

(1) 헤더 파일

▶ Server.h 파일은 그림 5-2와 같은 화면을 구성하는 정보를 가지고 있는 파일로 이 책에서는 Visual C로 작성하였다.

#include <windows.h> /* 윈도우 응용 프로그램의 마스터 헤더 파일 */

#include <Winsock.h> /* 윈속 관련 상수 및 함수 선언 */

#include "Server.h" /* 리소스 파일 */

(2) 전역 변수

▶ 채팅 서버 프로그램에서 사용하는 전역 변수와 각각의 기능은 다음과 같다.

SOCKET m_sAccept; /* 채팅 참가신청을 받기 위한 소켓 */

SOCKET m_sClient[]; /* 각 클라이언트와 메시지를 주고받기 위한 소켓 배열 */

char m_strMsg[]; /* 메시지를 저장할 버퍼 */

int m_Total; /* 채팅에 참가하고 있는 클라이언트 수 */

HWND hWnd; /* 윈도우 핸들 */

(3) WinMain() 함수

▶ 본 예제에서는 윈도우 대화상자(DialogBox)를 메인 윈도우로 사용하고 있다.

▶ 아래의 코드는 WinMain() 중에서 CreateDialog()로 대화상자를 생성하고, GetMessage()로 대화상자에 도착하는 메시지를 받아 이를 처리하는 루프를 보이고 있다.

if((hWnd = CreateDialog(hInstance, MAKEINTRESOURCE(IDD_SERVER),

NULL, HandleDialog)) == NULL) {

MessageBox(NULL, "초기화 에러", "확인", MB_OK);

return FALSE;

}

while(GetMessage(&msg, NULL, 0, 0)) {

if(!IsDialogMessage(hWnd, &msg)) {

TranslateMessage(&msg); /* 메시지 큐에서 메시지를 가지고 옴 */

DispatchMessage(&msg); /* 메시지를 분석한다 */

}

}

(4) 메시지 처리

▶ WChat_Server.c에서 처리해야 할 메시지는 아래와 같이 네 가지이며 각 메시지가 발생하는 조건은 다음과 같다.

▶ WM_INITDIALOG: 다이얼로그 박스가 생성될 때 발생하는데 WChat_Server.c에서는 이 메시지가 발생하면 전역 변수들과 윈속을 초기화한 후 클라이언트의 연결요청을 기다리기 위해 사용자 정의 함수인 InitSocket()을 호출한다.

▶ WM_COMMAND: 메뉴, 리스트박스, 버튼, 체크박스 등에서 발생하는 메시지로 WChat_Server.c에서는 사용자가 종료버튼을 클릭할 때만 발생한다.

▶ WM_DESTROY: 윈도우가 종료될 때 내부적으로 발생한다.

▶ WM_ASYNC: WChat_Server.c에서 정의한 메시지로 소켓을 통하여 데이터를 비동기 모드로 처리하는 데 사용된다.

▶ 메시지의 처리는 HandleDialog() 함수에서 이루어지는데 HandleDialog()가 함수 인자로 리턴하는 메시지 iMsg의 종류에 따라 해야 할 일을 다음과 같이 구분한다.

BOOL CALLBACK HandleDialog(HWND hWnd, UINT iMsg, WPARAM wParam,

LPARAM lParam) {

switch(iMsg) {

case WM_INITDIALOG:

/* 전역 변수 및 윈속 초기화 */

case WM_COMMAND:

/* "종료" 버튼 입력 처리 */

case WM_DESTROY:

/* 프로그램 종료 */

case WM_ASYNC:

/* 비동기 메시지 처리 */

}

}

(5) WM_INITDIALOG의 처리

▶ WChat_Server.c에서는 WM_INITDIALOG 메시지가 발생하면 사용자 정의 함수인 InitSocket()을 호출하도록 하였다.

▶ InitSocket() 함수에서는 WSAStartup() 시스템 콜을 호출하여 Winsock.dll을 초기화한다.

▶ 윈속에서도 소켓의 생성은 BSD 소켓에서와 같이 socket() 함수를 통해서 이루어진다

▶ 아래는 소켓을 만들고 소켓번호(m_sAccept)와 소켓주소 구조체 serv_addr를 bind()한 후 listen()을 호출하는 것을 보이고 있다.

#define CHAT_PORT 7001 /* 포트번호로 7001번 사용 */

m_sAccept = socket(AF_INET, SOCK_STREAM, 0);

serv_addr.sin_family = AF_INET;

serv_addr.sin_port = htons(CHAT_PORT);

serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

bind(m_sAccept, (LPSOCKADDR)&serv_addr, sizeof(serv_addr));

listen(m_sAccept, 5);

(6) WM_COMMAND의 처리

▶ WM_COMMAND 메시지는 사용자가 버튼, 리스트박스, 체크박스, 에디트박스, 메뉴 등을 사용할 때 발생하는 메시지이다.

▶ 본 프로그램에서는 "종료" 버튼이 눌릴 때 발생하는 메시지만 처리하면 된다.

▶ 아래의 WM_COMMAND 처리부분에서 wParam은 위에 열거한 것과 같은 여러 종류의 메시지를 구분하는 파라미터이며 IDC_EXIT는 종료 버튼의 ID이다.

▶ 한편 DestroyWindow() 함수는 윈도우 내부에 WM_ DESTROY 메시지를 보내준다.

case WM_COMMAND:

switch(wParam) {

case IDC_EXIT:

DestroyWindow(hWnd);

return TRUE;

}

(7) WM_DESTROY의 처리

▶ WM_DESTROY는 (위의 DestroyWindow()에 의해) 윈도우에서 내부적으로 발생되는 메시지로 윈도우를 종료시킬 때 사용된다.

▶ 다음의 코드에서 PostQuitMessage()는 윈도우 메시지 큐에 종료 메시지를 전송하는 함수이다.

case WM_DESTROY:

PostQuitMessage(0);

return TRUE;

(8) WM_ASYNC의 처리

▶윈속에서는 소켓이 블로킹 모드로 동작할 때의 문제점을 해결하기 위해서 WSAAsyncSelect() 함수를 제공한다.

▶ WChat_Server.c에서는 WSAAsyncSelect()가 WM_ASYNC라는 메시지를 발생시키도록 하였다.

▶ WChat_Server.c에서는 비동기 모드로 I/O를 처리하기 위하여 WSAAsyncSelect()를 아래와 같이 두 가지 소켓에 대하여 각각 호출해야 한다.

1) accept()를 비동기적으로 처리하기 위하여 초기소켓(m_sAccept)에 대하여 호출하는 것.

2) 각 클라이언트들과 채팅 메시지를 비동기적으로 송수신하기 위하여 소켓 배열 m_sClient[]에 대하여 호출하는 것.

▶ 1)번의 경우 WSAAsyncSelect()를 호출하는 것을 아래에 보였다.

▶ m_sAccept는 FD_ACCEPT 이벤트 발생을 감시할 초기소켓의 번호이고 m_hWnd는 FD_ACCEPT 이벤트가 발생했을 때 메시지를 받을 윈도우 핸들이며, WM_ASYNC는 전송할 메시지를 나타낸다.

WSAAsyncSelect(m_sAccept, hWnd, WM_ASYNC, FD_ACCEPT);

▶ 2)번의 경우는 새로운 채팅 클라이언트가 추가로 연결될 때 즉, accept()가 성공적으로 수행되었을 때 accept()가 리턴한 소켓번호 m_sClient[m_Total]에 대하여 수행된다(m_Total은 현재 채팅에 참가한 총 사람수).

▶그러나 2)의 경우는 1)의 경우와 달리 각 클라이언트와의 데이터의 송신, 수신, 종료 등 세 가지 이벤트를 처리하여야 하므로 FD_ACCEPT가 아니라 FD_READ|FD_WRITE| FD_CLOSE를 이벤트 리스트로 지정하여야 한다.

▶ 아래는 이러한 기능을 수행하는 프로그램 코드이며 여기서 msg에는 모든 채팅 가입자에게 보내는 메시지가 저장된다.

cli_len = sizeof(cli_addr);

m_sClient[m_Total] = accept(m_sAccept, (LPSOCKADDR)&cli_addr, &cli_len);

WSAAsyncSelect(m_sClient[m_Total], m_hWnd, WM_ASYNC,

FD_READ|FD_WRITE|FD_CLOSE);

wsprintf(msg, "%d번째 클라이언트 추가", m_Total);

m_Total++;

▶ WSAAsyncSelect()를 호출해 둔 이후에 WM_ASYNC 메시지가 발생하면 어느 소켓에서 이 메시지를 발생시켰는지를 알아내야 한다.

▶ 아래는 이러한 기능을 처리하는 프로그램 코드인데, WM_ ASYNC 메시지를 발생시킨 소켓이 어떤 소켓인지 확인하기 위하여 임시 소켓번호 sock_tmp를 사용하고 있다.

SOCKET sock_tmp;

case WM_ASYNC:

sock_tmp = LOBYTE(wParam); /* 이벤트를 발생시킨 소켓번호 추출 */

if(sock_tmp == m_sAccept) {

/* 새로운 클라이언트의 참가신청 처리 즉 accept() 수행 */

} else {

/* m_sClient[] 중에 해당 클라이언트의 채팅 메시지 수신 */

/* 채팅 메시지 방송 */

}

(9) 데이터 송수신

▶ WSAAsyncSelect()에서 지정한 사용자 메시지 WM_ ASYNC가 발생하였을 때 소켓에서 발생한 이벤트의 구체적인 내용이 lParam 파라미터에 들어 있게 된다.

▶ 따라서 lParam의 값을 확인하여 데이터의 송신 및 수신 등의 적절한 기능을 처리하면 된다.

▶ 다음에 데이터 읽기, 쓰기, 종료 이벤트를 각각 처리하는 것을 보이고 있다.

switch(lParam) {

case FD_READ: /* 읽을 데이터 발생 */

/* 클라이언트가 보낸 데이터를 읽는다 */

memset(buf, '\0', 512);

recv_len = recv(sock_tmp, buf, 512, 0);

/* 소켓(sock_tmp)에 FD_WRITE의 사용자 정의 메시지를 발생시킨다. */

PostMessage(WM_ASYNC, sock_tmp,

WSAMAKESELECTREPLY(FD_WRITE,0));

break;

case FD_WRITE: /* 모든 채팅 가입자에게 메시지를 전송한다 */

for(i=0; i<m_Total; i++) send(m_sClient[i], buf, sizeof(buf), 0);

break;

case FD_CLOSE: /* 해당 클라이언트를 종료시키고 */

/* 전체 클라이언트 수를 하나 줄인다 */

for(i = 0; i < m_Total; i++) {

if (sock_tmp == m_sClient[i]) { /* 종료를 원하는 소켓이 */

/* 클라이언트 소켓 배열에 있는지 확인 */

closesocket(m_sClient[i]);

/* 소켓 배열의 빈 곳을 채운다 */

if ( i != (m_Total - 1) ) {

m_sClient[i] = m_sClient[m_Total-1];

}

m_Total--;

break;

}

}

}

wchat_server 프로그램


현재강좌 : Winsock 프로그래밍 이전: 5.2 윈속의 동작 다음: 5.4 윈속 채팅 클라이언트