책정리/윈도우 네트워크 프로그래밍

9장 소켓 입출력 모델(I)

GONII 2015. 6. 23. 15:57
  • 개요

    좀더 많은 클라이언트 접속을 효율적으로 처리하려면 새로운 입출력 모델이 필요하다.

    소켓 모드

    소켓은 소켓 함수 호출 시 동작 방식에 따라 블로킹(blocking)과 넌블로킹(nonblocking)소켓으로 구분하며, 이를 소켓 모드(socket mode)라고 부른다.

    • 블로킹 소켓 특징

      소켓 함수 호출 시 조건이 만족되지 않으면 함수는 리턴하지 않고 해당 스레드는 대기 상태(wait state)가 된다. 대표적인 소켓 함수와 리턴 조건은 다음과 같다.

소켓 함수

리턴 조건

accept( )

클라이언트가 접속했을 때

send( ), sendto( )

송신 버퍼에 데이터를 모두 복사했을 때

recv( ), recvfrom( )

수신 버퍼에 도착한 데이터가 있을 때

이러한 조건이 만족되지 않으면 소켓 함수는 리턴하지 않으므로 멀티스레드를 사용하여 다른 작업을 하지 않는 한 애플리케이션이 더는 진행할 수 없다.

  • 넌블로킹 소켓

    소켓 함수 호출 시 조건이 만족되지 않더라도 함수가 리턴하므로 해당 스레드는 계속 진행할 수 있다. socket( )함수는 기본적으로 블로킹 소켓을 생성하므로 다음과 같이 ioctlsocket( )함수를 호출해야만 넌블로킹 소켓으로 바꿀 수 있다.

// 블로킹 소켓 생성

SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0);

if(listen_sock == INVALID_SOCKET) err_quit("socket()");

   

// 넌블로킹 소켓으로 전환

u_long on = 1;

retval = ioctlsocket(listen_sock, FIONBIO, &on);

if(retval == SOCKET_ERROR) err_quit("ioctlsocket()");

넌블로킹 소켓에 대해 소켓 함수를 호출했을 때 조건이 만족되지 않아 작업을 완료할 수 없으면 소켓 함수는 오류를 리턴한다. 이때는 WSAGetLastError() 함수를 호출하여 반드시 오류 코드를 확인해야 한다. 넌블로킹 소켓을 사용할 경우 대개 오류 코드는 WSAEWOULDBLOCK이 되며, 이는 조건이 만족되지 않음을 나타내므로 나중에 다시 소켓 함수를 호출하면 된다.

  • 예제 NonblockServer.cpp

#include <winsock2.h>

#include <stdlib.h>

#include <stdio.h>

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

   

#define BUFSIZE 512

   

// 소켓 함수 오류 출력 후 종료

void err_quit(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);

LocalFree(lpMsgBuf);

exit(-1);

}

   

void err_display(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

printf("[%s] %s", msg, (LPCTSTR)lpMsgBuf);

LocalFree(lpMsgBuf);

}

   

int main(int argc, char* argv[])

{

int retval;

   

// 윈속 초기화

WSADATA wsa;

if( WSAStartup(MAKEWORD(2,2), &wsa) != 0 )

return -1;

   

// socket()

SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0);

if( listen_sock == INVALID_SOCKET)

err_quit("socket()");

   

// 넌블로킹 소켓으로 전환

u_long on = TRUE;

retval = ioctlsocket(listen_sock, FIONBIO, &on);

   

// bind

SOCKADDR_IN servAddr;

ZeroMemory(&servAddr, sizeof(servAddr));

servAddr.sin_family = AF_INET;

servAddr.sin_port = htons(9000);

servAddr.sin_addr.s_addr = htonl(INADDR_ANY);

retval = bind(listen_sock, (SOCKADDR*)&servAddr, sizeof(servAddr));

if( retval == SOCKET_ERROR )

err_quit("bind()");

   

// listen

retval = listen(listen_sock, SOMAXCONN);

if( retval == SOCKET_ERROR )

err_quit("listen()");

   

// 데이터 통신에 사용할 변수

SOCKET client_sock;

SOCKADDR_IN clientAddr;

int addrLen;

char buf[BUFSIZE+1];

   

while(1)

