2009년 9월 29일 화요일

소켓의 입출력 모델(Select - 배열 사용)

Select 모델

  Select 모델을 사용하면 소켓모드(블로킹, 넌블로킹)에 관계없이 여러 소켓을 한 스레드로 처리하는 모델이다.

 

Select 모델 동작원리

  소켓 함수를 호출해야 할 시점을 알려줌으로써 함수 호출 시 항상 성공하도록 하는 것이다.

  효과는 다음과 같다.

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

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

 

   3가지의 소켓 셋을 준비한다.

    * 읽기 셋

          클라이언트가 접속했으므로 accept() 함수를 호출한다.

          데이터를 받았으므로 recv(), recvfrom()등의 함수를 호출한다.

          연결이 종료되었으므로 recv(), recvfrom()등의 함수를 호출한다. 이때 리턴 값은 0 or SOCKET_ERROR이다.

 

    * 쓰기 셋

          송신 버퍼에 데이터를 쓰고, send(), sendto()등의 함수를 호출하여 데이터를 보낼 수 있다.

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

 

    * 예외 셋

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

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

 

      OOB(Out-Of-Band) 데이터란 send() 함수의 마지막 인자로 MSG_OOB를 사용하여 보내는 데이터를 말한다. 이 경우 recv()함수의 마지막 인자 MSG_OOB를 사용해야만 데이터를 수신 할 수 있다. 원래 OOB 데이터는 긴급한 상황을 알리는 용도로 사용하는데, TCP/IP에서는 1바이트 이상을 보낼 수 없다는 특징때문에 잘 사용하지 않는다.

 

  Select() 함수는

     int Select(

         int nfds,  // 유닉스와 호환성을 위해 존재하며, 윈도우는 사용하지 않는다.

         fd_set* readfds, // 읽기 셋

         fd_set* writefds, // 쓰기 셋

         fd_set* exceptfds, // 예외 셋

         const struct timeval* timeout // 초와 마이크로초 단위로 타임아웃을 나타낸다.

         // NULL : 적어도 한개 소켓이 조건을 만족 할 때까지 무한히 기다린다. 리턴값은 조건을 만족하는 소켓의 개수가 된다.

         // {0, 0} : 소켓 셋에 포함된 모든 소켓을 검사한 수 곧바로 리턴한다.

         // 양수 : 적어도 한 소켓이 조건을 만족할 때까지 기다리되, 타임아웃으로 지정한 시간이 지나면 리턴한다.

     );

 

  절차는 다음과 같다.

     1. 소켓 셋을 비운다.

     2. 소켓 셋에 소켓을 넣는다. 최대 넣을 수 있는 개수는 FD_SETSIZE(64)로 정의되어 있다.

     3. select() 함수를 호출한다. select() 함수는 블로킹 함수로 동작

     4. select() 함수가 리턴한 후 소켓 셋에 남아있는 모든 소켓에 대해 적절한 소켓 함수를 호출하여 처리한다.

     5. 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) : 셋을 비운다.

 

예제

 

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

#define MAX_BUFFER_SIZE 512

 

// 소켓 정보 저장을 위한 구조체
typedef struct
{
    SOCKET sock;
    char buffer[MAX_BUFFER_SIZE + 1];
    int recvBytes;
    int sendBytes;
} SOCKET_INFO;

 

int g_nTotalSockets = 0;

 

SOCKET_INFO* g_aSocketInfoArray[FD_SETSIZE];

 

// 소켓 관리 함수
BOOL AddSocketInfo(SOCKET clientSocket);
void RemoveSocketInfo(int nIndex);

 

// 오류 출력 함수
void err_quit(char* msg);
void err_display(char* msg);

 

