CoInzBlog

[c/c++자료] 윈도우 소켓(Winsock) 프로그래밍 | C/C++ 관련자료 2006.04.19 13:05
퍼스나콘 진용훈(imsangchin) http://cafe.naver.com/javacircle/14135

// www.codein.co.kr

출처: http://jkkang.net/unix/netprg/chap5/net5_intro.html

 

제5장 윈도우 소켓(Winsock) 프로그래밍

2장부터 4장까지는 UNIX 제공하는 BSD 소켓을 이용하는 네트웍 프로그래밍에 대하여 설명하였다. 소켓 인터페이스는 이제 UNIX 운영체제의 호스트뿐 아니라 PC에서도 제공되고 있는데 특히 1994년부터 WWW(World Wide Web) 서비스가 확산되면서 PC에서도 TCP/IP 이용한 네트웍 프로그래밍이 중요시되고 있다.

5장에서는 윈도우 소켓(Winsock) 프로그래밍에 대하여 설명한다. 먼저 5.1절에서 윈속의 특징을 설명하고 5.2절에서는 윈속의 동작과 비동기 함수의 처리에 대하여 다루며 5.3절과 5.4절에서는 윈속 채팅 서버 클라이언트 프로그램의 구축과정을 각각 설명하겠다.

5.1 윈속의 이해

5.2 윈속의 동작

5.3 윈속의 채팅 서버

5.4 윈속 채팅 클라이언트

5.1 윈속의 이해

▶ 윈속은 마이크로소프트(Microsoft) 윈도우3.1 또는 윈도우95에서 제공하는 TCP/IP 프로그래밍을 위한 API(Appli- cation Program Interface)로서 사용방법 및 기본 동작이 UNIX의 BSD 소켓과 거의 같으며 BSD 소켓과 마찬가지로 클라이언트-서버 모델을 기초로 하여 이용된다.

5.1.1 UNIX BSD 소켓과 윈속의 차이

▶ 윈속의 사용방법과 문법이 BSD 소켓의 경우와 유사하기는 하지만 윈속과 BSD 소켓용으로 작성된 프로그램은 서로 호환성이 없으며 이 두 가지 소켓 응용 프로그램이 호환성을 갖으려면 많은 부분을 수정하여야 한다.

▶ 먼저 윈도우와 UNIX 운영체제의 차이점을 비교하면 다음과 같다.

▶ 첫째, UNIX와 달리 윈도우3.1 이하에서는 멀티태스킹을 지원하지 않는다.

▶ 윈도우95는 멀티태스킹과 유사한 멀티쓰레드(multi- thread)를 지원하기는 하지만 윈속이 처음 소개된 것은 멀티태스킹이 지원되지 않는 윈도우3.1에서였으며 윈도우95와 윈도우3.1에서의 프로그램 호환성을 유지하기 위하여 윈속은 윈도우95에서도 계속 '멀티태스킹이 지원되지 않는' 환경을 가정하고 있다.

▶ 이와같이 UNIX와 윈도우 운영체제의 기본적인 차이로 인하여 윈속은 BSD 소켓과 내부적으로 매우 다른 특성을 갖고 있으며 프로그램 작성시에도 이를 고려하여야 한다.

▶ 둘째로 윈속은 주로 윈도우 프로그래밍으로 구현되는데, 윈도우 프로그래밍이 '메시지 구동형 프로그래밍'이라는 것이 UNIX 프로그램과 크게 다른 점이다.

▶ 메시지는 '어떤 사건(event)의 발생을 알리는 신호'라고 할 수 있는데, 메시지는 사용자가 만든 응용 윈도우들이 서로 주고받거나, 윈도우 운영체제가 사용자 윈도우에게 전달하는 것이다.

▶ 한편 윈속 프로그래밍에서는 윈속의 기능을 제공하는 라이브러리로 DLL(Dynamic Link Library)인 Winsock.dll을 사용한다.

▶ 이러한 DLL 라이브러리는 컴파일시 링크되는 것이 아니라 응용 프로그램 수행시 링크된다.

▶ DLL을 사용하는 것의 장점은 여러 응용 프로그램이 하나의 DLL을 공유하여 사용할 수 있다는 것이다.

▶ 예를들면 하나의 Winsock.dll이 수행중이면 여러 윈속 응용 프로그램들이 이것을 동시에 사용할 수 있게 된다.

▶이외에도 BSD 소켓과 윈속은 소켓을 열고 닫는 데 쓰이는 함수가 다른데 윈속의 경우 WSAStartup(), WSACleanup()함수를 통해 소켓과 Winsock.dll과의 관계를 설정, 해제해 주는 과정이 필요하다.

▶ BSD 소켓처럼 close()를 이용해 소켓을 닫는 것이 아니라 closesocket() 함수를 사용해 닫아야 한다.

▶ 이와같은 차이는 UNIX와 윈도우 운영체제의 특성의 차이로 인한 것이며 함수의 이름만 다른 것이 아니라 함수의 구체적인 내부 동작도 서로 다르다.

5.1.2 윈속의 동작모드

▶ BSD 소켓과 마찬가지로 윈속이 제공하는 소켓도 다음과 같은 세 가지 동작모드 (operating mode)를 가지고 있다.

  • Blocking 모드
  • Non-blocking 모드
  • Asynchronous(비동기) 모드

(1) Blocking 모드

▶ socket() 시스템 콜을 호출하여 하나의 소켓을 만들면 이것은 디폴트로 blocking 모드로 동작하는 소켓이 된다.

▶ 이러한 blocking 모드의 소켓을 대상으로 accept(), close- socket(), connect(), recv(), recvfrom(), send(), sendto()와 같은 함수를 호출하면 함수가 원하는 동작을 완료할 때까지 함수를 호출한 프로세스가 blocking될 수 있다.

▶ 즉, blocking 모드의 소켓에서는 응용 프로그램에서 위와같은 함수를 호출하였을 때 그 동작이 완료되어 함수가 리턴되어야 다음 작업을 할 수 있게 된다.

▶ 한편 멀티태스킹이 지원되는 UNIX 컴퓨터에서는 block- ing 모드의 소켓을 사용하여 어떤 응용 프로그램이 블록되어도 컴퓨터 전체 동작에는 큰 문제가 되지 않는다.