{

// accept()

addrLen = sizeof(clientAddr);

client_sock = accept(listen_sock, (SOCKADDR*)&clientAddr, &addrLen);

if( client_sock == INVALID_SOCKET )

{

if( WSAGetLastError() != WSAEWOULDBLOCK )

err_display("accept()");

continue;

}

printf("\n[TCP 서버]클라이언트 접속: IP주소=%s, 포트번호=%d\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));

   

// 클라이언트와 데이터 통신

while(1)

{

// 데이터 받기

retval = recv(client_sock, buf, BUFSIZE, 0);

if( retval == SOCKET_ERROR )

{

if( WSAGetLastError() != WSAEWOULDBLOCK)

{

err_display("recv()");

break;

}

}

else if ( retval == 0 )

{

break;

}

else

{

// 받은 데이터 출력

buf[retval] = '\0';

printf("%s", buf);

}

}

   

// closesocket()

closesocket(client_sock);

printf("\n[TCP 서버]클라이언트 종료: IP주소=%s, 포트번호=%d\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));

}

   

// closesocket()

closesocket(listen_sock);

   

// 윈속 종료

WSACleanup();

   

return 0;

}

   

  • 넌블로킹 소켓의 장단점

장점

  • 소켓 함수 호출 시 블록되지 않으므로 다른 작업을 진행할 수 있다.
  • 멀티스레드를 사용하지 않고도 여러 개의 소켓 입출력을 처리할 수 있다.

단점

  • 소켓 함수를 호출할 때마다 WSAEWOULDBLOCK 등 오류 코드를 확인하고, 다시 해당 함수를 호출해야 하므로 프로그램 구조가 복잡해진다.
  • 블로킹 소켓을 사용한 경우보다 CPU 사용률이 높다.

서버 작성 모델

지금까지 배운 TCP 혹은 UDP 서버는 반복 서버(iterative server)와 병행 서버(concurrent server) 중 하나로 분류할 수 있다. 각 모델의 특징은 다음과 같다

  • 반복 서버

    접속한 여러 클라이언트를 하나씩 차례대로 처리한다.

장점

하나의 스레드로 클라이언트를 처리하므로 시스템 자원 소모가 적다

단점

서버와 클라이언트의 통신 시간이 길어지면 다른 클라이언트의 대기 시간이 길어진다

이와 같은 특징으로 인해 반복 서버는 일반적으로 UDP 서버를 작성할 때 적합하다. 그러나 UDP서버가 파일 전송과 같이 시간이 오래 걸리는 작업을 처리한다면 병행 서버로 작성하는 것이 바람직하다.

  • 병행 서버

    접속한 여러 클라이언트를 병렬적으로 처리한다. 5장에서 멀티스레드를 이용하여 작성한 TCP 서버가 이에 해당한다.

장점

서버와 클라이언트의 통신 시간이 길어지더라도 다른 클라이언트의 통신에 영향을 주지 않는다.

단점

멀티프로세스 또는 멀티스레드를 이용하여 구현하므로 시스템 자원 소모가 크다.

이와 같은 특징으로 인해 병행 서버는 일반적으로 TCP 서버를 작성할 때 적합하다. 그러나 TCP 서버가 각각의 클라이언트와 매우 짧은 시간 동안만 통신하고, 접속한 클라이언트 수가 많지 않다고 가정한다면 반복 서버로 작성해도 된다.

이상적인 소켓 입출력 모델

바람직한 소켓 입출력 모델은 반복 서버와 병행 서버의 장점을 모두 가지면서 각각의 단점을 해결한 형태가 될 것이다. 유한한 자원(CPU, 메모리, ...)을 가진 시스템에서 실행되는 서버가 제공해야 하는 이상적인 기능은 다음과 같다.

  • 모든 클라이언트 접속이 성공한다.
  • 서버는 각 클라이언트의 서비스 요청에 최대한 빠르게 반응하며(short response time), 고속으로 데이터를 전송한다(high throughput).
  • 위와 같은 기능을 제공하되 시스템 자원 사용량을 최소화한다. 즉, CPU 사용률이나 메모리 사용량 등을 최소화한다.

이와 같은 이상적인 서버를 작성하기 위해 소켓 입출력 모델에 요구되는 사항을 정리하면 다음과 같다.

  • 소켓 함수 호출 시 블로킹을 최소화한다.

    블로킹을 없애기 위해 넌블로킹 소켓을 사용하는 것으로는 충분하지 않다. CPU 사용률이 불필요하게 높아지기 때문이다. 이상적인 소켓 입출력 모델은 CPU 사용률을 최소로 하면서 넌블로킹 소켓을 사용한 모든 소켓 함수 호출이 성공하도록 할 것이다. 이때 함수 호출이 성공한다는 것은 함수 호출 시 조건이 만족되지 않아서 WSAEWOULDBLOCK 등 오류 코드가 발생하는 것을 미연에 방지한다는 뜻이다.

  • 입출력 작업을 다른 작업과 병행한다.

    하드웨어는 진정한 의미의 병렬 동작이 가능하므로 입출력 작업과 CPU 명령 수행(즉, 프로그램 실행)을 동시에 진행할 수 있다. 이상적인 소켓 입출력 모델은 이러한 특성을 최대한 활용하여 동일한 시간 동안 애플리케이션이 더욱 많은 일을 처리할 수 있도록 할 것이다.

  • 스레드 개수를 최소화한다.

    하나의 스레드를 생성할 때마다 운영체제는 관련 데이터 구조체를 위한 메모리를 할당해야 한다. 또한 스레드마다 스택을 따로 사용하므로 이를 위한 메모리도 필요하게 된다. 한편, 시스템 내의 스레드 수가 많아질수록 각 스레드의 응답 속도가 느려지므로 결과적으로 서버의 성능 저하로 이어지게 된다. 이상적인 소켓 입출력 모델은 CPU 하나당 하나의 스레드를 실행하여 여러 클라이언트 접속을 처리하도록 할 것이다.

  • 유저 모드와 커널 모드 전환 횟수와 데이터 복사(copy)를 최소화 한다.

    일반적으로 유저 모드(user mode)와 커널 모드(kernel mode) 전환은 상당항 CPU 사이클(cycle)을 소모한다. 이상적인 소켓 입출력 모델은 가능한 모든 작업을 모드 전환 없이 처리할 것이다. 또한 불필요한 데이터 복사를 최소화할 것이다.

소켓 입출력 모델의 종류

  • 운영체제별 소켓 입출력 모델 지원

소켓 입출력 모델

윈도우 CE

윈도우(클라이언트 버전)

윈도우(서버 버전)

Select

CE 1.0 이상

윈도우 95 이상

윈도우 NT 이상

WSAAsyncSelect

x

윈도우 95 이상

윈도우 NT 이상

WSAEventSelect

CE .NET 4.0 이상

윈도우 95 이상

윈도우 NT 3.51 이상

Overlapped

CE .NET 4.0 이상

윈도우 95 이상

윈도우 NT 3.51 이상

Completion Port

x

윈도우 NT 3.5 이상(윈도우 95/98/Me 제외)

  

  • Select 모델

    Select 모델은 Select() 함수가 핵심적인 역할을 한다는 뜻에서 붙인 이름이다. Select모델을 사용하면 소켓 모드(블로킹, 넌블로킹)에 관계없이 여러 소켓을 한 스레드로 처리할 수 있다.

    동작 원리

    Select 모델이 제공하는 핵심적인 기능을 소켓 함수를 호출해야 할 시점을 알려줌으로써 함수 호출 시 항상 성공하도록 하는 것이다.

    소켓 모드에 따른 Select 모델의 효과는 다음과 같다

    • 블로킹 소켓 : 소켓 함수 호출 시 조건이 만족되지 않아 블로킹 되는 상황을 막을 수 있다.
    • 넌블로킹 소켓 : 소켓 함수 호출 시 조건이 만족되지 않아 다시 호출해야 하는 상황을 막을 수 있다.

    Select 모델을 사용하기 위해서는 세 개의 소켓 셋(socket set)을 준비해야 한다. 소켓 셋은 소켓 디스크립터의 집합으로써 해당 소켓으로 하고자하는 작업의 종류를 나타낸다 예를 들어, 어떤 소켓에 대해 recv() 함수를 호출해야 할 시점을 알고 싶다면 읽기 셋에 넣고, send() 함수를 호출해야 할 시점을 알고 싶다면 쓰기 셋에 넣으면 된다.

    소켓 셋을 준비해서 select()함수에 전달하면 함수는 소켓 셋에 포함된 소켓에 대해 해당 작업을 할 수 있을 때까지 대기한다. 적어도 하나의 소켓이 준비가 되면 select()함수는 리턴한다. 이때 소켓 셋에는 준비가 된 소켓만 남고 나머지는 모두 제거된다.

    애플리케이션은 소켓 셋을 통해 소켓 함수의 적절한 호출 시점을 알아내거나 소켓 함수의 호출 결과를 확일 할 수 있다.

    • 읽기 셋(read set)

함수 호출 시점

  • 클라이언트가 접속했으므로 accept() 함수를 호출할 수 있다
  • 데이터를 받았으므로 recv(), recvfrom() 등의 함수를 호출할 수 있다.
  • 연결이 종료되었으므로 recv(), recvfrom() 등의 함수를 호출할 수 있다. 이때 리턴값은 0 또는 SOCKET_ERROR가 된다.
  • 쓰기 셋(write set)

함수 호출 시점

송신 버퍼가 충분하므로 send(), sendto() 등의 함수를 호출하여 데이터를 보낼 수 있다.

함수 호출 결과

넌블로킹 소켓을 사용한 connect() 함수 호출이 성공하였다.

  • 예외 셋(exception set)

함수 호출 시점

OOB(Out-of-Band) 데이터가 도착했으므로 recv(), recvfrom() 등의 함수를 호출하여 OOB 데이터를 받을 수 있다.

함수 호출 결과

넌블로킹 소켓을 사용한 connect() 함수 호출이 실패하였다.

select() 함수 원형은 다음과 같다

int select(

int nfds,

fd_set FAR * readfds,

fd_set FAR * writefds,

fd_set FAR * exceptfds,

const struct timeval FAR * timeout

);

// 성공 : 조건을 만족하는 소켓의 개수 또는 0(타임아웃)

// 실패 : SOCKET_ERROR

  • nfds

    유닉스와의 호환성을 위해 존재하며 윈도우에서 사용하지 않는다

  • readfds, writefds, exceptfds

    각각 읽기 셋, 쓰기 셋, 예외 셋을 나타낸다. 최대 두 개까지 NULL 값이 될 수 있다.

  • timeout

    초(seconds)와 마이크로초(microseconds)단위로 타임아웃을 나타낸다

struct timeval {

long tv_sec; /* seconds */

long tv_usec; /* and microseconds */

};

typedef struct timeval TIMEVAL;

타임아웃 값에 따른 select() 함수의 동작은 다음과 같다

  • NULL : 적어도 한 소켓이 조건을 만족할 때까지 무한히 기다린다. 리턴값은 조건을 만족하는 소켓의 개수가 된다.
  • {0, 0} : 소켓 셋에 포함된 모든 소켓을 검사한 후 곧바로 리턴한다. 리턴값은 조건을 만족하는 소켓의 개수 또는 0(타임아웃)이 된다.
  • 양수 : 적어도 한 소켓이 조건을 만족할 때까지 기다리되 타임아웃으로 지정한 시간이 지나면 리턴한다. 리턴값은 조건을 만족하는 소켓의 개수 또는 0(타임아웃)이 된다.

   

select() 함수를 이용한 소켓 입출력 절차는 다음과 같다

  • 소켓 셋을 비운다.(초기화)
  • 소켓 셋에 소켓을 넣는다. 셋에 넣을 수 있는 소켓의 최대 개수는 FD_SETSIZE(64)로 정의되어 있다.
  • select() 함수를 호출한다. select() 함수는 블로킹 함수로 동작하므로 조건을 만족하는 소켓이 있을 때까지 리턴하지 않는다.
  • select() 함수가 리턴한 후 소켓 셋에 남아있는 모든 소켓에 대해 적절한 소켓 함수를 호출하여 처리한다.
  • 1 ~ 4 를 반복한다.

   

소켓 셋을 다루기 위한 다음과 같은 매크로 함수가 제공된다

매크로 함수

역할

FD_CLR(SOCKET s, fd_set* set)

셋에서 소켓 s를 제거한다

FD_ISSET(SOCKET s, fd_set* set)

소켓 s가 셋에 들어 있으면 0이 아닌 값을 리턴한다.
그렇지 않으면 0을 리턴한다.

FD_SET(SOCKET s, fd_set* set)

셋에 소켓 s를 넣는다.

FD_ZERO(fd_set* set)

셋을 비운다(초기화)

[실습] Select 모델을 이용한 TCP 서버

  • 예제 SelectSever.cpp

#include <winsock2.h>

#include <stdlib.h>

#include <stdio.h>

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

   

#define BUFSIZE 512

   

// 소켓 정보 저장을 위한 구조체

struct SOCKETINFO

{

SOCKET sock;

char buf[BUFSIZE+1];

int recvbytes;

int sendbytes;

};

   

int nTotalSockets = 0;

SOCKETINFO* socketInfoArray[FD_SETSIZE];

   

// 소켓 관리 함수

BOOL addSocketInfo(SOCKET sock);

void removeSocketInfo(int nIndex);

// 오류 출력 함수

void err_quit(char* msg);

void err_display(char* msg);

   

int main(int argc, char* argv[])

{

int retval;

   

// 윈속 초기화

WSADATA wsa;

if( WSAStartup(MAKEWORD(2,2), &wsa) != 0 )

return -1;

   

// socket()

SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0);

if( listen_sock == INVALID_SOCKET)

err_quit("socket()");

   

// bind()

SOCKADDR_IN serverAddr;

ZeroMemory(&serverAddr, sizeof(serverAddr));

serverAddr.sin_family = AF_INET;

serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);

