2009년 9월 29일 화요일

WSAAsyncSelect

WSAAsyncSelect란?

   기존의 Select 모델을 윈도우 메시지 형태로 변화한 모델이라 생각하면 됨.

 

동작원리

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

     ex)  #define WM_SOCKET WM_USER + 1

            WSAAsyncSelect(clientSocket, hWnd, WM_SOCKET, FD_READ | FD_WRITE | FD_CLOSE);

 

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

 

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

 

함수 구조

int WSAAsyncSelect(

  SOCKET s,  // 설정하고 하는 소켓

  HWND hWnd, // 메세지 전달 처리를 위한 윈도우 핸들러

  unsigned int uMsg, // 확인할 윈도우 메세지

  long lEvent // 네트워크 이벤트 설정

);

 

 네트워크 이벤트  의미
 FD_ACCEPT  클라이언트가 접속하면 윈도우 메세지를 발생시킨다.
 FD_READ  데이터 수신이 가능하면..
 FD_WRITE  데이터 송신이 가능하면..
 FD_CLOSE  상대가 접속을 종료하면..
 FD_CONNECT  접속이 완료되면..
 FD_OOB  OOB 데이터가 도착하면

 

WSAAsyncSelect 함수의 유의 사항

1. WSAAsyncSelect 함수를 호출하면 해당 소켓은 자동으로 논블로킹 모드로 전환된다.

2. select 모드에서 소켓 함수당 읽기 셋 또는 쓰기 셋의 구분이 동일하다

3. 윈도우 메세지에 대응하여 소켓 함수를 호출하면 대부분 성공하지만, WSAEWOULDBLOCK 오류가 발생하는 경우도 있다. 꼭 체크하자

4. 윈도우 메세지를 받았을때 적절한 소켓 함수 처리를 안하면, 다음번 메세지는 발생하지 않는다.

 

예제 소스

  [유의사항]

    예제 소스를 작성하실때 비쥬얼스튜디오에서 제공하는 Win32 프로젝트를 사용하시면 됩니다.

    유니코드 기반이 아니므로, 프로젝트 설정시 언어체계 부분을 유니코드가 아닌 설정안함으로 변경하시길 바랍니다.

 

[Server.cpp]

 

#include <winsock2.h>
#include "Server.h"
#include <list>
#include <algorithm>

#define WM_SOCKET WM_USER + 1
#define MAX_BUFFER_SIZE 512
#define MAX_CONNECT 1024

// 소켓 정보 저장을 위한 구조체
typedef struct
{
        SOCKET sock; // 소켓
        char recvBuffer[MAX_BUFFER_SIZE + 1]; // 받기 버퍼
        char sendBuffer[MAX_BUFFER_SIZE + 1]; // 쓰기 버퍼
        int recvBytes; // 받은 데이터 크기
        int sendBytes; // 쓰기 데이터 크기
} SOCKET_INFO;

typedef std::list<SOCKET_INFO*> SOCKET_INFO_LIST;

SOCKET_INFO_LIST g_aFreeSocketInfo; // 사용되지 않은 소켓 정보 리스트
SOCKET_INFO_LIST g_aActiveSocketInfo; // 사용하고 있는 소켓 정보 리스트

SOCKET        g_ListenSocket = NULL; // 서버 리슨 소켓
HWND        g_hWndDisplay = NULL; // 출력을 위한 윈도우
BOOL        g_bStartServer = FALSE; // 서버 시작 확인 플래그

void err_quit(char* msg); // 에러 출력 후 종료 함수
void err_display(char* msg); // 에러 출력 함수
void err_display(int errCode); // 에러 코드를 통한 출력 함수
void DisplayText(char* fmt, ...); // 에러 출력 윈도우에 표현하기 위한 함수

BOOL                        AddActiveSocketInfo(SOCKET clientSocket); // 접속 소켓정보 추가 함수
void                        RemoveActiveSocketInfo(SOCKET clientSocket); // 접속해제 소켓 정보 삭제 함수
SOCKET_INFO*        GetActiveSocketInfo(SOCKET clientSocket); // 현재 접속되어 있는 소켓 정보 얻는 함수

