VisualStudio/C++서버

[C++] Overlapped모델 (비동기 + 논블로킹)

usingsystem 2024. 5. 24. 10:49
728x90

Overlapped 콜백기반은 비동기 입출력 함수가 완료가 되면 스레드마다 있는 APC 큐에 일감이 쌓인다.

Alertable Wait 상태로 들어가면 APC큐를 전체 비운다. (콜백 함수 호출)

APC큐는 쓰레드마다있기 때문에 멀티 스레드 환경에서 적절하게 배분하는데 어려움이 있다.( Alertable Wait 상태로 들어가 APC큐를 비워야 하기 때문이다.)  또 한 Alertable Wait 계속 호출하는 부담이 있다.

 

Overlapped  이벤트 방식으로는 소켓과 이벤트를 1대 1로 대응해야 하고 그리고 감시할 수 있는 수량도 64개 밖에 되지 않기 때문에 많은 수의 이벤트를 관찰하기 어렵다. 

 

이런 문제를 보안하기 위해 IOPC(Completion Port) 방식이 나오게된다.

Overlapped모델 (이벤트기반)

Overlapped 이벤트 기반 모델의 동작 흐름

  1. 비동기 입출력을 지원하는 소켓을 생성합니다.
  2. 비동기 입출력 함수를 호출하며, 통지를 받기 위한 이벤트 객체를 함께 넣어줍니다.
  3. 비동기 방식이기 때문에 함수가 즉시 실행되지 않을 수 있으며, 바로 완료되지 않으면 WSA_IO_PENDING 오류가 발생합니다.
  4. 입출력 작업이 완료되면 운영 체제는 이벤트 객체를 SIGNALED 상태로 만들어 완료 상태를 알려줍니다.
  5. 결과를 확인하기 위해 WSAWaitForMultipleEvents 함수를 호출하여 이벤트 객체의 상태를 판단합니다.
  6. 결과 확인은 WSAGetOverlappedResult 함수를 호출하여 비동기 입출력 결과를 확인하고 데이터를 처리합니다.
#include "pch.h"
#include <iostream>
#include "CorePch.h"
#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
{
	SOCKET socket;
	char recvBuffer[BUFSIZE] = {};
	int32 recvBytes = 0;
	WSAOVERLAPPED overlapped = {};
};

int main()
{
	//윈소켓 초기화(w2_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;

	//나의 주소 : IP주소 + PORT
	SOCKADDR_IN serverAddr;//IPv4
	::memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY);//아이피 알아서 설정
	serverAddr.sin_port = ::htons(7777);//PORT

	//socket과 주소 연동
	if (::bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
		return 0;

	//서버오픈
	if (::listen(listenSocket, 10) == SOCKET_ERROR)//10은 대기열 이걸넘으면 못들어옴
		return 0;

	cout << "Accept" << endl;

	//Overlapped IO 
	//Overlapped 함수를 건다 ( WSARecv, WSASend)
	//Overlapped 함수가 성공했는지 확인 후 성공했으면 결과 처리, 실패하면 사유확인
	// 비동기 입출력 지원하는 소켓 생성 + 통지 받기위한 이벤트 객체 생성
	// 비동기 입출력 함수 호출한다 이때 통지 받기위한 이벤트 객체를 함께 넣어준다.
	//비동기 방식이기 때문에 바로 실행되지 않을 수 있다. 바로 완료되지 않으면 WSA_IO_PENDING오류가 뜬다.
	//완료가된다면 운영체제는 이벤트 객체를 SIGNALED 상태로 만들어서 완료 상태를 알려준다. WSAWaitForMultipleEvent함수호출해서 판단
	//결과확인은 WSAGetOverlappedResult를 호출해서 비동기 입출력 결과 학인과 데이터를 처리한다.

	//WSASend
	//WSARecv
	//Scatter-Gather 쪼개진 버퍼를 한번에 보내준다.
	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();
		session.overlapped.hEvent = wsaEvent;

		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, nullptr) == SOCKET_ERROR)
			{
				if (::WSAGetLastError() == WSA_IO_PENDING)
				{
					//pending  지연
					::WSAWaitForMultipleEvents(1, &wsaEvent, TRUE, WSA_INFINITE, FALSE);//결과 받을 때 까지 대기
					::WSAGetOverlappedResult(session.socket, &session.overlapped, &recvLen, FALSE, &flags);
				}
				else 
				{
					//TODO : 문제 상황
				}
			}
			cout << "Data Recv Len = " << recvLen << endl;
		}
		::closesocket(session.socket);
		::WSACloseEvent(wsaEvent);
	}
	::WSACleanup();
}

Overlapped모델 (콜백기반)

