C++ Completion Port 모델

정은성·2023년 6월 3일
2
post-thumbnail

※ Rookiss님의 [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 강의를 보고 정리한 글입니다.

지난번에 했던 콜백 기반 overlapped 모델을 정리하고 Completion Port(IOCP) 모델을 알아보자

Overlapped 모델 (콜백 기반)

  • 비동기 입출력 함수가 완료되면, 쓰레드마다 있는 APC 큐에 일감이 쌓임
  • Alertable Wait 상태로 들어가서APC 큐 비우기(콜백 함수)
  • 단점 ⇒ APC 큐가 쓰레드 마다 있고, Alertable Wait 자체도 조금 부담이 된다.

IOCP(Completion Port) 모델

  • APC → Completion Port (쓰레드 마다 있는 개념이 아닌 1개만 존재함. → 중앙에서 관리하는 APC 큐)
  • Alertable Wait → CP 결과 처리를 GetQueueCompletionStatus(큐에서 하나 가져오기)로 처리함 ⇒ 쓰레드와 궁합이 좋음

구현

이번 서버는 Main Thread가 Accept를 담당하고 다른 쓰레드가 recv를 맡아보는 식으로 제작할 것이다.

⇒ 논블로킹 소켓이 필요없음!

소켓 세팅

listen까지의 과정이다.

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;

Completion Port 생성

중앙 APC 큐 역할인 Completion Port를 생성해준다.

HANDLE iocpHandle = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

여기서 사용하는 함수인 CreateIoCompletionPort() 함수는 두 가지 쓰임새를 가지고 있다.

  1. 현재처럼 Completion Port를 생성할 때
  2. 소켓을 Completion Port에 등록할 때

⇒ 생성할 땐 파라미터를 위와 같이 넣어주면된다.

Accept

이번에 구현하는 서버의 메인스레드는 Accept만 담당한다.

⇒ 블로킹 소켓으로 accept

while (true) {
	SOCKADDR_IN clientAddr;
	int32 addrLen = sizeof(clientAddr);
	SOCKET clientSocket;

	clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
	if (clientSocket == INVALID_SOCKET)
		return 0;

Session 생성 및 소켓 등록

이번 서버는 1대1 연결을 목표로하는 게아닌 여러 클라이언트와 연결을 목표로하기 때문에 Session을 동적할당해 줄 것이다.

또한 관리할 vector를 만들어주자.

vector<Session*> sessionManager;

...

Session* session = new Session();
session->socket = clientSocket;
sessionManager.push_back(session);

그리고 소켓을 Completion Port에 등록해주자.

::CreateIoCompletionPort((HANDLE)clientSocket, iocpHandle, (ULONG_PTR)session, 0);

이 때의 CreateIoCoompletionPort() 함수는 소켓 등록으로 사용된다.

파라미터는 각각 이렇게 넣어준다.

  1. 소켓(HANDLE로 캐스팅 해서 넣기)
  2. Completion Port 만들어준 것
  3. 자신만의 키값(여기선 Session) → 이것으로 더 많은 정보 전송 가능
  4. iocp에게 활용 할 최대 쓰레드 (0 입력 시 최대 코어 개수)

Recv

현재는 입출력이 완료 됐을 때를 관찰하기 위해 Completion Port에 등록한 것 뿐이다. 그러니 한 번의 입출력은 해주어야한다!!

WSARecv를 해주기 앞서 저번에 WSARecv를 할 땐 overlapped를 session 맨 앞에 두어 session의 정보를 활용하는 곳에 썼다. 하지만 지금은 소켓을 등록해줄 때 key값으로 session을 넣어준 상태!!

이를 잘 이용하면 다른 정보도 줄 수 있을 것이다.

OverlappedEx 구조체 만들기

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

struct OverlappedEx {
	WSAOVERLAPPED overlapped = {};
	int32 type = 0;// read, write, accept, connect
};

우리는 어떤 타입의 입출력이었는 지를 포함하여 보내줄 것이다.

WSARecv

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);

완료된 결과 가져오기

이제 Completion Port에서 완료된 결과를 가져와 후처리를 해볼 것이다.

결과 받아오기

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;
		}

		cout << "Recv Data IOCP = " << bytesTransferred << endl; 

GetQueuedCompletionStatus() 함수를 이요해 입출력한 바이트 수, key값, overlapped를 가져왔다.

GetQueuedCompletionStatus의 파라미터

  1. Completion Port → 어느 Completion Port에서 할 것인지(인자로 받아왔음!)
  2. 입출력 크기
  3. key값
  4. overlapped
  5. 대기 시간

그 후 다시 한 번 WSARecv를 해준다.

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);

왜 해줄까?

다시 Recv를 안해주면 이 소켓은 이대로 끝이 난다. 하지만 다시 Recv를 해줄 경우 이 소켓에서 오는 정보를 한 번 뿐만아니라 계속 받아줄 수 있다.

다른 입출력이하고싶다면 OverlappedEx를 새로 만들어서 넣어주면된다.

0개의 댓글