int _tmain(int argc, _TCHAR* argv[])
{
    int retValue;

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

 

    // socket()
    SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, 0);
    if(listenSocket == INVALID_SOCKET)
        err_quit("socket()");

 

    // 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(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
    if(retValue == SOCKET_ERROR)
        err_quit("bind()");

 

    //listen()
    retValue = listen(listenSocket, SOMAXCONN);
    if(retValue == SOCKET_ERROR)
        err_quit("listen()");

 

    // 넌블로킹 소켓으로 전환
    unsigned long on = TRUE;
    retValue = ioctlsocket(listenSocket, FIONBIO, &on);
    if(retValue == SOCKET_ERROR)
        err_display("ioctlsocket()");

 

    // 통신에 사용할 변수
    FD_SET rset;
    FD_SET wset;
    SOCKET clientSocket;
    SOCKADDR_IN clientAddr;
    int nAddrLength;

 

    while(1)
    {
        // 소켓 셋 초기화
        FD_ZERO(&rset);
        FD_ZERO(&wset);

 

        // 소켓 셋 지정
        FD_SET(listenSocket, &rset);

        for(int i = 0; i < g_nTotalSockets; ++i)
        {
            if(g_aSocketInfoArray[i]->recvBytes > g_aSocketInfoArray[i]->sendBytes)
                FD_SET(g_aSocketInfoArray[i]->sock, &wset);
            else
                FD_SET(g_aSocketInfoArray[i]->sock, &rset);
        }

 

        // select()
        retValue = select(0, &rset, &wset, NULL, NULL);
        if(retValue == SOCKET_ERROR)
            err_quit("select()");

 

        // 소켓 셋 검사 #1 : 클라이언트 접속
        if(FD_ISSET(listenSocket, &rset))
        {
            nAddrLength = sizeof(clientAddr);
            clientSocket = accept(listenSocket, (SOCKADDR*)&clientAddr, &nAddrLength);
            if(clientSocket == 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(clientSocket) == FALSE)
                {
                    printf("[TCP 서버] 클라이언트 접속을 해제 합니다n");
                    closesocket(clientSocket);
                }
            }
        }

 

        // 소켓 셋 검사 #2 : 데이터 통신
        for(int i = 0; i < g_nTotalSockets; ++i)
        {
            SOCKET_INFO* pInfo = g_aSocketInfoArray[i];
            if(FD_ISSET(pInfo->sock, &rset))
            {
                // 데이터받기
                retValue = recv(pInfo->sock, pInfo->buffer, MAX_BUFFER_SIZE, 0);
                if(retValue == SOCKET_ERROR)
                {
                    if(WSAGetLastError() != WSAEWOULDBLOCK)
                    {
                        err_display("recv()");
                        RemoveSocketInfo(i);
                        continue;
                    }
                }
                else if(retValue == 0)
                {
                    RemoveSocketInfo(i);
                    continue;
                }
                else
                {
                    pInfo->recvBytes = retValue;

 

                    // 받은 데이터 출력
                    SOCKADDR_IN sockAddr;
                    int nAddrLength = sizeof(sockAddr);
                    getpeername(pInfo->sock, (SOCKADDR*)&sockAddr, &nAddrLength);

                    pInfo->buffer[retValue] = '\0';
                    printf("[TCP/%s:%d] %s\n",
                        inet_ntoa(sockAddr.sin_addr), ntohs(sockAddr.sin_port), pInfo->buffer);
                }
            }

            if(FD_ISSET(pInfo->sock, &wset))
            {
                // 데이터보내기
                retValue = send(pInfo->sock, pInfo->buffer + pInfo->sendBytes,
                    pInfo->recvBytes - pInfo->sendBytes, 0);

                if(retValue == SOCKET_ERROR)
                {
                    if(WSAGetLastError() != WSAEWOULDBLOCK)
                    {
                        err_display("send()");
                        RemoveSocketInfo(i);
                        continue;
                    }
                }

                pInfo->sendBytes += retValue;
                if(pInfo->recvBytes == pInfo->sendBytes)
                    pInfo->recvBytes = pInfo->sendBytes = 0;
            }
        }
    }

    return 0;
}

 

BOOL AddSocketInfo(SOCKET clientSocket)
{
    if(g_nTotalSockets >= (FD_SETSIZE - 1))
    {
        printf("[오류] 소켓 정보를 추가할 수 없습니다.\n");
        return FALSE;
    }

    SOCKET_INFO* pSocketInfo = new SOCKET_INFO;
    if(pSocketInfo == NULL)
    {
        printf("[오류] 메모리가 부족합니다.\n");
        return FALSE;
    }

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

    g_aSocketInfoArray[g_nTotalSockets++] = pSocketInfo;

    return TRUE;
}

 

void RemoveSocketInfo(int nIndex)
{
    SOCKET_INFO* pInfo = g_aSocketInfoArray[nIndex];

 

    // 클라이언트 정보 얻기
    SOCKADDR_IN socketAddr;
    int nAddrLength = sizeof(socketAddr);
    getpeername(pInfo->sock, (SOCKADDR*)&socketAddr, &nAddrLength);
    printf("[TCP 서버] 클라이언트 종료: IP 주소 = %s, 포트번호 = %d\n",
        inet_ntoa(socketAddr.sin_addr), ntohs(socketAddr.sin_port));

    closesocket(pInfo->sock);
    delete pInfo;

    for(int i = nIndex; i < g_nTotalSockets; ++i)
        g_aSocketInfoArray[i] = g_aSocketInfoArray[i + 1];

    g_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\n", msg, (LPCTSTR)lpMsgBuf);
    LocalFree(lpMsgBuf);
}

댓글 없음:

댓글 쓰기