void ServerInit(HWND hWnd)
{
        int retValue;

        // 출력을 위한 자식 윈도우를 하나 만든다.
        HINSTANCE hInst = (HINSTANCE)GetWindowLong(hWnd, GWL_HINSTANCE);

        g_hWndDisplay = CreateWindow(
                "edit", NULL,
                WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL | ES_AUTOHSCROLL | ES_AUTOVSCROLL | ES_MULTILINE | ES_READONLY,
                0, 0, 0, 0, hWnd, 0, hInst, NULL);

        if(g_hWndDisplay == NULL)
        {
                MessageBox(NULL, "출력용 윈도우 생성 실패", "error", MB_ICONERROR | MB_OK);
                PostQuitMessage(-1);
                return;
        }

        // 윈속 초기화
        WSADATA wsa;
        if(WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
        {
                PostQuitMessage(-1);
                return;
        }

        // 소켓 생성
        g_ListenSocket = socket(AF_INET, SOCK_STREAM, 0);
        if(g_ListenSocket == INVALID_SOCKET)
        {
                err_quit("socket()");
                return;
        }

        // WSAASyncSelect()
        retValue = WSAAsyncSelect(g_ListenSocket, hWnd, WM_SOCKET, FD_ACCEPT | FD_CLOSE);
        if(retValue == SOCKET_ERROR)
        {
                err_quit("WSAASyncSelect()");
                return;
        }

        // bind()
        SOCKADDR_IN serverAddr;
        memset(&serverAddr, 0, sizeof(serverAddr));
        serverAddr.sin_family = AF_INET;
        serverAddr.sin_port = htons(5001);
        serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
        retValue = bind(g_ListenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
        if(retValue == SOCKET_ERROR)
        {
                err_quit("bind()");
                return;
        }

        // listen()
        retValue = listen(g_ListenSocket, SOMAXCONN);
        if(retValue == SOCKET_ERROR)
        {
                err_quit("listen()");
                return;
        }

        // 사용하지 않는 소켓 정보 메모리 할당
        for(int i = 0; i < MAX_CONNECT; ++i)
        {
                SOCKET_INFO* pInfo = new SOCKET_INFO;
                memset(pInfo, 0, sizeof(SOCKET_INFO));
                g_aFreeSocketInfo.push_back(pInfo);
        }

        g_bStartServer = TRUE; // 서버 시작 확인
}

void ServerShutdown()
{
        if(g_bStartServer == FALSE)
                return;

        // 접속되어진 리스트를 메모리 해제한다.
        // 접속되어진 클라이언트가 있으므로 소켓도 해제한다.
        for(SOCKET_INFO_LIST::iterator i = g_aActiveSocketInfo.begin();
                        i != g_aActiveSocketInfo.end(); ++i)
        {
                closesocket((*i)->sock);
                delete (*i);
        }

        // 사용하지는 않는 할당 정보에 대해 해제한다.
        for(SOCKET_INFO_LIST::iterator i = g_aFreeSocketInfo.begin();
                        i != g_aFreeSocketInfo.end(); ++i)
        {
                delete (*i);
        }
       
        g_aActiveSocketInfo.clear();
        g_aFreeSocketInfo.clear();

        // 리슨 소켓을 닫는다.
        closesocket(g_ListenSocket);

        // 윈속종료
        WSACleanup();

        g_bStartServer = FALSE; // 서버 종료 확인
}

LRESULT CALLBACK ServerMessageProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
        switch(uMsg)
        {
        case WM_SIZE :
                // 출력하려는 윈도우에 대한 출력 위치값 변경
                MoveWindow(g_hWndDisplay, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);
                break;
        case WM_SOCKET :
                {
                        int retValue;

                        // 오류 발생 여부 확인
                        if(WSAGETSELECTERROR(lParam))
                        {
                                err_display(WSAGETSELECTERROR(wParam));
                                RemoveActiveSocketInfo(wParam);
                                return 0;
                        }

                        // 각 상황별 메세지 처리
                        switch(WSAGETSELECTEVENT(lParam))
                        {
                        case FD_ACCEPT : // 클라이언트가 접속 되었을 시
                                {
                                        SOCKADDR_IN clientAddr;
                                        int addrLength = sizeof(clientAddr);
                                        SOCKET clientSocket = accept(g_ListenSocket, (SOCKADDR*)&clientAddr, &addrLength);
                                        if(clientSocket == INVALID_SOCKET)
                                        {
                                                if(WSAGetLastError() != WSAEWOULDBLOCK)
                                                {
                                                        err_display("accept()");
                                                        break;
                                                }
                                                return 0;
                                        }
                                       
                                        DisplayText("[TCP 서버] 클라이언트 접속 : IP 주소 = %s, 포트번호 = %d\r\n",
                                                inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));

                                        // 접속된 클라이언트도 WSAAsyncSelect를 통해 설정해 준다.
                                        retValue = WSAAsyncSelect(clientSocket, hWnd, WM_SOCKET, FD_READ | FD_WRITE | FD_CLOSE);
                                        if(retValue == SOCKET_ERROR)
                                        {
                                                err_display("WSAAsyncSelect()");
                                                closesocket(clientSocket);
                                                return 0;
                                        }

                                        // 활성된 소켓정보 리스트에 추가한다.
                                        AddActiveSocketInfo(clientSocket);
                                }
                                break;
                        case FD_READ : // 패킷 도착
                                {
                                        // wParam 즉 소켓 값을 통해서 해당 소켓 정보를 찾는다.
                                        SOCKET_INFO* pSocketInfo = GetActiveSocketInfo(wParam);

                                        // 데이터를 읽는다.
                                        retValue = recv(pSocketInfo->sock, pSocketInfo->recvBuffer, MAX_BUFFER_SIZE, 0);
                                        if(retValue == SOCKET_ERROR)
                                        {
                                                if(WSAGetLastError() != WSAEWOULDBLOCK)
                                                {
                                                        err_display("recv()");
                                                        RemoveActiveSocketInfo(wParam);
                                                        return 0;
                                                }
                                        }
                                        pSocketInfo->recvBytes = retValue;
                                        pSocketInfo->recvBuffer[retValue] = '\0';

                                        SOCKADDR_IN socketAddr;
                                        int nAddrLength = sizeof(socketAddr);
                                        getpeername(pSocketInfo->sock, (SOCKADDR*)&socketAddr, &nAddrLength);
                                        DisplayText("[TCP/%s:%d] %s\r\n", inet_ntoa(socketAddr.sin_addr),
                                                ntohs(socketAddr.sin_port), pSocketInfo->recvBuffer);

                                        // 쓰기 버퍼에 값을 전달한다. (에코서버이므로)
                                        memcpy(pSocketInfo->sendBuffer, pSocketInfo->recvBuffer, retValue);
                                        pSocketInfo->sendBuffer[retValue] = '\0';
                                        pSocketInfo->sendBytes = retValue;

                                        // 쓰기 버퍼에 값이 있으므로, 보내기를 수행하도록 메세지를 전달한다.
                                        PostMessage(hWnd, WM_SOCKET, wParam, FD_WRITE);
                                }
                                break;
                        case FD_WRITE : // 패킷 보내기
                                {
                                        // wParam 즉 소켓 값을 통해서 해당 소켓 정보를 찾는다.
                                        SOCKET_INFO* pSocketInfo = GetActiveSocketInfo(wParam);
                                        if(pSocketInfo->sendBuffer <= 0)
                                                return 0;

                                        // 데이터를 보낸다.
                                        retValue = send(pSocketInfo->sock, pSocketInfo->sendBuffer, pSocketInfo->sendBytes, 0);
                                        if(retValue == SOCKET_ERROR)
                                        {
                                                if(WSAGetLastError() != WSAEWOULDBLOCK)
                                                {
                                                        err_display("send()");
                                                        RemoveActiveSocketInfo(wParam);
                                                        return 0;
                                                }
                                        }

                                        pSocketInfo->sendBytes = 0;
                                }
                                break;
                        case FD_CLOSE : // 접속 해제
                                RemoveActiveSocketInfo(wParam);
                                return 0;
                        }
                }
                break;
        }
        return 0;
}

