VisualStudio/C++서버

[C++] Select 모델과 WASEventSelect모델

usingsystem 2024. 5. 23. 16:24
728x90

Select 모델 (동기방식)

Select 모델은 여러 소켓에 대한 입출력 이벤트를 감지하고 처리할 수 있도록 도와줍니다.

Select 모델은 여러 소켓의 상태를 감지하고, 해당 소켓들 중에서 입력이나 출력 준비가 된 소켓을 선택하여 처리할 수 있도록 해줍니다. 이는 비동기 소켓 프로그래밍의 복잡성을 줄이면서도 다중 클라이언트를 처리하는 데 효과적입니다. select() 함수를 사용하여 소켓의 상태를 검사하고, 필요한 작업을 수행합니다.

Select 모델은 단일 스레드에서 여러 소켓을 처리하기 때문에 비교적 간단하게 구현할 수 있으며, 블로킹 소켓을 사용하더라도 다중 클라이언트를 동시에 처리할 수 있습니다. 그러나 매우 많은 수의 소켓을 다루는 경우에는 비효율적일 수 있습니다. 이런 경우에는 다른 모델이나 비동기 소켓을 고려해야 할 수도 있습니다.

 

Select모델의 핵심은 소켓 함수 호출이 성공할 시점을 미리 알 수 있다는 것이다.

예를 들어 수신버퍼에 데이터가 없는데 read를 할 때, 송신버퍼가 꽉 찼는데 write를 할 때 블로킹이라면 대기를 하게 된다.

 

Select모델을 블로킹 소켓에 적용한다면 조건이 만족되지 않아서 블로킹되는 상황을 예방할 수 있고

논블로킹 소켓에서는 조건이 만족되지 않아서 불필요하게 반복 체크하는 상황을 예방할 수 있다.

 

1. Select모델은 읽기, 쓰기, 예외에 대한 관찰 대상을 등록한다.

2. select(readSet, writeSet, exceptSet);

3. 적어도 하나의 소켓이 준비되면 리턴하고 낙오자는 알아서 제거된다.

4. 남은 소켓 체크해서 진행

 

select() 함수와 함께 사용되는 매크로에는 FD_ZERO, FD_SET, FD_CLR, FD_ISSET 가 있다. 이들은 파일 디스크립터 세트를 초기화, 설정, 해제 및 테스트하는 데 사용됩니다. 

  1. FD_ZERO(fd_set *set): 주어진 파일 디스크립터 세트(set)를 초기화합니다. 이는 모든 파일 디스크립터를 비활성화하는 데 사용됩니다.
  2. FD_SET(int fd, fd_set *set): 주어진 파일 디스크립터를 파일 디스크립터 세트(set)에 추가합니다. 이는 해당 파일 디스크립터가 활성화되었음을 나타냅니다.
  3. FD_CLR(int fd, fd_set *set): 주어진 파일 디스크립터를 파일 디스크립터 세트(set)에서 제거합니다. 이는 해당 파일 디스크립터를 비활성화하는 데 사용됩니다.
  4. FD_ISSET(int fd, fd_set *set): 주어진 파일 디스크립터가 파일 디스크립터 세트(set)에 있는지 여부를 확인합니다. 이 함수는 주어진 파일 디스크립터가 활성화되었는지를 테스트하는 데 사용됩니다.
