서버/네트워크 프로그래밍

입출력모델 - Select 모델

season97 2025. 6. 12. 13:27
728x90
반응형

Select I/O 모델

  • 하나의 스레드에서 여래가의 소켓을 동시에 관리하기 위한 기술

 

 

Select 모델의 핵심 개념 "관찰"

  • 서버는 수많은 클라이언트와 통신해야 한다. 이때 어떤 클라이언트가 데이터를 보냈는지, 또는 데이터를 보내도 되는 상태인지 알수가 없다
  • Blocking 소켓의 문제점: recv() 함수를 호출했을 때 클라이언트가 데이터를 보내지 않았다면, 데이터가 올 때까지 프로그램 전체가 멈춘다.. 한 클라이언트를 기다리느라 모든 클라를 처리할수 없게 됨
  • Non-Blocking 소켓의 문제점: 무작정 recv()를 계속 호출하며 데이터가 왔는지 확인하면 CPU 자원을 많이 낭비하게됨..
  • Select 모델은 이러한 문제를 해결
    • 우리가 감시하고 싶은 소켓들의 목록(예: "데이터를 읽을 소켓들", "데이터를 쓸 소켓들")을 운영체제(OS)에 전달하고, "이 목록에 있는 소켓들하나라도 작업이 준비되면 나에게 알려줘" 라고 요청하는 방식

fd_set과 관련 함수들 : 관찰 목록 관리

  • Select 모델은 fd_set이라는 구조체를 사용해 소켓 목록을 관리한다.
  • fd_set: 소켓들의 집합(set)을 담는 비트 배열 형태의 자료구조
  • FD_ZERO(&set): set을 비워서 깨끗하게 초기화합니다. select 함수를 호출하기 전 매번 필요
  • FD_SET(socket, &set): set에 감시할 socket을 추가
  • FD_CLR(socket, &set): set에서 socket을 제거
  • FD_ISSET(socket, &set): select 함수 호출 후, set에 socket이 여전히 남아있는지(즉, 작업이 준비되었는지) 확인

전체 코드 개요

const int32 BUFSIZE = 1000;

struct Session
{
	SOCKET socket = INVALID_SOCKET;
	char recvBuffer[BUFSIZE] = {};
	int32 recvBytes = 0;
	int32 sendBytes = 0;

};

int main()
{
	WSAData wsaData;
	if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		return 0;
	SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0);
	if (listenSocket == INVALID_SOCKET)
	{
		HandleError("Socket");
		return 0;
	}
	u_long on = 1;
	if (::ioctlsocket(listenSocket, FIONBIO, &on)) //이렇게하면 논블로킹 소켓이됨
	{
		HandleError("ioctlsocket");
		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;
	

	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;
			int32 addrLen = sizeof(clientAddr);
			SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
			if (clientSocket != INVALID_SOCKET)
			{
				cout << "클라이언트 연결" << 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 : 세션에서 제거
					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();
}

 

 

흐름 분석

소켓 관찰목록 생성

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);
        }
    }
    ...
  • FD_ZERO(&reads); FD_ZERO(&writes);
     - select 함수는 호출되고 나면, 준비된 소켓만 남기고 나머지 소켓은 목록(fd_set)에서 제거 따라서 루프가 돌 때마다 관찰할 목록을 처음부터 다시 만들어야 한다.
  • FD_SET(listenSocket, &reads);
    - 서버는 항상 새로운 클라이언트의 접속을 기다려야 한다. 따라서 listenSocket은 "읽기(read)" 상태 변화(즉, 새로운 연결 요청)를 감시하기 위해 항상 reads 목록에 추가
  • for (Session& s : sessions) 루프
    if (s.recvBytes <= s.sendBytes): 받은 데이터(recvBytes)를 보낸 데이터(sendBytes)가 따라잡았다는 의미
    즉, 클라이언트로부터 받은 데이터를 모두 처리했으니, 새로운 데이터를 받을 준비가 되었다는 뜻으로, 해당 클라이언트 소켓을 reads 목록에 추가한다
    else: 아직 보내지 못한 데이터가 남아있다는 뜻으로, 클라이언트에게 데이터를 보낼(write) 준비가 되었는지 감시해야 함, 따라서 해당 클라이언트 소켓을 writes 목록에 추가

select 함수 호출

int32 retVal =  ::select(0, &reads, &writes, nullptr, nullptr);
if (retVal == SOCKET_ERROR)
    break;
  • Select 모델의 핵심 부분으로 여기서 프로그램은 잠시 대기 상태가 된다.
  • reads 목독의 소켓 중 하나라도 읽을 데이터가 생기거나 writes 목록의 소켓 중 하나라도 데이터를 쓸 수 있는공간이 생기면, select 함수는 대기를 멈추고 리턴한다.
  • retVal에는 준비된 총 개수 반환
  • 마지막 인자로 timeout 인자 설정 가능

 

결과 확인 및 작업처리

if (FD_ISSET(listenSocket, &reads))
{
    // ... accept ...
    sessions.push_back(Session{ clientSocket });
}
  • select함수가 리턴했다는건 준비된 소켓이 있다 라는 신호로 이제 어떤 소켓이 준비되었는지 확인해야 한다.
  • FD_ISSET 를 통해 listenSocket에 변화가 있었는지 확인하고, 있었다면 새 클라이언트가 접속을 시도한것이므로 accept()를 호출해 연결하고 sessions 벡터에 클라 정보를 추가한다.

요약 및 정리

  1. 준비: 감시할 소켓 목록(reads,writes)을 매번 새로 만든다. (FD_ZERO, FD_SET)
  2. 요청: select 함수를 호출하여 OS에 소켓 감시를 맡기고 대기한다.
  3. 확인: select가 리턴하면, 어떤 소켓이 준비되었는지 목록 전체를 확인한다 (FD_ISSET)
  4. 처리: 준비된 소켓에 대해서만 accept,recv,send 등의 작업을 수행한다.
  5. 반복: 1번으로 돌아가 위 과정을 반복한다.

문제점

클라 개수가 수천개이상 늘어나면, 내번 모든 세션을 순회하며 FD_ISSET으로 확인하는 과정에서 성능 저하가 발생할 수 있다라는 단점이 있다.. 이런 단점을 보완하기 위해 WSAEventSelect, IOCP와 같은 더 발전된 모델이 등장하게 되었다고 한다

728x90
반응형