상세 컨텐츠

본문 제목

Select 모델

똑똑한 개발/C++ 게임개발

by 성댕쓰 2021. 11. 11. 22:46

본문

지난 글에서 논블록킹 소켓 통신이 불필요한 무한 반복문을 돌며 CPU자원을 낭비하는 단점이 있다는 것을 알게 되었다. 단점을 해결하기 위해 여러 방법이 존재하는데 이번엔 Select 모델에 대해 알아보자.

Select 모델은 socket 함수를 실행할 준비가 됐는지 확인하고 소켓 통신을 진행하는 방식이다. 블록킹, 논블록킹 모든 경우에 사용할 수 있다. 논블록킹 통신에 사용할 경우 무한 반복문을 돌며 함수를 실행하는 낭비를 줄일 수 있다.

 

Select 모델이라고 이름 붙여진 이유는 select 함수가 핵심 역할을 하기 때문이다. ::select함수를 실행하면 read, write 또는 예외 소켓 통신이 준비 될 때까지 기다린다. 그리고 그중 하나라도 준비되면 리턴하는데 select함수 매개변수로 받은 socket set에서 준비된 socket만을 남기고 모두 날려버린다. 코드를 보면 이해하기 더 쉽다.

 

GameServer.cpp

struct Session
{
    SOCKET socket;
    char recvBuffer[BUFSIZE] = {};
    int32 recvBytes = 0;
    int32 sendBytes = 0;
};

int main()
{
    WSAData wsaData;
    if (::WSAStartup(MAKEWORD(2, 2), &wsaData))
        return 0;

    // 블록킹(Blocking)소켓
    // accept -> 접속한 클라가 있을 때
    // connet -> 서버 접속 성공했을 때
    // send, sendto -> 요청한 데이터를 송신 버퍼에 복사했을 때
    // recv, recvfrom -> 수신 버퍼에 도착한 데이터가 있고, 이를 유저레벨 버퍼에 복사했을 때

    // 논블록킹(Non-Blocking)

    SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0);
    if (listenSocket == INVALID_SOCKET)
        return 0;

    u_long on = 1;
    if (::ioctlsocket(listenSocket, FIONBIO, &on) == INVALID_SOCKET)
        return 0;

    SOCKADDR_IN serverAddr;
    ::memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY);
    serverAddr.sin_port = ::htons(7777);

    if (::bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
        return 0;

    if (::listen(listenSocket, SOMAXCONN) == SOCKET_ERROR)
        return 0;
    
    cout << "Accept" << endl;

    // Select 모델 = (select 함수가 핵심이 되는)
    // 소켓 함수 호출이 성공할 시점을 미리 알 수 있다!
    // 문제 상황)
    // 수신버퍼에 데이터가 없는데, read 한다거나!
    // 	송신버퍼가 꽉 찼는데, write 한다거나!
    // - 블로킹 소켓 : 조건이 만족하지 않아서 블로킹되는 상황 예방
    // - 논블로킹 소켓 : 조건이 만족하지 않아서 불필요하게 반복 체크하는 상황 예방

    // socket set
    // 1) 읽기[] 쓰기[] 예외(OOB)[] 관찰 대상 등록
    // OutOfBand는 send() 마지막 인자 MSG_OOB로 보내는 특별한 데이터
    // 받는 쪽에서도 recv OOB 세팅을 해야 읽을 수 있음.
    // 2) select(readSet, writeSet, exceptSet) -> 관찰 시작
    // 3) 적어도 하나의 소켓이 준비되면 리턴 -> 낙오자는 알아서 제거됨.
    // 4) 남은 소켓 체크해서 진행

    // fd_set read;
    // FD_ZERO : 비운다
    // ex) FD_ZERO(set)
    // FD_SET : 소켓 s를 넣는다
    // ex) FD_SET(s, &set);
    // FD_CLR : 소켓 s를 제거
    // ex) FD_CLR(s, &set);
    // FD_ISSET : 소켓 s가 set에 들어있으면 0이 아닌 값을 리턴한다.
    vector<Session> sessions;
    sessions.reserve(100);

    fd_set reads;
    fd_set writes;

    while (true)
    {
        // 소켓 셋 초기화
        FD_ZERO(&reads);
        FD_ZERO(&writes);

        // ListenSocket 등록
        FD_SET(listenSocket, &reads);

        // 소켓 등록
        for (Session& s : sessions)
        {
            if (s.recvBytes <= s.sendBytes)
                FD_SET(s.socket, &reads);
            else
                FD_SET(s.socket, &writes);
        }

        //[옵션] 마지막 timeout 인자 설정 가능
        int32 retVal = ::select(0, &reads, &writes, nullptr, nullptr);
        if (retVal == SOCKET_ERROR)
            break;

        // Listener 소켓 체크
        if (FD_ISSET(listenSocket, &reads))
        {
            SOCKADDR_IN clientAddr;
            int32 addrLen = sizeof(clientAddr);
            
            SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
            if (clientSocket != INVALID_SOCKET)
            {
                cout << "Client Connected" << endl;
                sessions.push_back(Session{ clientSocket });
            }
        }

        // 나머지 소켓 체크
        for (Session& s : sessions)
        {
            // Read
            if (FD_ISSET(s.socket, &reads))
            {
                int32 recvLen = ::recv(s.socket, s.recvBuffer, BUFSIZE, 0);
                if (recvLen <= 0)
                {
                    // TODO : sessions 제거
                    continue;
                }

                s.recvBytes = recvLen;
            }

            // Write
            if (FD_ISSET(s.socket, &writes))
            {
                // 블로킹 모드 -> 모든 데이터 다 보냄
                // 논블로킹 모드 -> 일부만 보낼 수가 있음(상대방 수신 버퍼 상황에 따라)
                int32 sendLen = ::send(s.socket, &s.recvBuffer[s.sendBytes], s.recvBytes - s.sendBytes, 0);
                if (sendLen == SOCKET_ERROR)
                {
                    // TODO : session 제거
                    continue;
                }

                s.sendBytes += sendLen;
                if (s.recvBytes == s.sendBytes)
                {
                    s.recvBytes = 0;
                    s.sendBytes = 0;
                }
            }
        }
    }

    // 윈속 종료
    ::WSACleanup();
}

 

