상세 컨텐츠

본문 제목

WSAEventSelect 모델

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

by 성댕쓰 2021. 11. 16. 22:32

본문

select 모델은 ::select(...)에서 block되는 구조였다.

::WSAEventSelect(...)를 통해 해당 함수 호출은 non-blocking으로 하고 이벤트가 발생하면 통보를 받는 방식으로 구현할 수 있다. 이를 WSAEventSelect 모델이라고 한다.

 

GameServer.cpp

const int32 BUFSIZE = 1000;

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 -> 접속한 클라가 있을 때
    // connect -> 서버 접속 성공했을 때
    // 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;

    // WSAEventSelect = (WSAEventSelect 함수가 핵심이 되는)
    // 소켓과 관련된 네트워크 이벤트를 [이벤트 객체]를 통해 감지

    // 이벤트 객체 관련 함수들
    // 생성 : WSACreateEvent (수동 리셋 Manual-Reset + Non-Signaled 상태 시작)
    // 삭제 : WSACloseEvent
    // 신호 상태 감지 : WSAWaitForMultipleEvents
    // 구체적인 네트워크 이벤트 알아내기 : WSAEnumNetworkEvents

    // 소켓 <-> 이벤트 객체 연동
    // WSAEventSelect(socket, event, networkEvents);
    // - 관심있는 네트워크 이벤트
    // FD_ACCEPT : 접속한 클라이 있음 accept
    // FD_READ : 데이터 수신 가능 recv, recvfrom
    // FD_WRITE : 데이터 송신 가능 send, sendto
    // FD_CLOSE : 상대가 접속 종료
    // FD_CONNECT : 통신을 위한 연결 절차 완료
    // FD_OOB

    // 주의 사항
    // WSAEventSelect 함수를 호출하면, 해당 소켓은 자동으로 Non-blocking 모드로 전환됨.
    // accept() 함수가 리턴하는 소켓은 listenSocket과 동일한 속성을 갖는다
    // - 따라서 clientSocket은 FD_READ, FD_WRITE 등을 다시 등록 필요
    // 드물게 WSAEWOULDBLOCK 오류가 뜰 수 있으니 예외 처리 필요
    // 중요)
    // - 이벤트 발생 시, 적절한 소켓 함수 호출해야 함
    // - 아니면 다음 번에는 동일 네트워크 이벤트가 발생 X
    // ex) FD_READ 이벤트 떳으면 recv() 호출해야 하고, 안하면 FD_READ 두 번 다시 X

    // 1) cout, event
    // 2) waitAll : 모두 기다림? 하나만 완료 되어도 OK?
    // 3) timeoout
    // 4) 지금은 false
    // return : 완료된 첫 번째 인덱스
    // WSAWaitForMultipleEvents

    // 1) socket
    // 2) eventObject : socket과 연동된 이벤트 객체 핸들을 넘겨주면, 이벤트 객체를 non-signaled
    // 3) networkEvent : 네트워크 이벤트 / 오류 정보가 저장
    // WSAEnumNetworkEvents

    vector<WSAEVENT> wsaEvents; // session 갯수만큼 WSAEVENT도 만들어준다
    vector<Session> sessions;
    sessions.reserve(100);

    WSAEVENT listenEvent = ::WSACreateEvent();
    wsaEvents.push_back(listenEvent);
    sessions.push_back(Session{ listenSocket });
    if (::WSAEventSelect(listenSocket, listenEvent, FD_ACCEPT | FD_CLOSE) == SOCKET_ERROR)
        return 0;

    while (true)
    {
        int32 index = ::WSAWaitForMultipleEvents(wsaEvents.size(), &wsaEvents[0], FALSE, WSA_INFINITE, FALSE);
        if (index == WSA_WAIT_FAILED)
            continue;
        index -= WSA_WAIT_EVENT_0;

        //::WSAResetEvent(wsaEvents[index]);

        WSANETWORKEVENTS networkEvents;
        if (::WSAEnumNetworkEvents(sessions[index].socket, wsaEvents[index], &networkEvents) == SOCKET_ERROR)
            continue;

        // Listener 소켓 체크
        if (networkEvents.lNetworkEvents & FD_ACCEPT)
        {
            // Error-Check
            if (networkEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
                continue;

            SOCKADDR_IN clientAddr;
            int32 addrLen = sizeof(clientAddr);

            SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
            if (clientSocket != INVALID_SOCKET)
            {
                cout << "Client Connected" << endl;

                WSAEVENT clientEvent = ::WSACreateEvent();
                wsaEvents.push_back(clientEvent);
                sessions.push_back(Session{ clientSocket });
                if (::WSAEventSelect(clientSocket, clientEvent, FD_READ | FD_WRITE | FD_CLOSE) == SOCKET_ERROR)
                    return 0;
            }
        }

        // Client Session 소켓 체크
        if (networkEvents.lNetworkEvents & FD_READ || networkEvents.lNetworkEvents & FD_WRITE)
        {
            // Error-Check
            if ((networkEvents.lNetworkEvents & FD_READ) && (networkEvents.iErrorCode[FD_READ_BIT] != 0))
                continue;
            // Error-Check
            if ((networkEvents.lNetworkEvents & FD_WRITE) && (networkEvents.iErrorCode[FD_WRITE_BIT] != 0))
                continue;

            Session& s = sessions[index];

            // Read
            if (s.recvBytes == 0)
            {
                int32 recvLen = ::recv(s.socket, s.recvBuffer, BUFSIZE, 0);
                if (recvLen == SOCKET_ERROR && ::WSAGetLastError() != WSAEWOULDBLOCK)
                {
                    // TODO : Remove Session
                    continue;
                }

                s.recvBytes = recvLen;
                cout << "Recv Data = " << recvLen << endl;
            }

            // Write
            if (s.recvBytes > s.sendBytes)
            {
                int32 sendLen = ::send(s.socket, &s.recvBuffer[s.sendBytes], s.recvBytes - s.sendBytes, 0);
                if (sendLen == SOCKET_ERROR && ::WSAGetLastError() != WSAEWOULDBLOCK)
                {
                    // TODO : Remove Session
                    continue;
                }

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

                cout << "Send Data = " << sendLen << endl;
            }
        }

        // FD_CLOSE 처리
        if (networkEvents.lNetworkEvents & FD_CLOSE)
        {
            // TODO : Remove Socket
        }

    }

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

 

WSAEventSelect 모델은 한계가 있다. ::WSAWaitForMultipleEvents 가 하나의 함수로 통지 받을 수 있는 최대 이벤트 개수는 64이다.

 

1. select 모델과 다른 점은?

- select 모델과 다르게 select에서 block 되지 않는다. 이벤트로 소켓상태를 통지 받는다.

- 그러나 WSAWaitForMultipleEvents에서 block 된다.

2. accept 함수가 리턴하는 socket은 어떤 성질을 갖는지? 이 성질이 read, write에는 어떤 영향을 미치는지?

- listen socket과 비슷한 성질을 갖는데, read, write 이벤트를 받으려면 이를 다시 등록해야 한다.

3. event 발생시, 적절한 함수 호출하지 않으면 발생하는 문제는?

- 동일 네트워크에서 동일한 이벤트가 더 이상 발생하지 않는다.

4. vector WSAEVENT 와 vector Session 을 1:1 매칭 하는 이유는?

- WSAWaitForMultipleEvents 으로 얻을 수 있는 index를 통해 해당 이벤트가 어떤 세션의 이벤트 였는지 알기 위함.

 

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

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

Overlapped모델 (콜백기반)  (0) 2021.11.18
Overlapped 모델(이벤트기반)  (0) 2021.11.17
Select 모델  (0) 2021.11.11
논블록킹 소켓  (0) 2021.11.09
소켓 옵션  (0) 2021.11.08

관련글 더보기

댓글 영역