serverAddr.sin_port = htons(9000);

retval = bind(listen_sock, (SOCKADDR*)&serverAddr, sizeof(serverAddr));

if( retval == SOCKET_ERROR )

err_quit("bind()");

   

// listen()

retval = listen(listen_sock, SOMAXCONN);

if( retval == SOCKET_ERROR )

err_display("listen()");

   

// 넌블로킹 소켓으로 전환

u_long on = TRUE;

retval = ioctlsocket(listen_sock, FIONBIO, &on);

if( retval == SOCKET_ERROR )

err_display("ioctlsocket()");

   

// 데이터 통신에 사용할 변수

FD_SET rset;

FD_SET wset;

SOCKET client_sock;

SOCKADDR_IN clientAddr;

int addrLen;

   

printf("[서버] 초기화 성공, 접속 대기...\n");

   

while(1)

{

// 소켓 셋 초기화

FD_ZERO(&rset);

FD_ZERO(&wset);

FD_SET(listen_sock, &rset);

for( int i = 0 ; i < nTotalSockets ; i++ )

{

if( socketInfoArray[i]->recvbytes > socketInfoArray[i]->sendbytes )

FD_SET(socketInfoArray[i]->sock, &wset);

else

FD_SET(socketInfoArray[i]->sock, &rset);

}

   

// select

retval = select(0, &rset, &wset, NULL, NULL);

if( retval == SOCKET_ERROR )

err_quit("select()");

   

// 소켓 셋 검사(1) : 클라이언트 접속 수용

if( FD_ISSET(listen_sock, &rset) )

{

addrLen = sizeof(clientAddr);

client_sock = accept(listen_sock, (SOCKADDR*)&clientAddr, &addrLen);

if( client_sock == INVALID_SOCKET )

{

if( WSAGetLastError() != WSAEWOULDBLOCK )

err_display("accept()");

}

else

{

printf("[TCP서버]클라이언트 접속: IP 주소=%s, 포트번호=%d\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));

// 소켓 정보 추가

if( addSocketInfo(client_sock) == FALSE )

{

printf("[TCP서버] 클라이언트 접속을 해제합니다.\n");

closesocket(client_sock);

}

}

}

   

// 소켓 셋 검사(2) : 데이터 통신

for( int i = 0 ; i < nTotalSockets ; i++ )

{

SOCKETINFO* ptr = socketInfoArray[i];

if( FD_ISSET(ptr->sock, &rset) )

{

// 데이터 받기

retval = recv(ptr->sock, ptr->buf, BUFSIZE, 0);

if( retval == SOCKET_ERROR )

{

if( WSAGetLastError() != WSAEWOULDBLOCK )

{

err_display("recv()");

removeSocketInfo(i);

}

continue;

}

else if( retval == 0 )

{

removeSocketInfo(i);

continue;

}

   

ptr->recvbytes = retval;

// 받은 데이터 출력

addrLen = sizeof(clientAddr);

getpeername(ptr->sock, (SOCKADDR*)&clientAddr, &addrLen);

ptr->buf[retval] = '\0';

printf("[TCP%s:%d] %s\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port), ptr->buf);

}

if( FD_ISSET(ptr->sock, &wset) )

{

// 데이터 보내기

retval = send(ptr->sock,

ptr->buf + ptr->sendbytes,

ptr->recvbytes - ptr->sendbytes, 0);

if( retval == SOCKET_ERROR )

{

if(WSAGetLastError() != WSAEWOULDBLOCK)

{

err_display("send()");

removeSocketInfo(i);

}

continue;

}

ptr->sendbytes += retval;

if( ptr->recvbytes == ptr->sendbytes )

{

ptr->recvbytes = ptr->sendbytes = 0;

}

}

}

}

return 0;

}

   

// 소켓 정보 추가

BOOL addSocketInfo(SOCKET sock)

{

if( nTotalSockets >= (FD_SETSIZE-1) )

{

printf("[오류] 소켓 정보를 추가할 수 없습니다.\n");

return FALSE;

}

SOCKETINFO* ptr = new SOCKETINFO;

if( ptr == NULL )

{

printf("[오류] 메모리 부족\n");

return FALSE;

}

   

ptr->sock = sock;

ptr->recvbytes = 0;

ptr->sendbytes = 0;

socketInfoArray[nTotalSockets++] = ptr;

   

return TRUE;

}

// 소켓 정보 삭제

void removeSocketInfo(int nIndex)

{

SOCKETINFO* ptr = socketInfoArray[nIndex];

   

// 클라이언트 정보 얻기

SOCKADDR_IN clientAddr;

int addrLen = sizeof(clientAddr);

getpeername(ptr->sock, (SOCKADDR*)&clientAddr, &addrLen);

printf("[TCP서버] 클라이언트 종료: IP주소=%s, 포트번호=%d\n",

inet_ntoa(clientAddr.sin_addr),

ntohs(clientAddr.sin_port));

   

closesocket(ptr->sock);

delete ptr;

   

for( int i = nIndex ; i < nTotalSockets ; i++ )

{

socketInfoArray[i] = socketInfoArray[i+1];

}

nTotalSockets--;

}

// 오류 출력 함수

void err_quit(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);

LocalFree(lpMsgBuf);

exit(-1);

}