#include <iostream>
#include <windows.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;
	int32 sendBytes = 0;
};

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;

	//fd_set set;
	//FD_ZERO(set) : 초기화(비운다)
	//FD_SET(s, &set) : 소켓 s를 넣는다
	//FD_CLR(s, &set) : 소켓 s를 제거
	//FD_ISSET : 소켓 s가 set에 들어있으면 0이 아닌 값을 리턴한다. 

	vector<Session> sessions;
	sessions.reserve(100);

	fd_set reads;
	fd_set writes;

	while (true)
	{
		//소켓 셋 초기화
		FD_ZERO(&reads);
		FD_ZERO(&writes);

		//ListenSocket 등록
		FD_SET(listenSocket, &reads);

		//소켓 등록
		for (Session& s : sessions)
		{
			if (s.recvBytes <= s.sendBytes) 
				FD_SET(s.socket, &reads);
			else
				FD_SET(s.socket, &writes);
		}

		//옵션 마지막 timeout 인자 설정 가능
	/*	timeval timeout;
		timeout.tv_sec;
		timeout.tv_usec;*/
		int32 retVal = ::select(0, &reads, &writes, nullptr, nullptr);//관찰시작 낙오자 제거
		if (retVal == SOCKET_ERROR)
			break;

		//Listener 소켓 체크
		if (FD_ISSET(listenSocket, &reads))
		{
			SOCKADDR_IN clientAddr;//IPv4
			::memset(&clientAddr, 0, sizeof(clientAddr));
			int32 addrLen = sizeof(clientAddr);
			SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);

			if (clientSocket != INVALID_SOCKET)
			{
				cout << "client connected" << endl;
				sessions.push_back(Session{ clientSocket });
			}
		}

		//나머지 소켓 체크 read나 write
		for (Session& s : sessions )
		{
			//read 체크
			if (FD_ISSET(s.socket, &reads)) 
			{
				int32 recvLen = ::recv(s.socket, s.recvBuffer, BUFSIZE, 0);
				if (recvLen <= 0)
				{
					//TODO : SESSIONS 제거 
					continue;
				}

				s.recvBytes = recvLen;
			}

			//write 체크
			if (FD_ISSET(s.socket, &writes))
			{
				int32 sendLen = ::send(s.socket, &s.recvBuffer[s.sendBytes],s.recvBytes - s.sendBytes, 0);
				if (sendLen == SOCKET_ERROR)
				{
					//TODO : SESSIONS 제거 
					continue;
				}
				s.sendBytes += sendLen;
				if (s.recvBytes == s.sendBytes)
				{
					s.recvBytes = 0;
					s.sendBytes = 0;
				}
			}
		}
	}
	::WSACleanup();
}

WASEventSelect ( 비동기 방식 )

Select 모델과 다르게 전체리 셋을 매번 해줄 필요가 없다는 장점이 있다.

WSAEventSelect함수를 호출하면 해당 소켓은 자동으로 넌블로킹 모드로 전환된다.
accept() 함수가 리턴하는 소켓은 listenSocket과 동일한 속성을 갖는다. 따라서 clientsocket은 FD_READ, FD_WRITE 등을 다시 등록할 필요가 있다.
드물게 WSAEWOULDBLOCK 오류가 뜰 수 있으니 예외 처리 필요하다.
중요)
- 이벤트 발생 시적 절한 소켓 함수 호출해야 함. 아니면 다음번에는 동일 네트워크 이벤트가 발생하지 않는다.
EX) FD_READ이벤트 떴으면 recv() 호출해야 하고 안 하면 FD_READ 두 번 다시 X

1. WSAEventSelect

int WSAEventSelect(SOCKET s, WSAEVENT hEventObject, long lNetworkEvents);

  • 특정 소켓에서 발생하는 네트워크 이벤트(FD_READ, FD_WRITE, FD_CONNECT 등)를 비동기적으로 감지합니다.
  • 지정된 이벤트가 발생하면 연관된 이벤트 객체가 신호 상태로 설정됩니다.
  • 소켓 개수 만큼 이벤트 객체에 연동해야 한다.

FD_ACCEPT : 접속한 클라이언트가 있는지 accept호출
FD_READ : 데이터 수신가능 recv, recvfrom
FD_WRITE : 데이터 송신 가능 send, sendto
FD_CONNECT : 통신을 위한 연결 절차 완료 확인

2. WSACreateEvent(생성)

WSAEVENT WSACreateEvent(void);

  • 새로운 이벤트 객체를 생성합니다.
  • 이 객체는 이후에 소켓과 연관 지어 네트워크 이벤트를 모니터링하는 데 사용됩니다.
  • 이벤트 객체는 초기 상태가 비신호 상태이며, 특정 이벤트가 발생하면 신호 상태로 변경됩니다.

3. WSACloseEvent(삭제)