▶ 왜냐하면 멀티태스킹 운영체제에서는 각각의 프로세스들이 독립적으로 수행되므로 한 프로세스가 블록되어 있어도 다른 프로세스들은 계속 수행될 수 있기 때문이다.

▶ 그러나 윈도우3.1과 같은 단일 태스킹 운영체제에서는 프로그램이 한 곳에 블록되어 있으면 PC 전체가 블록될 수 있다.

▶ 또한 blocking 모드의 소켓을 사용하면 하나의 프로그램에서 여러 개의 소켓을 동시에 개설하여 각각의 입출력을 처리하는 형태의 응용 프로그램을 작성하기가 어렵다.

▶ 따라서 윈속 프로그래밍에서는 이를 해결하기 위하여 소켓을 non-blocking 모드 또는 비동기 모드로 변경하여 사용하는 것이 필요하다.

(2) Non-blocking 모드

▶ Non-blocking 모드 소켓이란 accept(), closesocket(), connect(), recv(), recvfrom(), send(), sendto()와 같은 함수가 호출되었을 때 함수의 원하는 동작이 완료되는 것과 무관하게 일단 함수가 즉시 리턴되는 소켓을 말한다.

▶ 소켓이 처음 만들어지면 디폴트로 blocking 모드가 되는데 프로그래머는 필요에 따라 blocking 모드의 소켓을 non-blocking 모드로 바꿀 수 있다.

▶ Non-blocking 모드의 소켓을 사용하는 이유는 응용 프로그램이 이곳에서 멈추어 있지(block) 않게 하기 위해서이다.

▶ Non-blcoking 모드의 소켓에서 함수 호출이 즉시 리턴되었을 때 그 결과는 다음과 같이 두 가지로 종류로 나눌 수 있다.

1) 성공적인 리턴: 함수의 동작이 즉시 성공적으로 수행되었음.

2) 에러 리턴: 함수 수행중 에러가 발생했거나 함수가 블록되었음.

▶ 즉, non-blocking 모드 소켓에서 즉시 리턴된 함수 호출 결과는 성공적인 경우도 있고 실패한 경우도 있을 수 있다.

▶ 에러 리턴인 경우 에러코드가 WSAEWOULDBLOCK이면, 이것의 의미는 함수의 동작이 잘못되었다는 것이 아니라 '이 함수가 동작 완료될 때까지 기다린다면 block될 수 있다'는 것을 의미한다.

▶ 이와같이 에러코드가 WSAEWOULDBLOCK인 경우 그 원인은 다음의 두 가지 중 하나가 된다.

1) Winsock.dll이 함수가 원하는 동작을 시작했으나 아직 종료되지 않았음.

2) 함수의 동작이 시작되지 못했으며 다시 재시도를 필요로 함.

▶ 위의 첫번째 경우는 응용 프로그램이 함수의 동작 완료 시점을 알아서 그 결과를 처리하여야 하며 두번째 경우는 응용 프로그램이 이 함수가 성공적으로 시작될 때까지 함수를 계속 반복하여 호출해야 한다.

▶ 어떤 경우이든 non-blocking 모드 소켓의 처리는 다소 복잡하며 따라서 다음에 설명할 비동기 모드를 사용하는 것이 편리하다.

(3) Asynchronous(비동기) 모드

▶ 비동기 모드에서도 non-blocking 모드에서처럼 소켓 관련 함수의 호출이 바로 리턴된다.

▶ 그러나 비동기 모드에서는 non-blocking 모드와 달리, 함수의 동작이 완료되는 시점, 또는 함수의 실행이 시작되지 못한 경우 다음에 다시 재시도하여야 하는 시점을 시스템(Winsock.dll)이 메시지 처리 방식으로 나중에 응용 프로그램에게 알려준다.

▶ 즉, 비동기 모드의 소켓에서 소켓관련 함수의 실행결과의 에러코드가 WSAEWOULDBLOCK일 때의 의미는 다음과 같다.

1) 함수의 동작이 완료되면 그 때 Winsock.dll이 메시지를 통하여 동작의 완료를 알려주거나,

2) 함수의 동작이 시작하지 못했으며, 함수를 다시 호출해야 할 시점을 나중에 비동기적으로 알려주겠음.

▶ 앞에 언급한 바와 같이 UNIX와 같은 운영체제에서는 함수가 블록되는 것이 문제가 되지 않으므로 반드시 비동기 모드의 소켓을 사용할 필요는 없다.

▶ 그러나 윈속에서는 비동기 모드로 소켓을 사용하는 것이 편리하다.

(4) Blocking 함수

▶ 한편 blocking 모드의 소켓이 아니더라도 (즉, non- blocking 또는 비동기 모드의 소켓에서도) 어떤 함수들은 함수 자체의 특성상 블록될 수 있는 함수가 있다.

▶ 이러한 함수들은 네트웍 시스템(즉, TCP/IP)이 어떤 정보를 얻어내야만 그 결과를 리턴할 수 있으며 그 정보를 얻는데 다소 시간이 필요하기 때문이다.

▶예를들어 호스트의 도메인 네임을 입력하고 이 호스트의 IP 주소 등의 정보를 얻어내는 함수 gethostbyname()이 처리되기 위하여는, 네트웍 시스템이 DNS 서버에게 필요한 정보를 문의하고 그 결과가 도착할 때까지 기다려야만 한다.

▶ 이러한 함수들을 'blocking 함수'라고 부르며 표 5-1에 대표적인 blocking 함수들을 정리하였다.

함 수

기 능

select() 소켓의 상태 변화(읽기, 쓰기, 오류 발생)를 알려줌
gethostbyaddr() 호스트 주소로부터 호스트 정보를 얻음
gethostbyname() 호스트 이름으로부터 호스트 정보를 얻음
getprotobyname() 프로토콜 이름으로부터 프로토콜 번호를 얻음
getprotobynumber() 프로토콜 번호로부터 프로토콜 이름을 얻음
getservbyname() 서비스 이름으로부터 서비스 정보를 얻음
getservbyport() 포트번호로부터 서비스 정보를 얻음

표 5-1 대표적인 blocking 함수

▶ 표 5-1에 소개한 blocking 함수들은 소켓의 동작 모드에 관계없이 항상 블록될 수 있는 함수이다