void err_display(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

printf("[%s] %s", msg, (LPCTSTR)lpMsgBuf);

LocalFree(lpMsgBuf);

}

코드 분석

Select 모델은 여러 개의 소켓에 대한 함수 호출 시점(또는 호출 결과)을 알려주는 역할을 할 뿐이며 소켓 정보를 관리해주는 것은 아니다. 따라서 각 소켓에 필요한 정보(애플리케이션 버퍼, 송수신 바이트 정보 등)를 관리하는 기능은 애플리케이션이 구현해야 한다.

아래 그림은 이 예제에서 소켓을 관리하기 위해 사용하는 구조다. SOCKETINFO 타입 포인터 배열(원소 개수는 FD_SETSIZE)을 선언하고, 새로운 소켓이 생성될 때마다 동적으로 SOCKETINFO 구조체 크기의 메모리를 할당한다. 소켓 정보를 제거할 때는 포인터 배열 중간에 빈 곳이 없도록 삭제 위치 아래쪽의 데이터를 끌어올린다.

  • WSAAsyncSelect 모델

    WSAAsyncSelect 모델은 WSAAsyncSelect() 함수가 핵심적인 역할을 한다는 뜻에서 붙인 이름이다. WSAAsyncSelect모델을 사용하면 윈도우 메시지 형태로 소켓과 관련된 네트워크 이벤트를 처리할 수 있다. 모든 소켓과 관련된 메시지는 하나의 윈도우, 즉 하나의 윈도우 프로시저로 전달되므로 멀티스레드를 사용하지 않고도 여러 소켓을 처리할 수 있다.

    동작 원리

    WSAAsyncSelect 모델을 이용한 소켓 입출력 절차는 다음과 같다

    • WSAAsyncSelect() 함수를 이용하여 소켓을 위한 윈도우 메시지와 처리할 네트워크 이벤트를 등록한다. 예를 들면, 소켓을 통해 데이터를 보내거나 받을 수 있는 상황이 되면 특정 윈도우 메시지로 알려달라는 내용을 등록한다.
    • 등록한 네트워크 이벤트가 발생하면 윈도우 메시지가 발생하고 윈도우 프로시저가 호출된다.
    • 윈도우 프로시저에서는 받은 메시지의 종류에 따라 적절한 소켓 함수를 호출하여 처리한다.

    WSAAsyncSelect() 함수 원형은 다음과 같다

int WSAAsyncSelect(

SOCKET s,

HWND hWnd,

u_int wMsg,

long lEvent

);

// 성공 : 0

// 실패 : SOCKET_ERROR

  • s

    처리하고자하는 소켓

  • hWnd

    메시지를 받을 윈도우를 나타내는 핸들값

  • wMsg

    윈도우가 받을 메시지다. 소켓을 위한 메시지는 따로 정의되어 있지 않으므로 사용자 정의 메시지를 이용한다.

  • lEvent

    처리할 네트워크 이벤트 종류를 비트 마스크 조합으로 나타낸다.

  • 네트워크 이벤트 상수값

네트워크 이벤트

의미

FD_ACCEPT

클라이언트가 접속하면 윈도우 메시지를 발생시킨다.

FD_READ

데이터 수신이 가능하면 윈도우 메시지를 발생시킨다

FD_WRITE

데이터 송신이 가능하면 윈도우 메시지를 발생시킨다.

FD_CLOSE

상대가 접속을 종료하면 윈도우 메시지를 발생시킨다.

FD_CONNECT

접속이 완료되면 윈도우 메시지를 발생시킨다

FD_OOB

OOB 데이터가 도착하면 윈도우 메시지를 발생시킨다.

다음 코드는 소켓 s에 대해 FD_READ와 FD_WRITE 이벤트를 등록하는 예를 보여준다.

#define WM_SOCKET (WM_USER+1) // 사용자 정의 윈도우 메시지

WSAAsyncSelect(s, hWnd, WM_SOCKET, FD_READ | FD_WRITE);

WSAAsyncSelect() 함수 사용 시 유의할 점은 다음과 같다

  • WSAAsyncSelect() 함수를 호출하면 해당 소켓은 자동으로 넌블로킹 모드로 전환된다. 블로킹 소켓은 윈도우 메시지 루프를 정지시킬 가능성이 있기 때문에 WSAAsyncSelect 모델에서는 넌블로킹 소켓만 사용하도록 되어 있다.
  • socket() 함수가 리턴하는 소켓은 연결 대기 소켓(listening socket)과 동일한 속성을 지니게 된다. 연결 대기 소켓은 직접 데이터 송수신을 하지 않으므로 FD_READ, FD_WRITE 이벤트를 처리하지 않는다. 반면 accept() 함수가 리턴하는 소켓은 FD_READ, FD_WRITE 이벤트를 처리해야 하므로 다시 WSAAsyncSelect() 함수를 호출해서 속성을 변경해야 한다.
  • 윈도우 메시지에 대응하여 소켓 함수를 호출하면 대부분 성공하지만, WSAEWOULDBLOCK 오류 코드가 발생하는 경우도 있으므로 이를 체크해야 한다.
  • 윈도우 메시지를 받았을 때 적절한 소켓 함수를 호출하지 않으면, 다음 번에 같은 윈도우 메시지가 발생하지 않는다. 예를 들어 FD_READ 이벤트에 대응하여 recv() 함수를 호출하지 않으면, 동일한 소켓에 대한 FD_READ 이벤트는 더 발생하지 않는다. 따라서 윈도우 메시지가 발생하면 대응 함수를 호출해야 하며, 그렇지 않을 경우 애플리케이션이 직접 메시지를 발생시켜야 한다.
  • 네트워크 이벤트 대응 함수

네트워크 이벤트

대응 함수

FD_ACCEPT

accept()

FD_READ

recv(), recvfrom()

FD_WRITE

send(), sendto()

FD_CLOSE

없음

FD_CONNET

없음

FD_OOB

recv(), recvfrom()

이제 네트워크 이벤트 발생시 윈도우 프로시저를 통해 전달되는 내용을 살펴보자. 윈도우 프로시저는 다음과 같이 총 네 개의 인자를 통해 데이터를 받는다.

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
...

  • hwnd

    메시지가 발생한 윈도우를 나타내는 핸들값

  • msg

    WSAAsyncSelect() 함수 호출 시 등록한 사용자 정의 메시지

  • wParam

    네트워크 이벤트가 발생한 소켓이다. 따라서 이 값을 SOCKET 타입으로 캐스팅하여 소켓 함수 호출에 그대로 사용하면 된다.

  • lParam

    하위 16비트는 발생한 네트워크 이벤트를 나타내며, 상위 16비트는 오류 코드를 담고 있다. 항상 오류 코드를 먼저 확인한 후 네트워크 이벤트를 처리해야 한다. 이식성을 위해 다음과 같이 정의된 매크로를 사용하면 된다.

