본문 바로가기
게임 서버/[Inflearn_rookiss]올인원_클라&서버 연동

12. Select 방식

by 헛둘이 2023. 12. 11.

Select 방식이란

- Select 방식이란 논 블로킹 방식 중 하나로 논 블로킹 방식의 단점인 스핀락처럼 무한히 시도하는 문제와, 이중으로 에러를 체크해줘야 하는 문제를 해결하기 위한 방법 중 하나이다.
- accept, recv와 같은 함수들을 호출하기 전에 해당 이벤트가 일어났는지를 체크하고 일어났으면 처리하는 식으로 로직이 구성되어 있다.
 
1. 읽기/쓰기/예외 중 몇 개를 관찰 대상으로 등록한다.
2. select 함수 호출 -> 관찰 시작
3. 적어도 하나의 소켓이 준비가 되면 리턴 -> 낙오자 제거
4. 남은 소켓 제거한 후 진행

/* Server.cpp */

#include "pch.h"
#include "ThreadManager.h"

const int32 BUF_SIZE = 1000;

struct Session //접속한 클라당 하나의 세션을 갖는다 
{
	SOCKET socket = INVALID_SOCKET;
	char recvBuffer[BUF_SIZE] = {}; // 모든 세션마다 고유의 버퍼가 있어야 한다
	int32 recvBytes = 0; // 버퍼를 받는 크기를 설정
};

int main()
{
	SocketUtils::Init();
	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;

	// 이전에 사용했던 주소를 사용하기 위함
	SocketUtils::SetReuseAddress(listenSocket, true);

	if (SocketUtils::BindAnyAddress(listenSocket, 7777) == false)
		return 0;

	if (SocketUtils::Listen(listenSocket) == false)
		return 0;

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

	fd_set reads;
	fd_set writes;

	while (true)
	{
		FD_ZERO(&reads);

		// Listen Socket 등록
		FD_SET(listenSocket, &reads);

		for (Session& s : sessions)
		{
			FD_SET(s.socket, &reads);
		}

		// 남을 애들만 남아라!
		int32 retVal = ::select(0, &reads, nullptr, nullptr, nullptr);
		if (retVal == SOCKET_ERROR)
			break;

		// 연결이 안된 소켓들은 reads에서 다 튕겨져나가게 됨
		// 그래서 reads에 남아있는 애들은 read를 할 수 있는 상태라는 것을 알 수 있다.

		// 남아 있는 애들에 대한 처리
		if (FD_ISSET(listenSocket, &reads))
		{
			SOCKADDR_IN clientAddr;
			int32 addrLen = sizeof(clientAddr);

			SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
			if (clientSocket != INVALID_SOCKET)
			{
				if (WSAGetLastError() == WSAEWOULDBLOCK)
					continue;

				cout << "Client Connected!" << endl;
				sessions.push_back(Session{ clientSocket });
			}

		}

		// 블로킹 방식과 다른 점은 select 호출 후 FD_ISSET의 인자로 listenSocket과 reads 안에 연결된 소켓이 있는지 검사하고 하나 이상이 연결되었다면 true를 반환
		// 그리고 기존과 마찬가지로 소켓을 생성한 후 해당 소켓을 세션에 집어넣는다.
	}

	SocketUtils::Clear();
}

- select 함수에서 받는 인자중 첫번째 인자는 무시된다.
- 두 번째 인자는 readable 여부를 확인하고자 하는 소켓들의 집합을 넘기는데, 클라이언트로부터 연결 요청이 수신되어서, accept 함수가 문제 없이 완료될 수 있는 경우, 즉 클라이언트와의 연결 요청을 처리할 수 있는 경우를 나타낸다.
- select의 반환값은 위 조건을 충족하는 소켓의 개수를 반환한다.
- FD_ISSET은 reads안의 listenSocket의 flag가 세팅되었는지를 확인하고 세팅되었다면 아래 코드들을 실행하게 된다.
- 이후 코드는 기존 서버에서 클라이언트를 받는 코드와 동일하다.
 