반복문에서 매번 소켓 셋 초기화하고 다시 소켓을 넣어주는 이유는 select함수가 준비된 소켓을 남기고 모두 날리기 때문이다.

 

Select모델의 단점은 fd_set 하나가 담을 수 있는 socket의 크기가 작다는 것이다. FD_SETSIZE 정의를 보면 64로 나와있다. 만약 6400명 동접을 대응하려면 fd_set 100개를 만들어 사용해야 한다.

 

1. 이전 방식(논블록킹)과 다른 점은?

- 소켓 함수 호출이 성공할 시점을 알 수 있음. 따라서, 불필요하게 반복 체크하는 상황 예방 가능.

2. select 모델이 동작하는 방식은?

- 읽기, 쓰기,예외 셋 중 하나의 소켓이 준비되면 리턴하는 select 함수를 이용함.

- select 함수는 대기 시간을 따로 정하지 않으면 적어도 하나의 소켓이 준비될 때까지 무한히 대기함.

- accept 로 들어온 소켓이 read 또는 write 준비 되었는지 확인하고 처리함.

3. recvBytes가 sendBytes 보다 작으면 읽기 셋팅하는 이유는?

- 지금 에코 서버를 만들고 있음. 쓰기 다음 동작은 읽기임.

4. 단점은?

- 준비된 소켓 셋 이외는 모두 제거 되어서 다시 초기화, 확인 작업을 거쳐야 함.

- select 함수에서 블록킹 될 수 있음.

- fd_set 하나로 관리 가능한 socket 사이즈가 64로 정해져 있다.

 

참조 : [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 - 인프런 | 강의 (inflearn.com)

'똑똑한 개발 > C++ 게임개발' 카테고리의 다른 글

Overlapped 모델(이벤트기반)  (0) 2021.11.17
WSAEventSelect 모델  (0) 2021.11.16
논블록킹 소켓  (0) 2021.11.09
소켓 옵션  (0) 2021.11.08
UDP 서버실습  (0) 2021.11.06

관련글 더보기

댓글 영역