#define WSAGETSELECTERROR(lParam) HIWORD(lParam)
#define WSAGETSELECTEVENT(lParam) LOWORD(lParam)

[실습] WSAAsyncSelect 모델을 이용한 TCP 서버

  • 예제 AsyncSelectSever.cpp

#include <winsock2.h>

#include <stdlib.h>

#include <stdio.h>

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

   

#define BUFSIZE 512

#define WM_SOCKET (WM_USER+1)

   

// 소켓 정보 저장을 위한 구조체

struct SOCKETINFO

{

SOCKET sock;

char buf[BUFSIZE+1];

int recvbytes;

int sendbytes;

BOOL recvdelayed;

SOCKETINFO* next;

};

   

SOCKETINFO* socketInfoList;

   

// 윈도우 메시지 처리함수

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

void ProcessSocketMessage(HWND, UINT, WPARAM, LPARAM);

   

// 소켓 관리 함수

BOOL AddSocketInfo(SOCKET sock);

SOCKETINFO* GetSocketInfo(SOCKET sock);

void RemoveSocketInfo(SOCKET sock);

   

// 오류 출력함수

void err_quit(char* msg);

void err_display(char* msg);

void err_display(int errcode);

   

int main(int argc, char* argv[])

{

int retval;

   

// 윈도우 클래스 등록

WNDCLASS wndclass;

wndclass.cbClsExtra = 0;

wndclass.cbWndExtra = 0;

wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);

wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);

wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);

wndclass.hInstance = NULL;

wndclass.lpfnWndProc = (WNDPROC)WndProc;

wndclass.lpszClassName = "MyWindowClass";

wndclass.lpszMenuName = NULL;

wndclass.style = CS_HREDRAW | CS_VREDRAW;

if( !RegisterClass(&wndclass) )

return -1;

   

// 윈도우 생성

HWND hWnd = CreateWindow("MyWindowClass", "TCP서버",

WS_OVERLAPPEDWINDOW,

0, 0, 600, 300,

NULL, (HMENU)NULL, NULL, NULL);

if( hWnd == NULL )

return -1;

ShowWindow(hWnd, SW_SHOWNORMAL);

UpdateWindow(hWnd);

   

// 윈속 초기화

WSADATA wsa;

if( WSAStartup(MAKEWORD(2,2), &wsa) != 0 )

return -1;

   

// socket()

SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0);

if( listen_sock == INVALID_SOCKET )

err_quit("socket()");

   

// WSAAsyncSelect()

retval = WSAAsyncSelect(listen_sock, hWnd, WM_SOCKET, FD_ACCEPT | FD_CLOSE );

if( retval == SOCKET_ERROR )

err_quit("WSAAsyncSelect()");

   

// bind()

SOCKADDR_IN serverAddr;

ZeroMemory(&serverAddr, sizeof(serverAddr));

serverAddr.sin_family = AF_INET;

serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);

serverAddr.sin_port = htons(9000);

retval = bind(listen_sock, (SOCKADDR*)&serverAddr, sizeof(serverAddr));

if( retval == SOCKET_ERROR )

err_quit("bind()");

   

// listen()

retval = listen(listen_sock, SOMAXCONN);

if( retval == SOCKET_ERROR )

err_quit("listen()");

   

// 메시지 루프

MSG msg;

while(GetMessage(&msg, 0, 0, 0) > 0 )

{

TranslateMessage(&msg);

DispatchMessage(&msg);

}

   

return msg.wParam;

}

   

// 윈도우 메시지 처리함수

LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

{

switch(uMsg)

{

case WM_SOCKET: // 소켓 관련 윈도우 메시지

ProcessSocketMessage(hWnd, uMsg, wParam, lParam);

return 0;

case WM_DESTROY:

PostQuitMessage(0);

return 0;

}

   

return DefWindowProc(hWnd, uMsg, wParam, lParam);

}

void ProcessSocketMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

{

// 데이터 통신에 사용할 변수

SOCKETINFO* ptr;

SOCKET client_sock;

SOCKADDR_IN clientAddr;

int addrLen;

int retval;

   

// 오류 발생 여부 확인

if( WSAGETSELECTERROR(lParam) )

{

err_display(WSAGETSELECTERROR(lParam));

RemoveSocketInfo(wParam);

return ;

}

   

// 메시지 처리

switch( WSAGETSELECTEVENT(lParam) )

{

case FD_ACCEPT:

addrLen = sizeof(clientAddr);

client_sock = accept(wParam, (SOCKADDR*)&clientAddr, &addrLen);

if( client_sock == INVALID_SOCKET )

{

if( WSAGetLastError() != WSAEWOULDBLOCK )

err_display("accept()");

return;

}

printf("[TCP서버]클라이언트 접속: IP주소=%s, 포트번호=%d\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));

AddSocketInfo(client_sock);

retval = WSAAsyncSelect(client_sock, hWnd, WM_SOCKET, FD_READ | FD_WRITE | FD_CLOSE );

if( retval == SOCKET_ERROR )

{

err_display("WSAAsyncSelect()");

RemoveSocketInfo(client_sock);

}

break;

case FD_READ:

ptr = GetSocketInfo(wParam);

if( ptr->recvbytes > 0 )

{

ptr->recvdelayed = TRUE;

return ;

}

   

// 데이터 받기

retval = recv(ptr->sock, ptr->buf, BUFSIZE, 0);

if( retval == SOCKET_ERROR )

{

if( WSAGetLastError() != WSAEWOULDBLOCK)

{

err_display("recv()");

RemoveSocketInfo(wParam);

}

return ;

}

ptr->recvbytes = retval;

// 받은 데이터 출력

ptr->buf[retval] = '\0';

addrLen = sizeof(clientAddr);

getpeername(wParam, (SOCKADDR*)&clientAddr, &addrLen);

printf("[TCP/%s:%d] %s\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port), ptr->buf);

case FD_WRITE:

ptr = GetSocketInfo(wParam);

if( ptr->recvbytes <= ptr->sendbytes )

return ;

   

// 데이터 보내기

retval = send(ptr->sock, ptr->buf + ptr->sendbytes,

ptr->recvbytes - ptr->sendbytes, 0);

if( retval == SOCKET_ERROR )

{

if( WSAGetLastError() != WSAEWOULDBLOCK )

{

err_display("send()");

RemoveSocketInfo(wParam);

}

return;

}

ptr->sendbytes += retval;

// 받은 데이터를 모두 보냈는지 체크

if( ptr->recvbytes == ptr->sendbytes )

{

ptr->recvbytes = ptr->sendbytes = 0 ;

if( ptr->recvdelayed )

{

ptr->recvdelayed = FALSE;

PostMessage(hWnd, WM_SOCKET, wParam, FD_READ);

}

}

break;

case FD_CLOSE:

RemoveSocketInfo(wParam);

break;

}

}

   

// 소켓 관리 함수

BOOL AddSocketInfo(SOCKET sock)

{

SOCKETINFO* ptr = new SOCKETINFO;

if( ptr == NULL )

{

printf("[오류] 메모리가 부족합니다.\n");

return FALSE;

}

   

ptr->sock = sock;

ptr->recvbytes = 0;

ptr->sendbytes = 0;

ptr->recvdelayed = FALSE;

ptr->next = socketInfoList;

socketInfoList = ptr;

   

return TRUE;

}

   

// 소켓 정보 얻기

SOCKETINFO* GetSocketInfo(SOCKET sock)

{

SOCKETINFO* ptr = socketInfoList;

   

while(ptr)

{

if(ptr->sock == sock )

return ptr;

ptr = ptr->next;

}

   

return NULL;

}

   

// 소켓 정보 제거

void RemoveSocketInfo(SOCKET sock)

{

// 클라이언트 정보 얻기

SOCKADDR_IN clientAddr;

int addrLen = sizeof(clientAddr);

getpeername(sock, (SOCKADDR*)&clientAddr, &addrLen);

printf("[TCP서버] 클라이언트 종료: IP주소=%s, 포트번호=%d\n",

inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));

   

SOCKETINFO* curr = socketInfoList;

SOCKETINFO* prev = NULL;

   

while( curr )

{

if( curr->sock == sock )

{

if( prev )

prev->next = curr->next;

else

socketInfoList = curr->next;

closesocket(curr->sock);

delete curr;

return ;

}

prev = curr;

curr = curr->next;

}

}

   

// 소켓 함수 오류 출력 후 종료

void err_quit(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);

LocalFree(lpMsgBuf);

exit(-1);

}

   

