서버/네트워크

6. Select 모델

광란의슈가슈가룬 2024. 9. 5. 20:21

블로킹 소켓을 논블로킹으로 바꾼다고 다 좋은 것이 아니라는 것을 실습을 통해 알아보았다. 제대로 완료가 되지 않은 함수를 논블로킹 방식에서 그냥 리턴을 해주어서 예외처리를 해주어야 하고 그로 인해 무한루프를 돌며 CPU를 더 잡아먹는 불편함이 있었다. 그렇다면 논블로킹 모델을 사용하되 준비된 상태를 미리 파악하게 하면 어떨까하는 생각을 할 수 있는데 이가 바로 Select 모델의 컨셉이다.

 

Server.cpp

#include "pch.h"
#include "CorePch.h"
#include <Windows.h>
#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 = INVALID_SOCKET;
	char recvBuffer[BUFSIZE] = {};
	int32 recvBytes = 0;
	int32 sendBytes = 0;
};

int main()
{
	// 윈속 라이브러리 초기화 (ws2_32 라이브러리 초기화)
	// 관련 정보가 wsaData에 채워짐
	WSAData wsaData;
	if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		return 0;

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

	
	SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0); // TCP
	if (listenSocket == INVALID_SOCKET)
	{
		HandleError("Socket");
		return 0;
	}

	// 논블로킹(Non-Blocking)
	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)
	{
		HandleError("Bind");
		return 0;
	}

	if (::listen(listenSocket, SOMAXCONN) == SOCKET_ERROR)
	{
		HandleError("Listen");
		return 0;
	}

	cout << "Accept" << endl;

	// Select 모델(select 함수가 핵심!)
	// 소켓 함수 호출이 성공할 시점을 미리 알 수 있다!
	// 문제 상황)
	// 수신버퍼가 비어있는데 read,
	// 송신버퍼가 가득 찼는데 write
	// - 블로킹 소켓 : 조건이 만족되지 않아서 블로킹되는 상황 예방
	// - 논블로킹 소켓 : 조건이 만족되지 않아서 불필요하게 루프하는 상황 예방

	// socket set
	// 1) 읽기[ ] 쓰기[ ] 예외(OOB)[ ] 관찰대상 등록
	// OutOfBand는 send() 마지막 인자를 MSG_OOB로 보내는 특별한 데이터
	// 받는 쪽에서도 OOB 세팅을 해야 읽을 수 있음
	// 2) select(readSet, writeSet, exceptionSet); -> 관찰 시작
	// 3) 적어도 하나의 소켓이 준비되면 리턴 -> 낙오자 알아서 제거
	// 4) 남은 소켓 체크해서 진행

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

	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 설정 가능
		int32 retVal = ::select(0, &reads, &writes, nullptr, nullptr);
		if (retVal == SOCKET_ERROR)
			break;
		
		// Listener 소켓 체크
		if (FD_ISSET(listenSocket, &reads))
		{
			SOCKADDR_IN 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 });
			}
		}

		// 나머지 소켓 체크
		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;
				}
			}
		}

	}

	// 윈속 종료, WSAStartup이 호출된 수만큼 실행해줘야 함
	::WSACleanup();
}

코드를 보면 불필요한 반복문 없이 읽고 쓸 준비가 되어있는 소켓만 골라서 데이터를 주고 받는 것을 볼 수 있다.

 

단점이라고 한다면 매번 set에 등록을 해주어야 한다는 것과 fd_set의 사이즈가 64밖에 되지 않아 64개 이상의 소켓을 set에 담을려면 여러개의 set을 만들어야 한다는 것 정도가 있다.

'서버 > 네트워크' 카테고리의 다른 글

8. Overlapped 모델 (이벤트 방식)  (8) 2024.10.11
7. WSAEventSelect 모델  (0) 2024.09.09
5. 논블로킹(Non-Blocking) 소켓  (2) 2024.09.03
4. 소켓 옵션  (3) 2024.09.01
3. UDP 서버  (2) 2024.08.30