▶ 이러한 함수를 사용할 때 프로그램이 블록되는 문제를 해결하기 위하여, 윈속에서는 이들 blocking 함수와 같은 기능을 수행하면서 실제로는 비동기 모드로 동작하는 즉, 함수 실행결과를 비동기적으로 알려주는 비동기 함수들을 제공하고 있다.

▶ 이들을 표 5-2에 정리하였으며 자세한 내용은 5.2.2절에서 설명하겠다.

비동기 함수

기 능

WSAAsyncSelect() 소켓의 I/O 상태 변화 즉, 연결요청, 데이터 수신, 송신 버퍼의 사용가능 등의 이벤트를 시스템이 메시지를 통하여 알려주도록 요청함
WSAAsyncGetHostByAddr() 호스트 주소로부터 호스트 정보를 얻음
WSAAsyncGetHostByName() 호스트 이름으로부터 호스트 정보를 얻음
WSAAsyncGetProtoByName() 프로토콜 이름으로부터 프로토콜 번호를 얻음
WSAAsyncGetProtoByNumber() 프로토콜 번호로부터 프로토콜 이름을 얻음
WSAAsyncGetServByName() 서비스 이름으로부터 서비스 정보를 얻음
WSAAsyncGetServByPort() 포트번호로부터 서비스 정보를 얻음

표 5-2 Blocking 함수(표 5-1)의 기능을 수행하는 윈속의 비동기 함수

5.1.3 윈속의 시작과 종료

(1) WSAStartup()

▶ WSAStartup()은 윈속 관련 함수들을 사용하기 전에 응용 프로그램이 반드시 호출해야 하는 함수로, 윈속 라이브러리인 Winsock.dll을 초기화한다.

▶ WSAStartup()은 일반적으로 윈도우 메시지 WM_CREATE나 WM_INITDIALOG의 처리 부분에서 호출된다.

▶ WSAStartup()은 두 개의 인자를 필요로 하는데, 응용 프로그램이 요구하는 최소의 윈속 버전(1.0 또는 1.1) 값과, WSAStartup()이 수행된 후 네트웍 시스템(Winsock.dll)이 윈속 관련 정보를 알려주는데 사용할 윈속정보 구조체, 즉 WSADATA 타입의 구조체 변수의 주소를 필요로 한다.

▶ 다음은 응용 프로그램이 사용할 윈속의 버전값을 지정하고 WSAStartup()을 호출하는 예이다.

WORD wVersionRequired ; /* 사용할 윈속 버전 값 */

WSADATA wsaData ; /* 윈속정보를 담을 구조체 */

wVersionRequired = MAKEWORD(0, 1); /* 윈속 버전 값을 1.0으로 세팅 */

WSAStartup(wVersionRequired, &wsaData); /* 윈속의 시작 */

▶ 위에서 wsaData는 WSADATA 타입의 구조체 변수로 여기에는 WSAStartup() 함수가 개설하여 앞으로 사용할 윈속에 관한 정보가 수록되어 리턴된다.

▶ WSADATA 구조체의 정의는 다음과 같다(Winsock.h 파일 참조).

typedef struct WSADATA {

WORD wVersion; /* 윈속 버전 (예 1.0) */

WORD wHighVersion; /* 최상위의 윈속 버전 (예 1.1) */

char szDescription[WSADESCRIPTION_LEN+1]; /* 회사정보 */

char szSystemStatus[WSASYS_STATUS_LEN+1]; /* 구성정보 */

unsigned short iMaxSockets; /* 한 프로세스가 열 수 있는 소켓 수 */

unsigned short iMaxUdpDg; /* UDP 패킷 크기 */

char FAR *lpVendorInfo; /* 회사별 데이터 구조의 포인터 */

} WSADATA;

 

(2) WSACleanup()

▶ 윈속의 사용을 종료할 때 호출하며 WSAStartup()에 대응되는 함수이다.

5.2 윈속의 동작

▶ 윈속 관련 함수들의 사용법 즉, 함수호출 순서와 함수의 인자 및 결과 리턴값 등은 2장에서 소개한 BSD 소켓의 사용법과 거의 같다.

▶ 윈속 프로그램을 작성하기 위하여는 윈도우 프로그래밍과 c 언어에 대한 지식을 필요로 하나 이 책에서는 이에 대해 자세한 설명을 하지 않고 예제 프로그램을 이해하는데 필요한 기본적인 내용만 설명하겠다.

▶ 소켓의 기본 개념에 관하여는 2장을 먼저 참고하기 바란다.

5.2.1 기본적인 윈속 함수

(1) socket()

▶ 윈속에서도 BSD 소켓 프로그램에서와 같이 socket() 함수를 호출하여 통신의 창구 역할을 하는 소켓을 생성한다.

▶ 아래는 socket()의 사용 예로 인터넷 도메인의 주소 체계에서 스트림형 소켓을 개설하는 것을 보였다.

SOCKET s; /* SOCKET은 int 타입이다 */

char lpszMsg[100];

s = socket(AF_INET, SOCK_STREAM, 0);

if (s == INVALID_SOCKET) { /* 소켓 개설 에러 발생 */

wsprintf(lpszMsg, "socket() 에러번호 : %d", WSAGetLastError());

MessageBox(hwnd, lpszMsg, "에러", MB_OK);

}

▶ socket() 함수의 첫번째 인자는 소켓이 사용할 주소 체계(Address Family)의 종류를 지정한다.

▶ 윈속 버전 1.1에서는 인터넷 주소체계(AF_INET)만을 지원한다.

▶ 두번째 인자는 트랜스포트 프로토콜을 지정하는데 연결형인 스트림 소켓을 개설하려면 SOCK_STREAM을, 비연결형인 데이터그램 형태의 소켓을 개설하려면 SOCK_ DGRAM을 선택한다.

▶ 세번째 인자는 프로토콜 타입(Type)인데 일반적으로 0을 쓴다.

▶socket()의 수행이 성공한 경우에는 새로운 소켓번호를 리턴하고, 실패한 경우에는 INVALID_SOCKET이 리턴된다.

▶ 위에서 WSAGetLastError()는 가장 최근에 발생한 윈속 관련 함수의 에러코드를 리턴하는 함수이다

