상세 컨텐츠

본문 제목

Completion Port 모델

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

by 성댕쓰 2021. 11. 19. 12:24

본문

Completion Port 모델을 줄여서 IOCP라고 한다. 많은 MMORPG 서버에서 사용하는 네트워크 IO모델이다.

기존 모델과 다른 점은 Block하는 부분이 없고 멀티 Thread에서 처리가 용이하다는 것이다.

 

GameServer.cpp

const int32 BUFSIZE = 1000;

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

enum IO_TYPE
{
    READ,
    WRITE,
    ACCEPT,
    CONNECT
};

struct OverlappedEx
{
    WSAOVERLAPPED overlapped = {};
    int32 type = 0; // read, write, accept, connect ...
};

void WorkerThreadMain(HANDLE iocpHandle)
{
    while (true)
    {
        DWORD bytesTransferred = 0;
        Session* session = nullptr;
        OverlappedEx* overlappedEx = nullptr;

        BOOL ret = ::GetQueuedCompletionStatus(iocpHandle, &bytesTransferred,
            (ULONG_PTR*)&session, (LPOVERLAPPED*)&overlappedEx, INFINITE);

        if (ret == FALSE || bytesTransferred == 0)
        {
            // TODO : 연결 끊김
            continue;
        }

        ASSERT_CRASH(overlappedEx->type == IO_TYPE::READ);
        
        cout << "Recv Data IOCP = " << bytesTransferred << endl;

        WSABUF wsaBuf;
        wsaBuf.buf = session->recvBuffer;
        wsaBuf.len = BUFSIZE;

        DWORD recvLen = 0;
        DWORD flags = 0;

        ::WSARecv(session->socket, &wsaBuf, 1, &recvLen, &flags, &overlappedEx->overlapped, NULL);
    }
}

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

    SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0);
    if (listenSocket == 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;

    // Overlapped 모델(Completion Routine 콜백 기반)
    // - 비동기 입출력 함수 완료되면, 쓰레드마다 있는 APC 큐에 일감이 쌓임
    // - Alertable Wait 상태로 들어가서 APC 큐 비우기(콜백 함수)
    // 단점) APC큐가 쓰레드마다 있다! Alertable Wait 자체도 조금 부담!
    // 단점) 이벤트 방식 소켓 : 이벤트 1:1 대응

    // IOCP (Compleion Port) 모델
    // - APC -> Completion Port (쓰레드마다 있는 건 아니고 1개. 중앙에서 관리하는 APC 큐 같은 느낌)
    // - Alertable Wait -> CP 결과 처리를 GetQueuedCompletionStatus
    // 쓰레드랑 궁합이 굉장히 좋다!

    // CreateIoCompletionPort
    // GetQueuedCompletionStatus

    vector<Session*> sessionManager;

    // CP 생성
    HANDLE iocpHandle = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

    // WokerThreads
    for (int32 i = 0; i < 5; i++)
        GThreadManager->Launch([=]() {WorkerThreadMain(iocpHandle); });

    // Main thread = Accept 담당
    while (true)
    {
        SOCKADDR_IN clientAddr;
        int32 addrLen = sizeof(clientAddr);

        SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
        if (clientSocket == INVALID_SOCKET)
            return 0;

        Session* session = xnew<Session>();
        session->socket = clientSocket;
        sessionManager.push_back(session);

        cout << "Client Connected !" << endl;

        // 소켓을 CP에 등록
        ::CreateIoCompletionPort((HANDLE)clientSocket, iocpHandle, /*Key*/(ULONG_PTR)session, 0);

        WSABUF wsaBuf;
        wsaBuf.buf = session->recvBuffer;
        wsaBuf.len = BUFSIZE;

        OverlappedEx* overlappedEx = new OverlappedEx();
        overlappedEx->type = IO_TYPE::READ;

        DWORD recvLen = 0;
        DWORD flags = 0;

        ::WSARecv(clientSocket, &wsaBuf, 1, &recvLen, &flags, &overlappedEx->overlapped, NULL);

        // 유저가 게임 접속 종료! 
        // 유효하지 않은 메모리에 접근하기 때문에 후에 crash 남 
        // 더 큰 문제는 당장 안날 수도 있음
        // stopm allocator 활용하기
        //Session* s = sessionManager.back();
        //sessionManager.pop_back();
        //xdelete(s);
        // 
        //::closesocket(session.socket);
        //::WSACloseEvent(wsaEvent);
    }

    GThreadManager->Join();

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

 

session을 관찰하기로 등록해놓고 위 코드 마지막 부분 주석처럼 session을 지워버리면 나중에 worker thread에서 잘못된 주소 참조 오류가 난다. 바로 나면 그나마 다행이고 한참 후 이상한 곳에서 에러가 날 수도 있다.

이런 상황을 방지하기 위해서 이전에 만들어놓은 StompAllocator를 이용하여 디버그한다. 그리고 등록한 session을 지우거나 하지 못하게 하기 위한 작업을 해야 한다. 대표적으로 ref count를 이용할 수 있다. 관련 구현은 추후에 할 예정이다.

 

아래 링크를 참조하여 IOCP에 대해 더 정확히 이해하자.

https://www.slideshare.net/namhyeonuk90/iocp

 

1. iocp와 콜백기반 overlapped 모델을 구분하는 기준은 무엇?

- apc 큐 대신, completion port가 그 일을 함. apc 큐와 달리 쓰레드마다 있지 않음. 보통 하나만 만들어서 사용함.

- alertable wait 상태 만들기 대신, GetQueuedCompletionStatus 사용.

2. iocp 소켓을 만드는 함수와 소켓을 등록하는 함수는?

- CreateIoCompletionPort

3. OverlappedEx 의 용도는?

- overlapped 전달하고 받을 때, overlapped 이외의 값도 전달 그리고 받을 수 있게 하기 위함.

4. 메인 쓰레드에서 Recv 뒤 코드 흐름은?

- Recv만 걸어놓고 다른 클라이언트의 연결을 기다림

- Recv 처리는 별도의 thread에서 진행.

5. 완료된 일감이 있는지, 확인하는 함수는?

- GetQueuedCompletionStatus, 시간 무제한으로 해놓으면 여기서 block 됨.

 

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

관련글 더보기

댓글 영역