[C++]Overlapped

강병우·2023년 8월 20일
0

네트워크

목록 보기
7/7

epoll은 하나의 서버 스레드가 여러 개의 논블로킹 소켓을 다룰 때 유용하다면, IOCP(I/O Completion Port)는 다수의 Overlapped I/O을 처리하는데 유용하다. IOCP는 Overlapped가 등록된 소켓이 I/O 처리가 완료되면 이를 사용자에게 알려준다. 사용자는 Completion Event라는 I/O가 완료되었다는 신호를 꺼낼 수 있다. 사용자는 이 신호를 받고 처리하면 되기 때문에 모든 소켓에 대해 I/O가 완료되었는지 체크하는 루프를 돌지 않아도 된다(재시도 호출 절약).

아래는 Server 소스코드이다.

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>
#include <atomic>
#include <mutex>
#include <windows.h>
#include <future>
#include "ThreadManager.h"

#include <winsock2.h>
#include <mswsock.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

#include "Memory.h"

void HandleError(const char* cause)
{
	int32 errCode = ::WSAGetLastError();
	cout << cause << " ErrorCode : " << errCode << endl;
}

const int32 BUFSIZE = 1000;

struct Session
{
	// 시작주소는 Overlapped 구조체를 가리킴
	WSAOVERLAPPED overlapped = {};
	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;

		DWORD 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 wasData;
	if (::WSAStartup(MAKEWORD(2, 2), &wasData) != 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 (Completion Port) 모델
	// - APC -> Completion Port(쓰레드마다 있는건 아니고 1개. 중앙에서 관리하는 APC 큐)
	// - Alertable Wait -> CP 결과 처리를 GetQueuedCompletionStatus
	// 쓰레드랑 궁합이 좋다
	
	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); });
	}

	// 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);
		// WSAEVENT wsaEvent = ::WSACreateEvent();

		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;

		// ADD_REF
		DWORD recvLen = 0;
		DWORD flags = 0;
		::WSARecv(clientSocket, &wsaBuf, 1, &recvLen, &flags, &overlappedEx->overlapped, NULL);

		Session* s = sessionManager.back();
		sessionManager.pop_back();
		xdelete(s);


	}

	GThreadManager->Join();
	

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

서버의 메인스레드는 클라이언트 소켓이 리슨 소켓에 accept하는 역할을 담당한다. 처음에 iocpHandle를 초기화시킨 후::CreateIoCompletionPort로 클라이언트 소켓을 Completion Port 모델에 등록한다. 이후 WorkerThreadMain에서 메인 iocp 핸들값을 받아와서 I/O가 완료된 소켓을 ::GetQueuedCompletionStatus을 통해 가져온다. Recv가 끝나면 메인 스레드 측에서 소켓을 소멸시킨다.

게임서버 프로그래밍 교과서(저:배현직),
[C++과 언리얼로 만드는 MMORPG 게임개발 시리즈] Part4: 게임서버(강사 : 루키스)를 학습하고 정리한 내용입니다.

0개의 댓글

관련 채용 정보