C++ 콜백 함수 기반 Overlapped 모델

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

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

이번엔 Event 기반이 아닌 Callback함수를 넘기는 Callback 기반 Overlapped 모델을 만들어 보자.

콜백 함수 기반 Overlapped 모델 진행 순서

event 기반과 유사하지만 조금씩 다르다.

  1. 비동기 입출력 지원 소켓 생성

  2. 비동기 입출력 함수 호출 (완료 루틴[콜백함수]의 시작 주소를 넘겨준다)

  3. 비동기 작업이 바로 완료되지 안으면, WSA_IO_PENDING 오류 코드

  4. 비동기 입출력 함수를 호출한 쓰레드를 → Alertable Wait 상태로 만들기

    WaitForSingleObjectEx, WaitForMultipleObjectsEx, SleepEx, WSAWaitForMultipleEvents 같은 함수들로 Alertable Wait 상태로 만들 수 있음

  5. 비동기 IO가 완료되면, 운영체제에서 완료 루틴(콜백함수) 호출

Alertable Wait 상태??

쓰레드마다 apc 큐라는 것이 있다. 비동기 입출력이 완료가되면 APC큐에 콜백들을 쌓아두게된다. 이 때 alertable wait모드에 진입하게 되면 apc큐에 쌓여있는 것들을 모두 완료 루틴을 실행 한 후 다시 나와 나머지 일을 진행한다.

구현

소켓 세팅

listen 까지의 과정을 해준다!

// 원속 초기화 (ws2_32 라이브러리 초기화)
// 관련 정보가 wsaData에 채워짐
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;

u_long on = 1;
if (::ioctlsocket(listenSocket, FIONBIO, &on) == 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;

Accept

이번에도 recv, send만 모델을 적용할 것이기 때문에 일반 비동기 소켓으로 처리해준다.

SOCKADDR_IN clientAddr;
int32 addrLen = sizeof(clientAddr);
SOCKET clientSocket;

while (true) {
	clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
	if (clientSocket != INVALID_SOCKET)
		break;

	if (::WSAGetLastError() == WSAEWOULDBLOCK)
		continue;

	// 에러
	return 0;
}

Session session = Session{ clientSocket };

cout << "Client Connected! " << endl;

Recv

우리는 콜백 함수 방식으로 진행중이다. recv가 됐을 때 실행할 콜백함수를 만들어주자. 에코서버라면?? recv받아서 들어온 콜백함수에서 Send를 해주면 될것이다.

void CALLBACK RecvCallback(DWORD error, DWORD recvLen, LPWSAOVERLAPPED overlapped, DWORD flags) {
	cout << "Data Recv Len Callback = " << recvLen << endl;
}

콜백함수의 파라미터인터페이스를 ****맞춰줘야한다.

  1. 오류

    ⇒ 오류가 뜨면 0이 아닌 값이 들어옴

  2. 전송 바이트 수

  3. 비동기 입출력 함수 호출 시 넘겨준 WSAOVERLAPPED 구조체의 주소 값

  4. flag → 쓸모없는 값

이 함수의 인자들은 운영체제에서 콜백함수를 실행하며 채워줄 것이다.

세션이 여러개라고 가정했을 때 어떤 클라이언트 대상으로 이 콜백함수가 실행됐는지 알고싶다면 어떻게 해야할까??

session의 구조를 overlapped가 맨 위로 오게 한다면??

struct Session {
	WSAOVERLAPPED overlapped = {};
	SOCKET socket = INVALID_SOCKET;
	char recvBuffer[BUFSIZE] = {};
	int32 recvBytes = 0;
	int32 sendBytes = 0;
};

session의 주소가 overlapped의 주소가 된다!!

⇒ overlapped를 session으로 캐스팅을 사용해 활용 가능!!

while (true) {
	WSABUF wsaBuf;
	wsaBuf.buf = session.recvBuffer;
	wsaBuf.len = BUFSIZE;
	DWORD recvLen = 0;
	DWORD flags = 0;
	if (::WSARecv(clientSocket, &wsaBuf, 1, &recvLen, &flags, &session.overlapped, RecvCallback) == SOCKET_ERROR) {
		if (::WSAGetLastError() == WSA_IO_PENDING) {
			// Alertable wait

			::SleepEx(INFINITE, TRUE);
			cout << "Alertable Exit!" << endl;
		}
		else {
			//문제 상황
			break;
		}

	}
	else {
		cout << "Data Recv Len = " << recvLen << endl;
	}

콜백으로 실행하면 Data Recv Callback이 출력될 것이고 아니라면 Data Recv가 출력될 것이다.

  • Event 방식과 뭐가 다를까?
    Event는 socket과 이벤트를 하나씩 매칭 시켜줘야했음.
    다수를 처리할 때 정말 귀찮아짐.
    반면 콜백 방식은 SleepEx를 하는 순간 예약된 모든 콜백을 모두 해결해줌.

장/단점

장점

성능!!

단점

모든 비동기 소켓 함수에서 사용하지 않음(accept는 불가), 빈번하게 일어나는 alertable wait상태로 인해 성능이 저하도미.

APC 큐가 스레드 별로 잇음. ⇒ 다른 스레드가 대신 처리해줄 수 없다!

0개의 댓글