[C++]비동기(Overlapped) I/O

강병우·2023년 8월 18일
0

네트워크

목록 보기
4/7

회사에 복직해서 따로 공부할 시간이 없다. 몰아서 쓰는 것을 피하고 싶지만 어쩔 수 없다.. 개강 전까지 책이랑 병행하면서 기초를 다지고 네트워크 라이브러리를 짜보려고 한다

Overlapped(비동기) I/O

논블로킹 소켓은 블로킹에 빠지지 않고 송수신을 기다리는 동안 다른 작업을 할 수 있고 하나의 스레드에서 여러 개의 소켓을 다룰 수 있다는 장점이 있다. 하지만 이 완벽할 것만 같은 논블로킹 소켓에도 단점이 있다. 다음과 같다.

  1. I/O 함수가 리턴한 값이 블록킹인 경우, 재시도 호출에 따른 리소스 소모가 발생한다.
  2. I/O 함수가 호출할 때 입력하는 값에 대해 복사 연산이 발생한다.

위 두 문제를 해결하기 위해 Overlapped I/O를 사용한다. 소켓이 Overlapped I/O에 액세스하기 위해선 상태를 보관하는 구조체가 필요하다. 소켓이 송수신을 하면 즉시 결과가 반환되는데 송수신 중이라면 Pending값이 반환된다. 이후부터 Overlapped의 구조체를 통해 송수신 데이터를 볼 수 있다. 즉, recv나 send 함수를 재시도하지 않으며 메모리를 직접 주고 받기 때문에 복사 연산에 대한 리소스도 발생하지 않는다.

주의해야 할 점이 있다. 메모리를 직접 주고 받기 때문에 비동기로 처리되는 작업이 완료될 때까지 송수신에 사용된 데이터와 Overlapped 구조체는 없애거나 변경하면 안된다.
만약 동시에 여러 개의 Overlapped I/O 작업을 하고 싶다면, 새 객체를 선언해야 한다. 당연하게도 입력에 사용되는 데이터의 메모리도 서로 달라야 한다.

소스코드

#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")

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

void CALLBACK RecvCallBack(DWORD error, DWORD recvLen, LPWSAOVERLAPPED overlapped, DWORD flags)
{
	cout << "Data Recv Len Callback = " << recvLen << endl;
	// TODO : 에코 서버를 만든다면

	// 시작값이 overlapped 구조체이므로, 이렇게 캐스팅해서 보낼 수 있음.
	Session* session = (Session*)overlapped;
}

int main()
{
	WSAData wasData;
	if (::WSAStartup(MAKEWORD(2, 2), &wasData) != 0)
		return 0;

	// 블로킹(Blocking) 소켓
	// accept -> 접속한 클라가 있을 때
	// connect -> 서버 접속 성공했을 때
	// send, sendto -> 요청한 데이터를 송신 버퍼에 복사했을 때
	// recv, recvfrom -> 수신 버퍼에 도착한 데이터가 있고 이를 유저레벨 버퍼에 복사했을 때

	// 논블로킹(Non-Blocking)

	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;

	// Overlapped 모델 (이벤트 기반)
	// - 비동기 입출력 지원하는 소켓 생성
	// - 비동기 입출력 함수 호출 (1에서 만든 이벤트 객체를 같이 넘겨줌)
	// - 비동기 작업이 바로 완료되지 않으면, WSA_IO_PENDING 오류 코드
	// - 비동기 입출력 함수 호출한 쓰레드를 -> Alertable Wait 상태로 만든다
	// ex) WaitForSingleObjectEx, WaitForMultipleObjectsEx, SleepEx, WSAWaitForMultipleEvents
	// - 비동기 IO 완료되면, 운영체제는 완료 루틴 호출
	// - 완료 루틴 호출이 모두 끝나면, 쓰레드는 Alertable Wait 상태에서 빠져나온다.


	// Reactor Pattern (뒤늦게 논블로킹 소켓. 소켓 상태 확인 후 -> 뒤늦게 recv send 호출)
	// Proactor Pattern (미리 recv send 호출. Overlapped WSA)

	while (true)
	{
		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 };
		WSAEVENT wsaEvent = ::WSACreateEvent();
		

		cout << "Client Connected" << endl;

		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)
				{
					// Pending
					//Alertable Wait
					::SleepEx(INFINITE, TRUE);
					//::WSAWaitForMMultipleEvents(1, &wsaEvent, TRUE, WSA_INFINITE, TRUE);
				}
				else
				{
					// TODO : 문제있는 상황
					break;
				}

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

		}
	}

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

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

0개의 댓글

관련 채용 정보