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)
네트워크 라이브러리 만들기(Iocp Core) (0) | 2021.11.29 |
---|---|
네트워크 라이브러리 제작(Socket Utils) (0) | 2021.11.27 |
Overlapped모델 (콜백기반) (0) | 2021.11.18 |
Overlapped 모델(이벤트기반) (0) | 2021.11.17 |
WSAEventSelect 모델 (0) | 2021.11.16 |
댓글 영역