▶ MessageBox()는 메시지 상자를 만들어 화면에 출력하는 윈도우 함수이다.

▶ MessageBox()의 원형은 다음과 같으며 위에서는 에러 메시지와 OK 버튼을 출력하고 있다.

int MessageBox (

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

LPCTSTR lpText, /* 텍스트 배열 주소 */

LPCTSTR lpCaption, /* 캡션 배열 주소 */

UINT uType); /* 박스 스타일 */

▶ 위에서 HWND는 윈도우 핸들 타입을 나타내는데 실제로는 int 타입이다.

▶ LPCTSTR은 long pointer const char string 타입을 나타내고, UINT는 unsigned int 타입을 나타낸다.

(2) bind()

▶ bind()는 서버에서 호출되는데 socket()의 수행으로 생성된 소켓의 번호(s)와 서버의 소켓주소 구조체(SOCKADDR_IN)를 연결하기 위해 호출한다.

▶ bind()의 첫번째 인자로는 소켓번호를, 두번째 인자는 소켓주소 구조체의 주소를 쓰고 세번째 인자는 소켓주소 구조체의 크기를 쓴다.

▶ 소켓주소 구조체 SOCKADDR은 BSD 소켓에서와 같은 형태를 가진다(2.1.4절 참조).

▶ 아래는 인터넷 전용으로 정의된 소켓주소 구조체 SOCK- ADDR_IN을 보여주고 있다.

struct SOCKADDR_IN {

short sin_family; /* 주소 체계 */

u_short sin_port; /* 포트번호 */

struct in_addr sin_addr; /* 인터넷 주소 (4바이트) */

char sin_zero[8]; /* 총 16바이트를 맞추기 위한 여백 */

}

struct in_addr {

u_long s_addr; /* 32비트의 IP 주소를 저장할 구조체 */

};

▶ 아래 프로그램 코드는 bind() 함수의 사용 예인데 포트번호로 3000번을 지정하고 있다.

#define PORT_NO 3000

SOCKET s;

char lpszMsg[100];

SOCKADDR_IN m_addr; /* 인터넷 타입의 소켓주소 구조체 */

/* 소켓 개설 */

s = socket(AF_INET, SOCK_STREAM, 0);

/* 소켓주소 구조체의 값을 지정 */

m_addr.sin_family = AF_INET;

m_addr.sin_addr.s_addr = htonl(INADDR_ANY);

m_addr.sin_port = htons(PORT_NO);

bind(s, (LPSOCKADDR)&m_addr, sizeof(m_addr));

▶ 위에서 SOCKADDR_IN과 LPSOCKADDR은 각각 인터넷 타입의 소켓주소 구조체와 일반적인 소켓주소 구조체를 나타냈다.

▶ bind() 함수는 원래 LPSOCKADDR 타입의 인자를 사용하도록 정의되어 있으므로 SOCKADDR_IN 타입의 변수인 m_addr을 bind()를 호출하기 전에 LPSOCKADDR 타입으로 casting하는 것이 필요하다.

▶ 그러나 처음부터 LPSOCKADDR 타입의 소켓주소 구조체를 사용하지 않은 이유는 SOCKADDR_IN 타입이 IP 주소와 포트번호 값을 지정하기에 편리하기 때문이다.

▶ 위에서 htonl()와 htons()는 컴퓨터 내부에서 사용하는 바이트 순서(호스트 바이트 순서)를 네트웍 바이트 순서(데이터의 바이트 단위 전송 순서)로 바꾸는 함수들로 자세한 내용은 2.2.1절을 참조하기 바란다.

(3) listen()

▶ listen()은 연결형(TCP) 소켓을 개설한 서버에서 호출하는데, 지정된 소켓을 통하여 들어오는 클라이언트로부터의 연결요청을 기다리는 함수이다.

▶ listen()의 첫번째 인자는 소켓번호이고, 두번째 인자에는 연결을 대기시킬 수 있는 클라이언트의 최대수를 지정한다.

▶ listen()이 성공적으로 수행된 경우에는 0을 리턴하며, 실패한 경우에는 SOCKET_ERROR를 리턴한다.

▶ 다음은 listen()의 사용 예이다.

listen(s, 5);

(4) accept()

▶ accept()는 연결형 서버에서 listen()을 실행하여 수동 대기모드로 연결요청을 기다리다가 클라이언트에서 서버로 연결요청을 보내오면 이를 수락하기 위하여, 서버가 호출해 두는 함수이다.

▶ accept()의 수행이 성공한 경우에는 소켓이 하나 새로 만들어지며 새로 만들어진 소켓번호가 리턴된다.

▶ 또한 이때 접속된 클라이언트의 소켓주소 구조체의 포인터와 소켓주소 구조체의 길이는 accept() 함수 인자를 통하여 알려준다.

▶ 다음은 accept()의 사용 예인데 accpet()가 성공적으로 리턴되면 연결된 클라이언트의 소켓주소가 구조체 m_client Addr에 실려있게 된다.

SOCKET s; /* 서버가 개설한 소켓번호 */

SOCKET client_s; /* accept()가 리턴할 새로운 소켓번호 */

char lpszMsg[100]; /* 메시지 박스에 출력할 메시지 */

SOCKADDR_IN m_addr; /* 서버의 소켓주소 구조체 */

SOCKADDR_IN m_clientAddr; /* 접속된 클라이언트의 소켓주소 구조체 */

client_s = accept(s, (LPSOCKADDR)&m_clientAddr, &m_nClientAddrLen);

if (client_s == INVALID_SOCKET) {

wsprintf(lpszMsg, "accept() 에러 번호: %d", WSAGetLastError());

MessageBox(hwnd, lpszMsg, "에러", MB_OK);

} else {

wsprintf(lpszMsg, "client IP: %s",inet_ntoa(m_clientAddr.sin_addr.s_addr)); MessageBox(hwnd, lpszMsg, "에러", MB_OK);

}

(5) connect()

▶ 연결형 소켓을 개설한 클라이언트가 서버와 통신을 하려면 먼저 서버와 연결되어야 하는데 이를 위하여 클라이언트가 호출하는 함수가 connect()이다.

▶ 이러한 클라이언트의 connect() 호출에 대해, 서버가 미리 호출해 둔 accept()가 정상적으로 처리되어 연결이 완료되면 connect()는 0을 리턴한다.

