Completion Port Model

Date:     Updated:

카테고리:

태그:

인프런에 있는 루키스님의 게임 서버 강의를 듣고 정리한 내용입니다.


Completion Port Model

Overlapped(Completion Routine Callback Based)모델의 경우

  • 비동기 입출력 함수가 완료되면, APC 큐에 콜백 함수가 쌓임
  • Alertable Wait 상태로 진입해 APC 큐에 들어있는 함수들 처리
  • Alertable Wait 상태를 왔다갔다하는 비용 발생
  • APC 큐 관리를 OS가 하기 때문에 동기화가 까다로움
  • 콜백 함수를 실행할 때 어느 스레드에서 실행하는지 추적하기가 까다로움
  • 따라서 클라이언트가 많이 붙는 멀티스레드 환경에서는 적합하지 않음


Image

반면 IOCP 모델의 경우

  • 중앙의 Completion Port에서 IO상태를 관리함
  • Completion 큐에 일감들이 쌓이면 Worker Thread들이 일감을 가져가 처리
  • 특정 스레드를 Alertable Wait 상태로 만든다던가 하는 별도의 처리가 필요 없음


// Bind ListenSocket

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

struct OverlappedEx
{
	WSAOVERLAPPED overlapped = {};
	int32 type = 0;
};

vector<Session*> sessionManager;

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

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

// Accept : Main Thread에서 담당
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, (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);
}

// WinSock 종료
::WSACleanup();
}
  • overlapped 구조체 구조 변경
    • overlapped 구조체의 type을 통해 작업을 구별하도록 구조 변경
    • 이후 WorkerThread에서 ovelapped->type을 통해 해당 작업 처리
  • session과 ovelappedEx를 new로 힙에 할당한 이유는 대규모 서버 가정
    • 세션이 굉장히 많이 붙는 상황을 가정해 스택이 아닌 힙에 할당


CreateIoCompletionPort

HANDLE CreateIoCompletionPort(
    HANDLE FileHandle,      // 파일 또는 소켓 핸들 (연결할 대상)
    HANDLE ExistingCompletionPort, // 기존 IOCP 핸들 (없으면 새로 생성)
    ULONG_PTR CompletionKey, // I/O 완료 시 함께 반환할 키값 (사용자 정의)
    DWORD NumberOfConcurrentThreads // 병렬로 실행할 최대 스레드 수
);
  • ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
    • 기본값들 넘겨주면 Completion Port 생성
  • ::CreateIoCompletionPort((HANDLE)clientSocket, iocpHandle, (ULONG_PTR)session, 0);
    • 또는 Completion Port에 소켓 등록
    • 이때 넘겨주는 key값을 통해 소켓 정보 복원


GetQueuedCompletionStatus

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);
	}
}
  • WSARecv()함수를 통해 IO 이벤트가 발생하면, GetQueuedCompletionStatus() 함수로 대기 중인 스레드 하나를 깨우고 해당 이벤트 처리
  • 매개변수를 통해 완료된 작업의 정보 복원
  • 작업 처리 후 다시 WSARecv() 함수로 비동기 작업을 걸어줘야 함



맨 위로 이동하기

Server 카테고리 내 다른 글 보러가기

댓글 남기기