Completion Port 모델
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)