// 소켓 함수 오류 출력

void err_display(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

printf("[%s] %s", msg, (LPCTSTR)lpMsgBuf);

LocalFree(lpMsgBuf);

}

void err_display(int errcode)

{

PVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,

NULL, errcode,

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

printf("[오류] %s", (LPCTSTR)lpMsgBuf);

LocalFree(lpMsgBuf);

}

코드 분석

WSAAsyncSelect 모델을 사용할 때도 Select 모델과 마찬가지로 소켓 정보를 애플리케이션이 관리해야 한다. Select모델에서는 최대 FD_SETSIZE개의 소켓을 처리할 수 있으므로 배열을 사용했지만 WSAAsyncSelect 모델에는 이러한 제약이 없으므로 연결 리스트를 사용하도록 하였다.

아래 그림은 이 예제에서 소켓을 관리하기 위해 사용하는 구조다. socketInfoList 변수는 SOCKETINFO 타입 포인터로, 연결 리스트의 시작점이다.

새로운 소켓이 생성될 때마다 동적으로 SOCKETINFO 크기의 메모리를 할당하고, 연결 리스트 제일 앞쪽에 삽입한다.

소켓 정보를 제거할 때는 해당 데이터 앞뒤의 포인터를 조작하여 연결리스트가 계속 유지되도록 한다.

  • WSAEventSelect 모델

    WSAEventSelect 모델은 WSAEventSelect() 함수가 핵심적인 역할을 한다는 뜻에서 붙인 이름이다. WSAEventSelect 모델에서는 이벤트 객체를 통해 네트워크 이벤트를 감지한다. 각 소켓에 대해 이벤트 객체를 생성하고, 이 이벤트 객체를 관찰함으로써 멀티스레드를 사용하지 않고도 여러 개의 소켓을 처리할 수 있다.

    동작 원리

    각 소켓마다 이벤트 객체를 하나씩 생성하여 짝지어두면, 네트워크 이벤트가 발생할 때마다 이벤트 객체는 신호 상태가 된다. 따라서 이벤트 객체의 신호 상태를 통해 네트워크 이벤트 발생을 감지할 수 있다. 그러나 이것만으로는 구체적으로 어떤 종류의 이벤트가 발생했는지 혹은 어떤 오류가 발생했는지 알 수 없다는 문제가 있다.

    WSAEventSelect 모델이 제대로 동작하기 위해서는 다음과 같은 기능이 반드시 필요하다.

    • 이벤트 객체 생성과 제거 : WSACreateEvent(), WSACloseEvent()
    • 소켓과 이벤트 객체 짝짓기 : WSAEventSelect()
    • 이벤트 객체의 신호 상태 감지하기 : WSAWaitForMultipleEvents()
    • 구체적인 네트워크 이벤트 알아내기 : WSAEnumNetworkEvents()

    WSAEventSelect 모델을 이용한 소켓 입출력 절차는 다음과 같다.

    • 소켓을 생성할 때마다 WSACreateEvent() 함수를 이용하여 이벤트 객체를 생성
    • WSAEventSelect() 함수를 이용하여 소켓과 이벤트 객체를 짝지음과 동시에, 처리할 네트워크 이벤트를 등록한다. 예를 들면,, 소켓을 통해 데이터를 보내거나 받을 수 있는 상황이 되면 이벤트 객체를 신호 상태로 변경하라는 내용을 등록한다.
    • WSAWaitForMultipleEvents() 함수를 호출하여 이벤트 객체가 신호 상태가 되기를 기다린다. 등록한 네트워크 이벤트가 발생하면 해당 소켓과 연관된 이벤트 객체가 신호 상태가 된다.
    • WSAEnumNetworkEvents() 함수를 호출하여 발생한 네트워크 이벤트를 알아내고, 적절한 소켓 함수를 호출하여 처리한다.

    이벤트 객체 생성과 제거

    WSACreateEvent() 함수는 이벤트 객체를 생성하는 역할을 한다. 이 때 생성되는 이벤트 객체는 항상 수동 리셋(manual-reset) 이벤트며, 비신호 상태로 시작한다. 사용이 끝난 이벤트 객체는 WSACloseEvent() 함수를 호출하여 제거한다.

WSAEVENT WSACreateEvent();
// 성공 : 이벤트 객체 핸들

// 실패 : WSA_INVALID_EVENT

BOOL WSACloseEvent(WSAEVENT hEvent);
// 성공 : TRUE
// 실패 : FALSE

소켓과 이벤트 객체 짝짓기

WSAEventSelect() 함수는 소켓과 이벤트 객체를 짝지음과 동시에, 처리할 네트워크 이벤트를 등록하는 역할을 한다.

int WSAEventSelect( SOCKET s, WSAEVENT hEventObject, long lNetworkEvents);
// 성공 : 0
// 실패 : SOCKET_ERROR

  • s

    처리하고자 하는 소켓

  • hEventObject

    소켓과 연관시킬 이벤트 객체의 핸들값

  • lNetworkEvents

    처리할 네트워크 이벤트 종류를 비트 마스크 조합으로 나타낸다. WSAAsyncSelect 모델에서 사용한 것과 동일하다

네트워크 이벤트

의미

FD_ACCEPT

클라이언트가 접속하면 이벤트 객체가 신호 상태로 된다.

FD_READ

데이터 수신이 가능하면 이벤트 객체가 신호 상태가 된다.

FD_WRITE

데이터 송신이 가능하면 이벤트 객체가 신호 상태로 된다.

FD_CLOSE

상대가 접속을 종료하면 이벤트 객체가 신호 상태로 된다.

FD_CONNECT

접속이 완료되면 이벤트 객체가 신호 상태로 된다.

FD_OOB

OOB 데이터가 도착하면 이벤트 객체가 신호 상태로 된다.

사용 예

WSAEVENT hEvent = WSACreateEvent();

WSAEventSelect(s, hEvent, FD_READ | FD_WRITE);

   

