Completion Port Model
카테고리: Server
인프런에 있는 루키스님의 게임 서버 강의를 듣고 정리한 내용입니다.
Completion Port Model
Overlapped(Completion Routine Callback Based)모델의 경우
- 비동기 입출력 함수가 완료되면, APC 큐에 콜백 함수가 쌓임
- Alertable Wait 상태로 진입해 APC 큐에 들어있는 함수들 처리
- Alertable Wait 상태를 왔다갔다하는 비용 발생
- APC 큐 관리를 OS가 하기 때문에 동기화가 까다로움
- 콜백 함수를 실행할 때 어느 스레드에서 실행하는지 추적하기가 까다로움
- 따라서 클라이언트가 많이 붙는 멀티스레드 환경에서는 적합하지 않음
반면 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()
함수로 비동기 작업을 걸어줘야 함
댓글 남기기