BOOL AddActiveSocketInfo(SOCKET clientSocket)
{
        if(g_aFreeSocketInfo.empty())
        {
                DisplayText("[오류] 소켓 정보를 추가할 수 없습니다.\r\n");
                return FALSE;
        }

        // 사용하지 않는 소켓 정보를 하나 꺼낸다.
        SOCKET_INFO* pSocketInfo = (*g_aFreeSocketInfo.begin());
        g_aFreeSocketInfo.pop_front();

        pSocketInfo->sock = clientSocket;
        pSocketInfo->recvBytes = 0;
        pSocketInfo->sendBytes = 0;

        // 사용하는 소켓 정보 리스트의 관리로 넣는다.
        g_aActiveSocketInfo.push_back(pSocketInfo);

        return TRUE;
}

void RemoveActiveSocketInfo(SOCKET clientSocket)
{
        SOCKADDR_IN socketAddr;
        int nAddrLength = sizeof(socketAddr);
        getpeername(clientSocket, (SOCKADDR*)&socketAddr, &nAddrLength);
        DisplayText("[TCP 서버] 클라이언트 종료: IP 주소 = %s, 포트번호 = %d",
                inet_ntoa(socketAddr.sin_addr), ntohs(socketAddr.sin_port));

        // 사용하고 있는 소켓정보 리스트에서 조건에 맞는 소켓정보를 찾아
        // 접속을 해제하고, 사용하고 있는 소켓정보 리스트에서 삭제, 사용하지 않는 소켓정보 리스트에 추가하여
        // 다음번 사용을 다시 할 수 있도록 한다.
        for(SOCKET_INFO_LIST::iterator i = g_aActiveSocketInfo.begin();
                        i != g_aActiveSocketInfo.end(); ++i)
        {
                if(clientSocket == (*i)->sock)
                {
                        SOCKET_INFO* pSocketInfo = (*i);
                        closesocket(pSocketInfo->sock);

                        g_aActiveSocketInfo.erase(i);
                        g_aFreeSocketInfo.push_back(pSocketInfo);
                        DisplayText(" (%d/%d)\r\n", g_aActiveSocketInfo.size(), g_aFreeSocketInfo.size());
                        return;
                }
        }
}