WSAEventSelect() 함수 사용 시 유의할 점은 다음과 같다

  • WSAEventSelect() 함수를 호출하면 해당 소켓은 자동으로 넌블로킹 모드로 전환된다.
  • socket() 함수가 리턴하는 소켓은 연결 대기 소켓(listening socket)과 동일한 속성을 지니게 된다. 연결 대기 소켓은 직접 데이터 송수신을 하지 않으므로 FD_READ, FD_WRITE 이벤트를 처리하지 않는다. 반면 accept() 함수가 리턴하는 소켓은 FD_READ_FD_WRITE 이벤트를 처리해야 하므로, 다시 WSAAsyncSelect() 함수를 호출해서 속성을 변경해야 한다.
  • 네트워크 이벤트에 대응하여 소켓 함수를 호출하면 대부분 성공하지만, WSAEWOULDBLOCK 오류 코드가 발생하는 경우도 있으므로 이를 체크 해야 한다.
  • 네트워크 이벤트 발생 시 적절한 소켓 함수를 호출하지 않으면, 다음 번에 같은 네트워크 이벤트가 발생하지 않는다. 따라서 네트워크 이벤트가 발생하면 대응 함수를 호출해야 하며, 그렇지 않을 경우 애플리케이션이 네트워크 이벤트 발생 사실을 기록해두고 나중에 대응 함수를 호출해야 한다.
    • 네트워크 이벤트 대응 함수

네트워크 이벤트

대응 함수

FD_ACCEPT

accept()

FD_READ

recv(), recvfrom()

FD_WRITE

send(), sendto()

FD_CLOSE

없음

FD_CONNECT

없음

FD_OOB

recv(), recvfrom()

이벤트 객체의 신호 상태 감지하기

WSAWaitForMultipleEvents() 함수는 여러 이벤트 객체를 동시에 관찰할 수 있는 기능을 제공한다.

DWORD WSAWaitForMultipleEvents(

DWORD cEvents,

const WSAEVENT* lphEvents,

BOOL fWaitAll,

DWORD dwTimeout,

BOOL fAlertable

);

// 성공 : WSA_WAIT_EVENT_0 ~ WSA_WAIT_EVENT_0 + cEvent - 1또는 WSA_WAIT_TIMEOUT

// 실패 : WSA_WAIT_FAILED

  • cEvents, lphEvents

    WSAWaitForMultipleEvents() 함수를 사용할 때는 이벤트 객체 핸들을 모두 배열에 저장해야 한다. cEvents는 배열 원소 개수, lphEvents는 배열의 시작 주소를 나타낸다. cEvents 최대값은 WSA_MAXIMUM_WAIT_EVENTS(64)이다.

  • fWaitAll

    TRUE면 모든 이벤트 객체가 신호 상태가 될 때까지 대기한 후 리턴한다. FALSE이면 한 이벤트 객체가 신호 상태가 되는 즉시 리턴한다.

  • dwTimeout

    대기 시간으로 밀리초 단위를 사용한다. 이 시간 내에 조건이 만족되지 않더라도 WSAWaitForMultipleEvents() 함수는 리턴한다. WSA_INFINITE값을 사용하면 조건이 만족할 때까지 대기한다.

  • fAlertable

    입출력 완료 루틴(I/O completion routine)과 관련된 부분이다. WSAEventSelect 모델에서는 FALSE를 사용한다.

구체적인 네트워크 알아내기

WSAEnumNetworkEvents() 함수는 소켓과 관련하여 발생한 구체적인 네트워크 이벤트를 알려주는 역할을 한다.

int WSAEnumNetworkEvents(

SOCKET s,

WSAEVENT hEventObject,

LPWSANETWORKEVENTS lpNetworkEvents

);

// 성공 : 0

// 실패 : SOCKET_ERROR

  • s

    대상 소켓을 나타낸다.

  • hEventObject

    대상 소켓s와 짝지어둔 이벤트 객체 핸들을 넘겨주면 자동으로 이벤트 객체가 비신호 상태로 된다. 이 인자는 선택 사항이므로 사용하지 않으려면 NULL을 넘겨준다

    lpNetworkEvents

    WSANETWORKEVENTS 구조체 변수 주소값을 넘겨주면, 발생한 네트워크 이벤트와 오류 정보가 이 변수에 저장된다. WSANETWORKEVENTS 구조체 정의는 다음과 같다

typedef struct _WSANETWORKEVENTS{

long lNetworkEvents;

int iErrorCode[FD_MAX_EVENTS];

} WSANETWORKEVENTS, *LPWSANETWORKEVENTS;

lNetworkEvents 인자에는 상수값이 조합된 형태로 저장되어, 발생한 네트워크 이벤트를 알려준다. iErrorCode[]에는 네트워크 이벤트와 연관된 오류 정보가 저장된다. 오류 정보를 참조하기 위해서는 배열의 인덱스 값을 사용해야 한다.

  • iErrorCode[] 를 참조하기 위한 배열 인덱스 값

네트워크 이벤트

배열 인덱스

FD_ACCEPT

FD_ACCEPT_BIT

FD_READ

FD_READ_BIT

FD_WRITE

FD_WRITE_BIT

FD_CLOSE

FD_CLOSE_BIT

FD_CONNECT

FD_CONNECT_BIT

FD_OOB

FD_OOB_BIT

[실습] WSAEventSelect 모델을 이용한 TCP 서버

  • 예제 EventSelectServer.cpp

#include <winsock2.h>

#include <stdlib.h>

#include <stdio.h>

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

   

#define BUFSIZE 512

   

// 소켓 정보 저장을 위한 구조체

struct SOCKETINFO

{

SOCKET sock;

char buf[BUFSIZE+1];

int recvbytes;

int sendbytes;

};

   

int nTotalSockets = 0;

SOCKETINFO* socketInfoArray[WSA_MAXIMUM_WAIT_EVENTS];

WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS];

   

// 소켓 관리 함수

BOOL addSocketInfo(SOCKET sock);

void removeSocketInfo(int nIndex);

// 오류 출력 함수

void err_quit(char* msg);

void err_display(char* msg);

void err_display(int errcode);

   

int main(int argc, char* argv[])

{

int retval;

   

// 윈속 초기화

WSADATA wsa;

if( WSAStartup(MAKEWORD(2,2), &wsa) != 0 )

return -1;

   

// socket()

SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0);

if( listen_sock == INVALID_SOCKET)

err_quit("socket()");

   

// 소켓 정보 추가

if( addSocketInfo(listen_sock) == FALSE )

return -1;

   

// WSAEventSelect()

retval = WSAEventSelect(listen_sock, eventArray[nTotalSockets-1], FD_ACCEPT | FD_CLOSE );

if( retval == SOCKET_ERROR )

err_quit("WSAEventSelect()");

   

// bind()

SOCKADDR_IN serverAddr;

ZeroMemory(&serverAddr, sizeof(serverAddr));

serverAddr.sin_family = AF_INET;

serverAddr.sin_port = htons(9000);

serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);

retval = bind(listen_sock, (SOCKADDR*)&serverAddr, sizeof(serverAddr));

if( retval == SOCKET_ERROR )

err_quit("bind()");

   

// listen

retval = listen(listen_sock, SOMAXCONN);

if( retval == SOCKET_ERROR )

err_quit("listen()");

   

// 데이터 통신에 사용할 변수

int index;

WSANETWORKEVENTS networkEvents;

SOCKET client_sock;

SOCKADDR_IN clientAddr;

int addrLen;

   

while(1)

