IT_Programming/Network Programming

[펌] 소켓 입출력 모델 - WSAAsyncSelect 모델

JJun ™ 2009. 11. 18. 12:52

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

출처: http://blog.naver.com/ree31206/46430161

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

 

소켓 입출력 모델 - WSAAsyncSelect 모델

 

WSAAsyncSelect() 함수가 핵심적인 역활을 한다. 윈도우 메시지 형태로 소켓과 관련된 네트워크

이벤트를 처리 할 수 있다. 모든 소켓과 관련된 메시지는 하나의 윈도우 프로시저로 전달되므로

멀티스레드를 사용하지 않고도 여러 소켓을 처리할 수 있다.

 

 

[WSAAsyncSelect 모델을 이용한 소켓 입출력 절차]

 

1. WSAAsyncSelect() 함수를 이용하여 소켓을 위한 윈도우 메시지와 처리할 네트워크 이벤트를

    등록한다. 예를 들면 소켓을 통해 데이터를 보내거나 받을수 있는 상황이 되면 특정 윈도우 메시지

    로 알려달라는 내용을 등록한다.

 

2. 등록한 네트워크 이벤트가 발생하면 윈도우 메시지가 발생하고 윈도우 프로시저가 호출된다.

3. 윈도우 프로시저에서는 받은 메시지 종류에 따라 적절한 소켓 함수를 호출하여 처리한다.

 

 

 

* WSAAsyncSelect() 함수 

int WSAAsyncSelect(

    SOCKET s,                    // 처리하고자 하는 소켓

    HWND hWnd,                 // 메시지를 받을 윈도우를 나타내는 핸들

    unsigned int wMsg,       // 윈도우가 받을 메시지. 소켓을 위한 메시지는 따로 정의되어

                                         // 있지 않으므로 사용자 정의 메시지를 이용

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

);            성공 : 0, 실패 : SOCKET_ERROR

 

wMsg  를 사용하기 위해선 사용자 정의 윈도우 메시지를 등록해야 한다.

 

#define WM_SOCKET (WM_USER+1)     WM_SOCKET 메시지를 정의한다.

WM_USER 란것은 우리가 지정해줄수 있는 숫자라고 생각하면 된다. 

다른 메시지를 추가한다면 +2, +3 해서 추가하면 된다.

 

// 소켓 s 에 대해 FD_READ | FD_WRITE 이벤트를 등록하는 예

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

 

* 네트워크 이벤트 상수값

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

FD_READ     : 데이터 수신이 가능하면 윈도우 메시지를 발생시킨다.  recv(), recvfrom()

FD_WRITE     : 데이터 송신이 가능하면 윈도우 메시지를 발생시킨다. send(), sendto()

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

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

FD_OOB       : OOB 데이터가 도착하면 윈도우 메시지를 발생시킨다.  recv(), recvfrom()

 

 

- WSAAsyncSelect() 함수를 사용하면 해당 소켓은 자동으로 넌블로킹 모드로 전환된다.

 

- accept() 함수가 리턴하는 소켓은 연결 대기 소켓과 동일한 속성을 지니게 된다

 

- 윈도우 메시지에 대응하여 소켓함수를 호출하면 대부분은 성공하지만

   WSAEWOULDBLOCK 오류코드가 발생하는 경우도 있으므로 이를 체크해야 한다.

 

- 윈도우 메시지를 받았을때 적절한 소켓함수를 호출하지 않으면 다음 번에는 같은 윈도우 메세지가

   발생하지 않는다. 예를 들어 FD_READ 이벤트에 대응하여 recv() 함수를 호출하지 않으면 동일한

   소켓에 대한 FD_READ 이벤트는 더 발생하지 않는다. 그러므로 윈도우 메시지가 발생하면 앞의

   표에 있는 대응함수를 호출해야 하며 그렇지 않을경우 애플리케이션이 직접 메시지를 발생시켜야

   한다.

 

 

 

* 윈도우 프로시저 lParam

이식성을 위해 다음과 같이 정의된 매크로를 사용

#define WSAGETSELECTERROR(lParam)     HIWORD(lParam)

#define WSAGETSELECTEVENT(lParam)      LOWORD(lParam)

 

 

[ WSAAsyncSelect() 함수를 이용한 에코 서버 ]

Win32 Application 에서 그냥 하면 되지만 Win32 Console Application 으로 생성해

윈도우창 만드는 것도 구현..

 

#include <winsock2.h>
#include <stdlib.h>
#include <stdio.h>

 

#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 listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if(listen_sock == INVALID_SOCKET) err_quit("socket()");
 
    retval = WSAAsyncSelect(listen_sock, hWnd,
    WM_SOCKET, FD_ACCEPT|FD_CLOSE);
    if(retval == SOCKET_ERROR) err_quit("WSAAsyncSelect()");
 
    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()");
 
    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);
    }
 
    // 윈속 종료
    WSACleanup();
    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)
{
    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);
}

 

[소스 분석]

간단한 리스트로 클라이언트 접속이 이루어진다. 리스트의 앞부분으로 계속 추가된다.

AddSocketInfo(SOCKET sock)  함수는 클라이언트 접속시 소켓을 추가해주는 함수이다.

데이터를 초기화 하고..

 

ptr->recvdelayed = FALSE;       // 일단 FALSE 
ptr->next = SocketInfoList;        // 리스트.   다음을 현재 리스트의 앞부분을 가리키게 함
SocketInfoList = ptr;                  // 리스트 앞부분에 현재 소켓 정보 저장

 

GetSocketInfo(SOCKET sock) 함수는 소켓을 검색해서 얻는 것이다.

RemoveSocketInfo(SOCKET sock) 함수는 소켓을 삭제하는 함수이다.

간단한 리스트로 구현되어 있으니 Pass ~~

 

 

- listen_sock

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

listen_sock 의 역활은 클라이언트의 접속과 종료를 얻는것이기 때문에 FD_ACCEPT | FD_CLOSE 로 설정해주면 된다.

 

 

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

    {
    case WM_SOCKET: // 소켓 관련 윈도우 메시지
        ProcessSocketMessage(hWnd, uMsg, wParam, lParam);
        return 0;

    ......

    }

}

 

사용자 정의해준 WM_SOCKET 이벤트 발생시 처리해주는 함수를 호출.. 

WndProc 함수에는 최대한 간단히 해준다. 코드가 길어지기 때문에 함수 호출..

이 서버자체가 간단한 에코서버 이다 보니 ProcessSocketMessage() 함수 역시 간단하게 해석이

된다.

 

 

1. 클라이언트 접속(소켓 추가)

 

case FD_ACCEPT:
    .......    

    AddSocketInfo(client_sock);     // 소켓 추가(연결)
    // 쓰기, 읽기, 종료 설정

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

    ......

 

 

2. 클라이언트로부터 데이터 받기

 

case FD_READ:

    ptr = GetSocketInfo(wParam);      // 소켓 검색
    if(ptr->recvbytes > 0) {                // send 를 해야하므로 return 을 한다는 것이다.

                                                       // 처음엔 recvdelayed = FALSE
        ptr->recvdelayed = TRUE;
        return;
    }

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

    ptr->recvbytes = retval;             // 값을 넣어줬으니 다음에는 WRITE

 

 

3. 클라이언트에 데이터 전송

 

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);

    .....

    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);
        }
    }

    ........

뭐 간단하니.... Pass~~~   다 보냈으면 다시 READ