▶ 다음은 connect()의 사용 예이다.

#define SERVER_PORT_NO 3001

#define SERVER_IP_ADDR "203.252.65.3"

SOCKET s;

SOCKADDR_IN m_addr; /* 연결할 서버의 소켓주소 구조체 */

char lpszMsg[100];

/* 클라이언트가 연결하려고 하는 서버의 소켓주소 구조체를 만든다 */

m_addr.sin_family = AF_INET;

m_addr.sin_port = htons(SERVER_PORT_NO);

m_addr.sin_addr.s_addr = inet_addr(SERVER_IP_ADDR);

connect(s, (LPSOCKADDR)&m_addr, sizeof(m_addr));

(6) closesocket()

▶ 소켓의 사용이 끝난 후 소켓을 닫기 위해 호출하며 BSD의 close()와 같은 동작을 한다.

▶ closesocket()을 호출하면 아직 처리되지 않은 데이터들, 즉 송신 버퍼에 들어 있으나 아직 목적지로 전송되지 않은 패킷 또는 수신 버퍼에 도착해 있으나 아직 윈속이 읽어들이지 않은 패킷들이 모두 처리된 후에 실제로 소켓이 종료된다.

▶ 그러나 이들 미처리 패킷들을 바로 버리거나, 정해진 시간까지만 처리되기를 기다리려면 setsockopt()을 호출하여 소켓의 옵션을 바꾸어 두어야 한다(다음 항목의 setsockopt() 참조).

(7) setsockopt()

▶ 개설된 소켓의 동작에 관한 각종 옵션을 지정하는 데 사용된다. setsockopt()의 사용 문법은 다음과 같다.

▶ setsockopt()은 송수신 버퍼의 크기 지정, OOB(Out of band) 데이터 처리 방법, 방송형 메시지의 전송 허용 등 여러 가지 소켓 옵션 지정에 사용될 수 있다.

▶ 주로 사용되는 optname으로는, 먼저 소켓 종료시 아직 송수신이 완료되지 않은 패킷들의 처리 방안을 지정하거나(SO_LINGER), 소켓의 디버그 정보를 출력하도록 지정하거나(SO_DEBUG), 해당 소켓에서 방송형 메시지를 전송하는 것을 허용하는 것(SO_ BROADCAST) 등이다.

▶ SO_LINGER 옵션은 closesocket() 호출시 전송 시스템(TCP/IP) 내부에 남아 있는 미전송 패킷 또는 아직 수신 처리되지 않은 패킷들의 처리 방안을 미리 지정하는 것으로 이때 optval은 다음과 같은 구조체 linger의 주소를 가리키도록 한다.

struct linger {

int l_onoff; /* LINGER 모드의 선택/취소 */

int l_linger; /* LINGER 시간 */

};

▶ 예를들어 미처리된 패킷들을 바로 버리지 않고 지정된 시간내에 처리되기를 기다리려면(즉, LINGER를 선택하려면) l_onoff를 1로 세트하고 l_linger에 처리 유예시간을 초단위로 지정해 준다.

▶ LINGER를 선택하지 않으려면 즉, 미처리된 패킷들을 closesocket() 호출시 즉시 버리도록 하려면 l_onoff를 0으로 하고 SO_DONTLINGER 옵션을 선택하여 setsockopt()을 호출하면 된다.

▶ 한편 소켓이 처음 만들어지면 후자의 상태인 DONT- LINGER 상태로 되어 있다.

5.2.2 비동기 모드의 사용

▶ 윈도우(특히 3.1버전)는 멀티태스킹이 지원되지 않는 운영체제이므로 어떤 함수 호출이 블록되어 있으면 시스템 전체가 멈추어 있게 된다.

▶ 윈속에서 개설한 소켓은 기본적으로 blocking 모드로 동작하는데 예를들어 accept()를 호출하였을 때 클라이언트로부터 요구된 connect()가 없다면 서버는 블록된다.

▶ 이를 해결하기 위하여 소켓을 비동기 모드로 바꾸는 것이 필요한데 이를 위하여 WSAAsyncSelect()를 사용한다.

▶ WSAAsyncSelect()를 호출함으로써 소켓은 자동으로 비동기 모드로 바뀌고 블록될 수 있는 함수들(accept(), send(), recv() 등)을 비동기 모드로 처리할 수 있게 된다.

▶ 아래는 WSAAsyncSelect()의 사용 문법이다.

int WSAAsyncSelect (

SOCKET s, /* 상태변화(이벤트)를 감지할 소켓번호 */

HWND hWnd, /* 소켓 s에서 이벤트가 발생했을 때

메시지를 받을 윈도우 핸들 */

UINT wMsg, /* 이벤트 발생시 보낼 메시지 */

long lEvent); /* 관심을 갖는 이벤트를 규정하는 bitmask */

▶ WSAAsyncSelect()는 호출 즉시 리턴되며 에러발생시에는 SOCKET_ERROR를 리턴한다.

▶ 에러가 발생하는 경우는 Winsock.dll이 초기화되지 않았거나, 네트웍이 동작하지 않거나, 다른 blocking 함수가 처리중이거나, 함수의 인자를 잘못 입력한 경우이다.

▶ WSAAsyncSelect()가 에러없이 리턴되면 다음과 같은 조건이 만족된 것이다.

1) 소켓이 비동기 모드로 되었다.

2) 원하는 이벤트가 발생하면 그 사실이 메시지로 전달될 것이다.

3) 이 소켓에 accept()가 호출되어 새로운 소켓이 만들어지면 새로운 소켓에 대해서도 WSAAsyncSelect() 호출시 요구한 이벤트를 똑같이 처리해 준다.

▶ WSAAsyncSelect()를 호출하면 소켓은 비동기 모드로 바뀌고 소켓에서 지정된 이벤트가 발생하면 메시지 전달을 통하여 이 사실을 WSAAsyncSelect()를 호출한 응용 프로그램에게 알리게 된다.

▶ 응용 프로그램에서는 각 이벤트에 해당하는 기능을 수행하면 된다.