{

// 이벤트 객체 관찰

index = WSAWaitForMultipleEvents(nTotalSockets, eventArray, FALSE, WSA_INFINITE, FALSE);

if( index == WSA_WAIT_FAILED )

{

err_display("WSAWaitForMultipleEvents()");

continue;

}

index -= WSA_WAIT_EVENT_0;

   

// 구체적인 네트워크 이벤트 알아내기

retval = WSAEnumNetworkEvents(socketInfoArray[index]->sock, eventArray[index], &networkEvents);

if( retval == SOCKET_ERROR )

{

err_display("WSAEnumNetworkEvents()");

continue;

}

   

// FD_ACCEPT 이벤트 처리

if(networkEvents.lNetworkEvents & FD_ACCEPT )

{

if( networkEvents.iErrorCode[FD_ACCEPT_BIT] != 0 )

{

err_display(networkEvents.iErrorCode[FD_ACCEPT_BIT]);

continue;

}

   

addrLen = sizeof(clientAddr);

client_sock = accept(socketInfoArray[index]->sock, (SOCKADDR*)&clientAddr, &addrLen);

   

if( client_sock == INVALID_SOCKET )

{

err_display("accept()");

continue;

}

   

printf("[TCP서버]클라이언트 접속:IP주소=%s, 포트번호=%d\n",

inet_ntoa(clientAddr.sin_addr),

ntohs(clientAddr.sin_port));

   

if( nTotalSockets >= WSA_MAXIMUM_WAIT_EVENTS)

{

printf("[오류] 더이상 접속을 받을 수 없습니다.\n");

closesocket(client_sock);

continue;

}

   

if( addSocketInfo(client_sock) == FALSE )

continue;

   

retval = WSAEventSelect(client_sock, eventArray[nTotalSockets-1], FD_READ | FD_WRITE | FD_CLOSE );

if( retval == SOCKET_ERROR )

err_quit("WSAEventSelect()");

   

// FD_READ, FD_WRITE 이벤트 처리

if(networkEvents.lNetworkEvents & FD_READ || networkEvents.lNetworkEvents & FD_WRITE )

{

if( networkEvents.lNetworkEvents & FD_READ && networkEvents.iErrorCode[FD_READ_BIT] != 0 )

{

err_display(networkEvents.iErrorCode[FD_READ_BIT]);

continue;

}

if( networkEvents.lNetworkEvents & FD_WRITE && networkEvents.iErrorCode[FD_WRITE_BIT] != 0 )

{

err_display(networkEvents.iErrorCode[FD_WRITE_BIT]);

continue;

}

   

SOCKETINFO* ptr = socketInfoArray[index];

   

if( ptr->recvbytes == 0 )

{

// 데이터 받기

retval = recv(ptr->sock, ptr->buf, BUFSIZE, 0);

if( retval == SOCKET_ERROR )

{

if(WSAGetLastError() != WSAEWOULDBLOCK )

{

err_display("recv()");

removeSocketInfo(index);

}

continue;

}

ptr->recvbytes = retval;

// 받은 데이터 출력

addrLen = sizeof(clientAddr);

getpeername(ptr->sock, (SOCKADDR*)&clientAddr, &addrLen);

printf("[TCP/%s:%d] %s\n",

inet_ntoa(clientAddr.sin_addr),

ntohs(clientAddr.sin_port), ptr->buf );

}

if(ptr->recvbytes > ptr->sendbytes )

{

// 데이터 보내기

retval = send(ptr->sock, ptr->buf + ptr->sendbytes, ptr->recvbytes - ptr->sendbytes, 0);

if( retval == SOCKET_ERROR )

{

if( WSAGetLastError() != WSAEWOULDBLOCK )

{

err_display("send()");

removeSocketInfo(index);

}

continue;

}

ptr->sendbytes += retval;

// 받은 데이터를 모두 보냈는지 체크

if(ptr->recvbytes == ptr->sendbytes )

ptr->recvbytes = ptr->sendbytes = 0;

}

}

// FD_CLOSE 이벤트 처리

if(networkEvents.lNetworkEvents & FD_CLOSE )

{

if( networkEvents.iErrorCode[FD_CLOSE_BIT] != 0 )

err_display(networkEvents.iErrorCode[FD_CLOSE_BIT]);

removeSocketInfo(index);

}

}

}

   

// 윈속 종료

WSACleanup();

return 0;

}

   

// 소켓 관리 함수

BOOL addSocketInfo(SOCKET sock)

{

if( nTotalSockets >= WSA_MAXIMUM_WAIT_EVENTS )

{

printf("[오류] 소켓 정보를 추가할 수 없습니다.\n");

return FALSE;

}

   

SOCKETINFO* ptr = new SOCKETINFO;

if( ptr == NULL )

{

printf("[오류] 메모리가 부족합니다.\n");

return FALSE;

}

   

WSAEVENT hEvent = WSACreateEvent();

if( hEvent == WSA_INVALID_EVENT )

{

err_display("WSACreateEvent()");

return FALSE;

}

   

ptr->sock = sock;

ptr->recvbytes = 0;

ptr->sendbytes = 0;

socketInfoArray[nTotalSockets] = ptr;

eventArray[nTotalSockets] = hEvent;

nTotalSockets++;

   

return TRUE;

}

   

// 소켓 정보 삭제

void removeSocketInfo(int nIndex)

{

SOCKETINFO* ptr = socketInfoArray[nIndex];

   

// 클라이언트 정보 얻기

SOCKADDR_IN clientAddr;

   

int addrLen = sizeof(clientAddr);

getpeername(ptr->sock, (SOCKADDR*)&clientAddr, &addrLen);

printf("[TCP서버] 클라이언트 종료:IP주소=%s, 포트번호=%d\n",

inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));

   

closesocket(ptr->sock);

delete ptr;

WSACloseEvent(eventArray[nIndex]);

   

for( int i = nIndex ; i < nTotalSockets ; i++ )

{

socketInfoArray[i] = socketInfoArray[i+1];

eventArray[i] = eventArray[i+1];

}

   

nTotalSockets--;

}

// 오류 출력 함수

void err_quit(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

   

MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);

LocalFree(lpMsgBuf);

exit(-1);

}

void err_display(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

   

printf("[%s] %s", msg, (LPCTSTR)lpMsgBuf);

LocalFree(lpMsgBuf);

}

void err_display(int errcode)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,

NULL, errcode,

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

   

printf("[오류] %s", (LPCTSTR)lpMsgBuf);

LocalFree(lpMsgBuf);

}

코드 분석

WSAEventSelect 모델을 사용할 때도 다른 모델(Select, WSAAsyncSelect)과 마찬가지로 소켓 정보를 애플리케이션이 관리해야 한다. WSAEventSelect모델은 동시에 처리 할 수 있는 소켓의 개수가 WSA_MAXIMUM_WAIT_EVENTS(64)로 한정되어 있다는 점에서 Select 모델과 동일하다.

이 예제에서 소켓을 관리하기 위해 사용하는 구조다. SOCKETINFO 타입 포인터 배열을 선언하고 새로운 소켓이 생성될 때마다 동적으로 SOCKETINFO 구조체 크기의 메모리를 할당한다. 소켓 정보를 제거할 땐느 포인터 배열 중간에 빈 곳이 없도록 삭제 위치 아래쪽의 데이터를 끌어올린다.

   

   

반응형

'책정리 > 윈도우 네트워크 프로그래밍' 카테고리의 다른 글

10장 소켓 입출력 모델(II)  (0) 2015.07.17
7장 소켓 옵션  (0) 2015.04.23
6장 UDP 서버/클라이언트  (0) 2015.04.20
5장 멀티스레드  (0) 2015.04.20
4장 TCP 서버/클라이언트 구조  (0) 2015.04.13