BOOL WSACloseEvent(WSAEVENT hEvent);

  • 사용이 끝난 이벤트 객체를 닫고 자원을 해제합니다.
  • 메모리 누수를 방지하고 시스템 자원을 효율적으로 관리할 수 있게 합니다.
  • 다른 함수들(WSEventSelect, WSAWaitForMultipleEvents 등)과 연동되어 동작하는 이벤트 객체를 적절하게 정리할 수 있습니다.

4. WSAWaitForMultipleEvents(신호 상태 감지)

DWORD WSAWaitForMultipleEvents(DWORD cEvents, const WSAEVENT* lphEvents, BOOL fWaitAll, DWORD dwTimeout, BOOL fAlertable);

  • 다수의 이벤트 객체가 신호 상태가 될 때까지 대기합니다.
  • fWaitAll 매개변수를 통해 모든 이벤트가 신호 상태가 될 때까지 대기하거나, 하나의 이벤트가 신호 상태가 되면 반환할 수 있습니다.
  • dwTimeout을 통해 대기 시간을 설정할 수 있어 무한 대기 또는 제한된 시간 동안만 대기하도록 제어할 수 있습니다.

5. WSAEnumNetworkEvents(구체적인 네트워크 이벤트 알아내기)

int WSAEnumNetworkEvents(SOCKET s, WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvents);

  • 특정 소켓과 연관된 네트워크 이벤트들을 열거합니다.
  • 발생한 이벤트의 종류를 확인하고 해당 이벤트에 대해 적절한 처리를 할 수 있도록 합니다.
  • 이벤트 객체의 상태를 리셋하여 다음 이벤트를 감지할 준비를 합니다.

 

  • WSAEventSelect: 소켓 이벤트를 비동기적으로 감지하고 이벤트 객체를 신호 상태로 설정합니다.
  • WSACreateEvent: 비동기 이벤트 처리를 위한 새로운 이벤트 객체를 생성합니다.
  • WSACloseEvent: 더 이상 필요하지 않은 이벤트 객체를 닫아 자원을 해제합니다.
  • WSAWaitForMultipleEvents: 여러 이벤트 객체 중 하나 또는 모두가 신호 상태가 될 때까지 대기합니다.
  • WSAEnumNetworkEvents: 특정 소켓과 연관된 발생한 네트워크 이벤트를 확인하고 처리합니다.