▶ 이벤트의 종류에는 수신 데이터의 도착, 데이터를 송신할 준비가 됨, 연결요청을 accept할 준비가 됨 등의 사건들이 있는데, 응용 프로그램이 관심을 갖는 이벤트 즉, 알고 싶은 소켓의 상태변화는 WSAAsyncSelect()를 호출할 때 lEvent 인자에 지정할 수 있다.

▶ 해당 소켓에서, lEvent 인자에 지정된 이벤트 중 하나라도 발생하면 윈속은 응용 프로그램에게 이 사실을 알려주게 된다.

▶ 이벤트의 종류는 표 5-3과 같다.

이벤트

의 미

FD_READ 수신된 데이터가 있음
FD_WRITE 데이터 송신이 가능함(송신 버퍼가 비었음)
FD_OOB Out of band 데이터가 수신됨
FD_ACCEPT connect 요청이 있어 accept 처리가 가능함 (서버에서)
FD_CONNECT connect 요청이 성공적으로 이루어짐 (클라이언트에서)
FD_CLOSE 소켓 종료가 이루어짐

표 5-3 WSAAsyncSelect()에서 다루는 이벤트 종류

▶ 표 5-3에서 FD_READ 이벤트는 해당 소켓에 데이터가 수신되어 있으며 현재 이를 읽을 수 있다는 것을 알리는 메시지이다.

▶ 이때 응용 프로그램은 소켓 종류에 따라(TCP또는 UDP) recv() 또는 recvfrom()으로 데이터를 읽으면 된다.

▶ FD_WRITE는 연결형 소켓이 상대방 소켓과 처음 연결될 때, 즉 서버에서는 accept()가 성공하거나 클라이언트에서는 connect()가 성공한 때에 처음 발생한다. 이 때 응용 프로그램은 send()를 호출하여 데이터를 송신할 수 있다.

▶ 데이터가 송신버퍼를 떠나 송신버퍼가 비게 되어도 FD_ WRITE가 발생하는데, 이것은 이제 새로운 송신이 가능하다는 것을 의미한다.

▶ FD_ACCEPT는 서버 소켓에서 발생하는데, 클라이언트가 connect()로 연결요청을 해왔으므로 서버가 accept()를 처리할 수 있다는 것을 알려준다. 즉, 해당 소켓으로 accept()를 호출할 수 있음을 알려준다.

▶ FD_CONNECT는 클라이언트에서 발생하는데 connect() 시스템 콜이 성공적으로 이루어졌을 때 발생하는 이벤트이다.

▶ 수신측에 도착한 모든 패킷은 일반적으로 도착한 순서대로 버퍼에 저장된 후 차례대로 처리된다.

▶ OOB 데이터는 이러한 순서를 무시하고 우선적으로 처리되어야 할 데이터를 말하며 이를 처리하기 위하여 OOB 데이터가 도착하면 FD_OOB 이벤트가 발생한다.

▶ OOB 데이터는 'Ctrl-C'같은 제어 문자를 전달하는데 사용된다.

▶ WSAAsyncSelect()는 각 소켓에 대하여 한 번만 부르는 것이 안전하다. 만약 WSAAsyncSelect()를 중복하여 호출하면 이전에 호출된 WSAAsyncSelect()의 각종 옵션은 무시되므로 주의하여야 한다.

▶ 이전에 지정한 WSAAsyncSelect() 옵션을 취소하여 어떤 이벤트 발생에 대한 메시지를 수신하고 싶지 않으면 wMsg=0, lEvent=0로 하여 WSAAsyncSelect()를 다시 호출하면 된다.

▶ 아래는 소켓 m_s로 클라이언트의 접속요청 connect()가 접수되어 FD_ACCEPT 이벤트가 발생하면 사용자 메시지(WM_USER + 1)를 보내도록 하는 예이다.

WSAAsyncSelect(m_s, m_hWnd, WM_USER + 1, FD_ACCEPT);

▶ 이벤트를 처리하는 프로그램에서는 이벤트의 종류를 알기 위하여 변수 lParam를 참조하여야 하는데 lParam의 하위 16비트에는 발생한 이벤트의 종류가 실려있고 상위 16비트에는 에러코드가 실려있다.

▶ 이러한 이벤트 종류와 에러코드를 lParam에서 편리하게 찾아내기 위하여 매크로 WSAGETSELECTEVENT()와 WSA- GETSELECTERROR()가 다음과 같이 정의되어 있다.

#define WSAGETSELECTEVENT(lParam) LOWORD(lParam)

#define WSAGETSELECTERROR(lParam) HIWORD(lParam)

▶ 아래의 코드는 응용 프로그램에서 발생한 이벤트가 FD_ACCEPT인지를 즉, 클라이언트로부터 연결요청이 와서 이벤트가 발생했는지를 확인하고 accept()를 호출하는 예이다.

if (WSAGETSELECTEVENT(lParam) == FD_ACCEPT) {

m_clientS = accept(m_s, NULL, NULL);

}

▶ 어떤 소켓에서 이벤트가 하나 발생하면 메시지 전송은 한번만 일어난다. 그러나 이 소켓에 같은 종류의 이벤트가 다시 발생할 수 있는데, 이와같이 같은 이벤트가 다시 발생할 때마다 새로운 메시지가 응용 프로그램으로 전달되도록 하는 것이 필요하다.

▶ 예를들어 FD_READ 이벤트가 발생한 소켓에 응용 프로그램이 recv() 함수를 호출하여 데이터를 읽어들인 후에 새로운 데이터가 수신되면 FD_READ 이벤트가 다시 발생하여야 이를 읽을 수 있다.

▶ 이것은 recv() 호출시 내부적으로 자동으로 처리되도록 되어 있다(즉, revc() 호출이 WASAsyncSelect()를 다시 호출한 효과를 준다).

▶ 표 5-4에 같은 종류의 메시지를 반복하여 발생시키도록 하는 함수들과 해당 이벤트를 정리하였다.

▶ 예를들어 FD_ACCEPT 이벤트가 발생하였을 때 accept()를 호출하여 이를 처리하면 새로운 FD_ACCEPT가 발생할 때 같은 종류의 메시지가 다시 응용 프로그램에게 전달된다.

메시지 반복 발생 함수

관련 이벤트

accept() FD_ACCEPT
send(), sendto() FD_WRITE
recv(), recevfrom() FD_READ
recv() FD_OOB