SOCKET_INFO* GetActiveSocketInfo(SOCKET clientSocket)
{
        // 사용하고 있는 소켓 정보들에서 조건에 만족하는 소켓 정보를 얻는다.
        for(SOCKET_INFO_LIST::iterator i = g_aActiveSocketInfo.begin();
                        i != g_aActiveSocketInfo.end(); ++i)
        {
                if(clientSocket == (*i)->sock)
                        return (*i);
        }

        return NULL;
}


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

        DisplayText("[%s] %s\r\n", 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);

        DisplayText("[오류] %s", (LPCTSTR)lpMsgBuf);
        LocalFree(lpMsgBuf);
}

void DisplayText(char* fmt, ...)
{
        // printf와 같은 출력과, 가변적인 파라메터를 처리하는 루틴
        // va_list, va_start, vsprintf 등 MSDN을 참고하기 바람
        va_list arg;
        va_start(arg, fmt);

        char buf[1024];
        vsprintf(buf, fmt, arg);

        int nLength = GetWindowTextLength(g_hWndDisplay);
        SendMessage(g_hWndDisplay, EM_SETSEL, nLength, nLength);
        SendMessage(g_hWndDisplay, EM_REPLACESEL, FALSE, (LPARAM)buf);
}

 

[Server.h]

// 서버 초기화 함수
void        ServerInit(HWND hWnd);

 

// 서버 종료 함수
void        ServerShutdown();

 

// 서버 메세지 처리 함수
LRESULT CALLBACK ServerMessageProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

 

[기존 윈도우 메세지 함수 처리 내부]

 

case WM_CREATE :
        ServerInit(hWnd);
        break;

default:
        ServerMessageProc(hWnd, message, wParam, lParam);
        return DefWindowProc(hWnd, message, wParam, lParam);

 

댓글 없음:

댓글 쓰기