WSAEventSelect

- 이 방식은 Select 방식이지만 Windows에서만 동작한다는 차이점이 있다.
 

/* Server.cpp */

#include "pch.h"
#include "ThreadManager.h"

// WSAEventSelect = WSAEventSelect가 메인
// 소켓과 관련된 네트워크 이벤트를 [이벤트 객체]를 통해 감지

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


const int32 BUF_SIZE = 1000;

struct Session //접속한 클라당 하나의 세션을 갖는다 
{
	SOCKET socket = INVALID_SOCKET;
	char recvBuffer[BUF_SIZE] = {}; // 모든 세션마다 고유의 버퍼가 있어야 한다
	int32 recvBytes = 0; // 버퍼를 받는 크기를 설정
};

int main()
{
	SocketUtils::Init();
	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;

	// 이전에 사용했던 주소를 사용하기 위함
	SocketUtils::SetReuseAddress(listenSocket, true);

	if (SocketUtils::BindAnyAddress(listenSocket, 7777) == false)
		return 0;

	if (SocketUtils::Listen(listenSocket) == false)
		return 0;

	// WSAEVENT 구조체를 담는 벡터를 만든다
	vector<WSAEVENT> wsaEvents;

	// 클라이언트당 하나씩 부여하는 세션이라는 구조체를 담는 벡터를 만든다.
	vector<Session> sessions;
	sessions.reserve(100);

	// 서버쪽 이벤트를 하나 만들어서 wsaEvents 벡터에 담는다.
	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);
		if (index == WSA_WAIT_FAILED)
			continue;

		index -= WSA_WAIT_EVENT_0; // 이렇게 하면 이벤트가 일어난 소켓의 인덱스 위치를 알 수 있다.

		// 구체적인 네트워크 이벤트 알아내기
		WSANETWORKEVENTS networkEvents;
		if (WSAEnumNetworkEvents(sessions[index].socket, wsaEvents[index], &networkEvents) == SOCKET_ERROR)
			continue;

		// accept 이벤트가 발생했는지 체크
		if (networkEvents.lNetworkEvents & FD_ACCEPT) // 해당 이벤트가 발생했는지 비트플래그로 체크
		{
			// msdn과 맞게 2차 체크
			if (networkEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
				continue;

			SOCKADDR_IN clientAddr;
			int32 addrLen = sizeof(clientAddr);

			// 여기까지 들어왔다는건 accept가 일어났다는걸 사전에 인지한 상태였기 때문에 블로킹되는 상태가 발생하지 않는다.
			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;
			}
		}

		// recv 이벤트가 발생했는지 체크
		if (networkEvents.lNetworkEvents & FD_READ)
		{
			// msdn에 맞게 2차 체크
			if (networkEvents.iErrorCode[FD_READ_BIT] != 0)
				continue;

			Session& s = sessions[index];

			// 여기서의 recv도 recv가 발생했다는 이벤트를 받고 분기에 들어온 것이기 때문에 블로킹되지 않는다.
			int32 recvLen = recv(s.socket, s.recvBuffer, BUF_SIZE, 0);
			if (recvLen == SOCKET_ERROR && WSAGetLastError() != WSAEWOULDBLOCK)
			{
				if (recvLen <= 0)
					continue;
			}

			cout << "Recv Data : " << s.recvBuffer << endl;
			cout << "Recv Len  : " << recvLen << endl;
		}
	}

	SocketUtils::Clear();
}

 

'게임 서버 > [Inflearn_rookiss]올인원_클라&서버 연동' 카테고리의 다른 글

14. IOCP  (0) 2023.12.11
13. Overlapped 방식  (0) 2023.12.11
11. 논 블로킹 소켓  (1) 2023.12.11
10. TCP vs UDP  (0) 2023.12.11
9. 소켓 프로그래밍  (0) 2023.12.11

댓글