표 5-4 메시지 반복 발생 함수

5.2.3 윈속의 IP 주소 변환 함수

(1) WSAAsyncGetHostByAddr()

▶ IP 주소로부터 해당 호스트의 이름과 기타 정보를 알아내기 위한 비동기 함수이며 BSD 소켓의 gethostbyaddr()과 마찬가지로 hostent 구조체를 리턴한다.

▶ hostent 구조체의 내용은 아래와 같다.

struct hostent {

char FAR * h_name; /* 호스트 이름 */

char FAR * FAR * h_aliases; /* 호스트의 별명 목록 */

shor h_addrtype; /* 호스트 주소 타입 */

short h_length; /* 주소의 길이 */

char FAR * FAR * h_addr_list; /* 주소 목록 */

#define h_addr h_addr_list[0] /* 대표적인 주소를 h_addr에 저장 */

};

▶ WSAAsyncGetHostByAddr()의 사용 문법은 다음과 같다.

HANDLE WSAAsyncGetHostByAddr (

HWND Hwnd, /* 윈도우 핸들 */

unsigned int Msg, /* 이 함수 종료시 전송할 메시지 */

const char *addr, /* 호스트의 IP 주소 */

int len, /* addr의 길이 */

int type, /* 주소 타입으로 인터넷에서는 AF_INET */

char *buf, /* hostent 구조체의 포인터 */

int buflen); /* buf의 길이 */

▶ 아래는 WSAAsyncGetHostByAddr()의 사용 예인데 IP 주소가 203.252.65.3인 호스트의 도메인 네임을 메시지 박스에 출력한다.

▶ 아래에서 PHOSTENT는 hostent 구조체를 가리키는 포인터 타입을 지정하며 MAXGETHOSTSTRUCT는 hostent 구조체를 담기에 충분한 크기의 상수로 Winsock.h에 정의되어 있다.

u_long m_Addr ;

char m_lpszHostEntryBuf[MAXGETHOSTSTRUCT] ;

PHOSTENT m_phostent; /* hostent 구조체 포인터 */

m_Addr = inet_addr("203.252.65.3");

WSAAsyncGetHostByAddr(m_hWnd, WM_USER_ASYNCGETHOSTBYADDR,

(char *)&m_Addr, 4, PF_INET, m_lpszHostEntryBuf, MAXGETHOSTSTRUCT) ;

m_phostent = (PHOSTENT)m_lpszHostEntryBuf ;

MessageBox(hwnd, m_phostent->h_name, "호스트 이름", MB_OK);

▶ 한편 WSAAsyncGetXByY과 같은 형태의 비동기 함수의 실행 결과는 lParam에 들어있게 된다.

▶ lParam의 상위 16비트에는 에러코드가 하위 16비트에는 비동기 함수 실행 결과가 들어있는 버퍼의 크기가 들어 있다.

▶ 이러한 에러 값과 버퍼의 크기 값을 lParam으로부터 편리하게 알아내기 위하여 WSAGETASYNCERROR()와 WSA- GETASYNCBUFLEN() 매크로가 각각 정의되어 있다.

(2) WSAAsyncGetServByName()

▶ 이 함수는 ftp, mail과 같은 TCP/IP 표준 응용 프로그램의 포트번호를 찾기 위해 사용되는 비동기 함수이다.

▶ 이 함수가 성공적으로 수행되면 servent라는 구조체의 포인터를 리턴하는데 servent 구조체의 내용은 다음과 같다.

struct servent {

char FAR * s_name; /* 공식적인 서비스 이름 */

char FAR * FAR * s_aliases; /* 서비스의 별명 리스트 */

short s_port; /* 포트번호 */

char FAR * s_proto; /* 프로토콜 종류 */

};

▶ WSAAsyncGetServByName()의 사용 문법은 다음과 같다.

HANDLE WSAAsyncGetServByName (

HWND Hwnd, /* 이 함수의 메시지를 수신할 윈도우 핸들 */

unsigned int Msg, /* 이 함수 종료시 전송할 메시지 */

const char *name, /* 포트번호를 알고자 하는 서비스 이름 */

const char *proto, /* TCP나 UDP를 지정(또는 NULL) */

char *buf, /* servent 구조체의 포인터 */

int buflen); /* buf의 길이 */

▶ 이 함수의 사용 예로 UDP 프로토콜로 구현된 time 서비스의 포트번호를 찾아내어 화면에 출력하는 예를 아래에 보였다.

▶ 여기서 PSERVENT는 servent 구조체를 가리키는 포인터 타입을 말한다.

PSERVENT m_pservent; /* servent 구조체 포인터 */

char m_lpszMsg[100]; /* 화면에 출력할 메시지 버퍼 */

char m_lpszServEntryBuf[MAXGETHOSTSTRUCT];

WSAAsyncGetServByName(m_hWnd, WM_USER_ASYNCGETSERVBYNAME, "time",

"udp", m_lpszServEntryBuf, MAXGETHOSTSTRUCT) ;

m_pservent = (PSERVENT)m_lpszServEntryBuf;

wsprintf(m_lpszMsg, "%s 서비스 포트 : %d", m_pservent->s_name,

m_pservent->s_port);

(3) WSAAsyncGetHostByName()

▶ 이 함수는 호스트 이름을 사용하여 IP 주소를 찾아내는 함수이며 사용 문법은 아래와 같다.

HANDLE WSAAsyncGetHostByName (

HWND hwnd, /* 메시지를 수신할 윈도우 핸들 */

unsigned int wMsg, /* 수신할 메시지 */

char FAR *name, /* 찾을 호스트 네임의 포인터 */

char FAR *buf, /* 결과가 리턴되는 버퍼의 포인터 */

int bufflen); /* 버퍼의 길이 */

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 프로그램

5.4 윈속 채팅 클라이언트

▶ 윈속 채팅 클라이언트 프로그램 WChat_Client.c는 사용자가 키보드에서 입력한 문자열을 서버에게 전송하고 서버가 보내온 메시지를 화면에 출력하는 기능을 수행한다.

▶ 그림 5-3은 채팅 클라이언트 프로그램의 실행 예이다 (그림 5-3을 구성하는 리소스 파일 Client.rc는 Visual C로 작성하였다).