#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;
	int32 sendBytes = 0;
};

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;

	//WASEventSelect = 소켓과 관련된 네트워크 이벤트 객체를 통해 감지(비동기) 

	//이벤트 객체 관련 함수
	//생성 : WSACreateEvent( 수동리셋 Manual-Reset + Non-Signaled 상태 시작)
	//삭제 : WSACloseEvent
	//신호 상태 감지 : WSAWaitForMultipleEvents)
	//구체적인 네트워크 이벤트 알아내기 : WSAEnumNetworkEvents

	//소켓 개수 만큼 이벤트 객체 연동해야함.
	//WSAEventSelect(socket, event, networkEvents);
	//-관심있는 네트워크 이벤트
	//FD_ACCEPT : 접속한 클라이언트가 있는지 accept호출
	//FD_READ : 데이터 수신가능 recv, recvfrom
	//FD_WRITE : 데이터 송신 가능 send, sendto
	//FD_CONNECT : 통신을 위한 연결 절차 완료 확인

	//주의사항
	//WSAEventSelect함수를 호출하면 해당 소켓은 자동으로 넌블로킹 모드로 전환
	//accept() 함수가 리턴하는 소켓은 listenSocket과 동일한 속성을 갖는다. 따라서 clientsocket은 FD_READ, FD_WRITE등을 다시 등록할 필요가 있다.
	//드물게 WSAEWOULDBLOCK 오류가 뜰 수 있으니 예외 처리 필요
	//중요)
	//- 이벤트 발생 시적절한 소켓 함수 호출해야함.
	//- 아니면 다음 번에는 동일 네트워크 이벤트가 발생하지 않는다.
	//EX) FD_READ이벤트 떴으면 recv() 호출해야하고 안하면 FD_READ 두 번 다시 X

	//WSAWaitForMultipleEvent
	// 1) count, event
	// 2) waitAll : 모두 기다리거나 하나만 완료되어도 빠져나간다.
	// 3) timeout : 타임아웃
	// 4) 지금은 false
	// 5) return : 완료된 첫번째 인덱스

	vector<WSAEVENT> wsaEvents;
	vector<Session> sessions;
	sessions.reserve(100);

	WSAEVENT listenEvent = ::WSACreateEvent();//이벤트생성
	wsaEvents.push_back(listenEvent);
	sessions.push_back(Session{ listenSocket });//인덱스 맞추려고
	if (::WSAEventSelect(listenSocket, listenEvent, FD_ACCEPT | FD_CLOSE) == SOCKET_ERROR)//이벤트와 소켓 연결
		return 0;

	while (true)
	{
		int32 index = ::WSAWaitForMultipleEvents(wsaEvents.size(), &wsaEvents[0], FALSE, WSA_INFINITE, FALSE);//3번쨰인자 하나만 완료되어도 빠져나가게 설정
		if (index == WSA_WAIT_FAILED)
			continue;

		index -= WSA_WAIT_EVENT_0;

		//::WSAResetEvent(wsaEvents[index]);//WSAEnumNetworkEvents를 사용하면 자동으로 reset해줌

		WSANETWORKEVENTS networkEvents;//WSAEnumNetworkEvents의 결과물을 받을 구조체
		if (::WSAEnumNetworkEvents(sessions[index].socket, wsaEvents[index], &networkEvents) == SOCKET_ERROR)//관찰하려는 이벤트
			continue;

		//Listener 소켓 체크
		if (networkEvents.lNetworkEvents & FD_ACCEPT)
		{
			//Error-check
			if (networkEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
				continue;

			SOCKADDR_IN clientAddr;
			int32 addrLen = sizeof(clientAddr);

			SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
			if (clientSocket != INVALID_SOCKET)
			{
				cout << "client Connected" << endl;

				WSAEVENT clientEvent = ::WSACreateEvent();
				wsaEvents.push_back(clientEvent);
				sessions.push_back(Session{ clientSocket });
				if (::WSAEventSelect(clientSocket, clientEvent, FD_READ | FD_WRITE | FD_CLOSE) == SOCKET_ERROR)//이벤트와 소켓 연결
					return 0;
			}
		}

		//Client 소켓 체크
		if (networkEvents.lNetworkEvents & FD_READ || networkEvents.lNetworkEvents & FD_WRITE)
		{
			//Error-check
			if ((networkEvents.lNetworkEvents & FD_READ) && (networkEvents.iErrorCode[FD_READ_BIT] != 0))
				continue;
			//Error-check
			if ((networkEvents.lNetworkEvents & FD_WRITE) && (networkEvents.iErrorCode[FD_WRITE_BIT] != 0))
				continue;

			Session& s = sessions[index];

			//read
			if (s.recvBytes == 0)
			{
				int32 recvLen = ::recv(s.socket, s.recvBuffer, BUFSIZE, 0);
				if (recvLen == SOCKET_ERROR && ::WSAGetLastError() != WSAEWOULDBLOCK)
				{
					//TODO : 오류 SESSIONS 제거 
					continue;
				}

				s.recvBytes = recvLen;
				cout << "recv data = " << recvLen << endl;
			}

			if (s.recvBytes > s.sendBytes)
			{
				int32 sendLen = ::send(s.socket, &s.recvBuffer[s.sendBytes], s.recvBytes - s.sendBytes, 0);
				if (sendLen == SOCKET_ERROR && ::WSAGetLastError() != WSAEWOULDBLOCK)
				{
					//TODO : 오류 SESSIONS 제거 
					continue;
				}

				s.sendBytes += sendLen;

				if (s.recvBytes == s.sendBytes)
				{
					s.recvBytes = 0;
					s.sendBytes = 0;
				}
				cout << "send data = " << sendLen << endl;
			}
		}

		//FD_CLOSE 처리
		if (networkEvents.lNetworkEvents & FD_CLOSE)
		{
			//TODO : SESSION 제거
		}
	}
	::WSACleanup();
}
728x90