모델의 동작 흐름

  1. 비동기 입출력 지원하는 소켓을 생성합니다.
  2. 비동기 입출력 함수를 호출하고 완료 루틴의 시작 주소를 넘겨줍니다.
  3. 비동기 방식이기 때문에 함수가 즉시 실행되지 않을 수 있으며, 바로 완료되지 않으면 WSA_IO_PENDING 오류가 발생합니다.
  4. 비동기 입출력 함수를 호출한 스레드를 Alertable Wait 상태로 만듭니다. 이를 위해 WaitForSingleObjectEx, WaitForMultipleObjectsEx, SleepEx, WSAWaitForMultipleEvents 등의 함수를 사용할 수 있습니다.
  5. 비동기 입출력 작업이 완료되면 운영 체제는 완료 루틴을 호출합니다.
  6. 완료 루틴이 모두 실행되면 스레드는 Alertable Wait 상태에서 빠져나옵니다.
#include "pch.h"
#include <iostream>
#include "CorePch.h"
#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
{
	WSAOVERLAPPED overlapped = {};
	SOCKET socket;
	char recvBuffer[BUFSIZE] = {};
	int32 recvBytes = 0;
};

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

	Session* session = (Session*)overlapped;
}

int main()
{
	//윈소켓 초기화(w2_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;

	//나의 주소 : IP주소 + PORT
	SOCKADDR_IN serverAddr;//IPv4
	::memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY);//아이피 알아서 설정
	serverAddr.sin_port = ::htons(7777);//PORT

	//socket과 주소 연동
	if (::bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
		return 0;

	//서버오픈
	if (::listen(listenSocket, 10) == SOCKET_ERROR)//10은 대기열 이걸넘으면 못들어옴
		return 0;

	cout << "Accept" << endl;

	//Overlapped (콜백기반)
	//비동기 입출력 지원하는 소켓 생성
	//비동기 입출력 함수 호출한다다(완료 루틴의 시작 주소를 넘겨준다.)
	//비동기 방식이기 때문에 바로 실행되지 않을 수 있다. 바로 완료되지 않으면 WSA_IO_PENDING오류가 뜬다.
	// 비동기 입출력 함수 호출한 쓰레드를 Alertable Wait 상태로 만든다.
	// ex) WaitForSingleObjectEx, WaitForMultipleObjectsEx, SleepEx, WSAWAitForMultipleEvents
	//비동기 io완료되면 운영체제는 완료 루틴 호출
	//완료 루틴 호출이 모두 끝나면 쓰레드는 Alertable Wait 상태에서 빠져나온다.

	//void CompletionRoutine()
	//1) 오류 발생시 0 아닌 값
	//2) 전송 바이트수
	//3) 비도익 입출력 함수 호출 시 넘겨준 WSAOVERLAPPED 구조체의 주소값
	//4) 0

	//select 모델
	//장점) 윈도우/리눅스 공통
	//단점) 서능 최하 매번등록해야하는 비용, 64개로 제한
	//WSAEventSelect 모델
	//장점) 비교적 뛰어난 성능(클라이언트에 적합)
	//단점) 64개로 제한
	//Overlapped (이벤트 기반)
	//장점) 성능이 좋다
	//단점) 64개 제한
	//Overlapped (콜백 기반)
	//장점) 성능이 좋다
	//단점) 모든 비동기 소켓 함수에서 사용 가능하진 않다(accept), 빈번한 Alertable Wait으로 인한 성능 저하가일어남.

	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 };
		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  지연
					::SleepEx(INFINITE, TRUE);//Alertable Wait apc 상태
				}
				else
				{
					//TODO : 문제 상황
				}
			}
			else
			{
				cout << "Data Recv Len = " << recvLen << endl;
			}
		}
		::closesocket(session.socket);
	}
	::WSACleanup();
}

 

모델 비교

Select 모델:

  • 장점:
    • 윈도우 및 리눅스와 같은 여러 플랫폼에서 사용 가능합니다.
    • 구현이 비교적 간단하고 이해하기 쉽습니다.
  • 단점:
    • 매번 소켓을 등록해야 하며, 이로 인해 오버헤드가 발생할 수 있습니다.
    • 소켓 개수에 제한이 있을 수 있습니다 (일반적으로 64개).

WSAEventSelect 모델:

  • 장점:
    • 비교적 뛰어난 성능을 제공합니다.
    • 특히 클라이언트에 적합합니다.
  • 단점:
    • 소켓 개수에 제한이 있을 수 있습니다 (일반적으로 64개).

Overlapped (이벤트 기반):

  • 장점:
    • 높은 성능을 제공합니다.
    • 비동기 작업을 효과적으로 처리할 수 있습니다.
  • 단점:
    • 소켓 개수에 제한이 있을 수 있습니다 (일반적으로 64개).

Overlapped (콜백 기반):

  • 장점:
    • 높은 성능을 제공합니다.
    • 비동기 작업 완료 후 즉시 콜백이 호출되므로 응답 시간이 빠릅니다.
  • 단점:
    • 모든 비동기 소켓 함수에서 사용할 수 없으며, 특히 accept 함수와 같은 경우에는 지원되지 않을 수 있습니다.
    • 빈번한 Alertable Wait 호출로 인한 성능 저하가 발생할 수 있습니다.
728x90