그림 5-3 채팅 클라이언트 프로그램 실행 예

5.4.1 프로그램 주요부분 설명

(1) 헤더 파일과 전역변수

▶ WChat_Client.c는 다음과 같은 헤더 파일과 전역변수를 필요로 한다.

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

#include <stdlib.h> /* 문자 처리를 위한 헤더 파일 */

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

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

HINSTANCE hInst; /* 현재 윈도우의 인스턴스 */

WNDPROC OldProc; /* 서브클래싱을 하기 위해 저장할 기존의 프로시져 */

char msg[512]; /* 입출력용 데이터 */

SOCKET m_s; /* 소켓번호 */

SOCKADDR_IN m_addr; /* 인터넷 타입의 소켓주소 구조체 */

HWND hParent; /* Parent 윈도우의 핸들 */

(2) WM_INITDIALOG의 처리

▶ 윈도우가 처음 생설될 때 발생하는 메시지인 WM_ INITDIALOG의 처리부에서는 다음과 같이 윈속의 초기화(InitSocket())와 다음에 설명한 에디트 컨트롤 서브클래싱(subclassing)을 수행한다.

case WM_INITDIALOG:

memset(msg, '\0', sizeof(msg));

if(!InitSocket(hWnd))

return FALSE;

OldProc = (WNDPROC) SetWindowLong (GetDlgItem(hWnd, IDC_EDIT),

GWL_WNDPROC, (LONG)EditProc);

return TRUE;

(3) 서브클래싱

▶ 클라이언트 프로그램은 다이얼로그 박스로부터 실행이 시작되는데 에디트 컨트롤(edit control)을 통하여 데이터를 입력받는다.

▶ 에디트 컨트롤에서는 사용자가 리턴('\n')까지 입력한 문자열을 받아, 사용자 정의 메시지인 WM_ASYNC를 이용하여 처리한다. 이를 위하여 서브클래싱을 사용한다.

▶ 서브클래싱이란 윈도우에서 수행되던 프로시져를 사용자 정의 프로시져로 연결하여 원하는 작업을 하다가 그 작업이 완료되면 다시 원래의 프로시져로 되돌아오는 기능을 말한다.

▶ 본 예제에서는 서브클래싱을 이용하여 사용자가 리턴키를 입력한 사실을 Parent 윈도우에 알려준다.

▶ 아래의 프로그램 코드는 현재의 프로시져를 OldProc로 잠시 저장하고 사용자 프로시져인 EditProc를 등록하는 것을 나타낸다.

OldProc = (WNDPROC) SetWindowLong (GetDlgItem(hWnd, IDC_EDIT),

GWL_WNDPROC, (LONG)EditProc);

▶ 위에서 IDC_EDIT는 에디트 컨트롤의 ID이며, EditProc는 에디트 컨트롤을 처리하기 위해 새로 정의한 프로시져 이름이다.

(4) WM_CHAR 메시지 처리

▶아래는 EditProc의 구현 내용으로, 사용자가 키보드로 글자를 입력하였을 때 발생하는 메시지 WM_CHAR를 처리한다.

▶ 리턴키를 제외한 다른 문자들은 모두 원래의 프로시져로 보내며(CallWindowProc() 호출) 사용자가 리턴키를 누르면 지금까지 에디트 컨트롤에 저장된 데이터를 GetWindowText() 함수를 사용하여 얻어온 후 PostMessage()를 사용하여 사용자 정의 메시지 WM_ASYNC를 발생시킨다.

LRESULT CALLBACK EditProc(HWND hWnd, UINT iMsg, WPARAM wParam, LPARAM lParam) {

switch(iMsg) {

case WM_CHAR:

if(wParam == VK_RETURN) { /* 리턴키 입력 */

/* 현재 에디트 컨트롤에 저장된 문자열을 얻는다 */

GetWindowText(hWnd, (LPTSTR)msg, sizeof(msg));

/* WM_ASYNC 메시지 발생 */

PostMessage(hParent, WM_ASYNC, m_s,

WSAMAKESELECTREPLY(FD_WRITE, 0));

SetWindowText(hWnd, "");

break;

}

}

/* 리턴키가 아니면 원래의 프로시져로 문자열을 입력 */

return CallWindowProc(OldProc, hWnd, iMsg, wParam, lParam);

}

(5) WM_COMMAND의 처리

▶ "종료" 버튼이 눌릴 때 발생하는 메시지로 소켓을 종료하기 위하여 WSACleanup()을 호출하고 윈도우에 종료 메시지 WM_DESTROY를 보내기 위하여 Destroy- Window() 함수를 호출한다.

case WM_COMMAND:

switch(wParam) {

case IDC_EXIT: /* 종료버튼 입력 처리 */

closesocket(m_s);

m_s = INVALID_SOCKET; /* 소켓을 원상태로 초기화 */

WSACleanup();

DestroyWindow(hWnd); /* WM_DESTROY 발생 */

return TRUE;

}

break;

(6) WM_DESTROY의 처리

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

▶ 아래에서 PostQuitMessage() 함수는 윈도우 메시지 큐에 종료 메시지를 전송한다.

case WM_DESTROY:

PostQuitMessage(0);

return TRUE;

 

(7) WM_ASYNC의 처리

▶ 서버와의 연결, 연결종료, 데이터 송수신 등은 시스템의 I/O 변화시 발생하는 메시지 WM_ASYNC를 이용하여 처리된다.

▶ 발생한 이벤트의 종류를 구분하기 위하여 매크로 WSA- GETSELECTEVENT()를 사용한다.

▶ WChat_Client.c에서 처리해야 할 이벤트의 종류는 FD_ CONNECT, FD_CLOSE, FD_WRITE, FD_READ이며 이들을 각각 처리하는 것을 아래에 보였다.

case WM_ASYNC:

hListBox = GetDlgItem(hWnd, IDC_LIST);

switch(WSAGETSELECTEVENT(lParam)) {

case FD_CONNECT:

/* 서버와 연결 처리 */

case FD_CLOSE:

/* 소켓 종료 처리 */

case FD_WRITE:

/* 채팅 데이터 송신 처리 */

case FD_READ:

/* 채팅 데이터 수신 처리 */

default:

break;

}

WChat_Client.c 프로그램

댓글 0