서버/네트워크 프로그래밍
입출력모델 - 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 벡터에 클라 정보를 추가한다.
요약 및 정리
- 준비: 감시할 소켓 목록(reads,writes)을 매번 새로 만든다. (FD_ZERO, FD_SET)
- 요청: select 함수를 호출하여 OS에 소켓 감시를 맡기고 대기한다.
- 확인: select가 리턴하면, 어떤 소켓이 준비되었는지 목록 전체를 확인한다 (FD_ISSET)
- 처리: 준비된 소켓에 대해서만 accept,recv,send 등의 작업을 수행한다.
- 반복: 1번으로 돌아가 위 과정을 반복한다.
문제점
클라 개수가 수천개이상 늘어나면, 내번 모든 세션을 순회하며 FD_ISSET으로 확인하는 과정에서 성능 저하가 발생할 수 있다라는 단점이 있다.. 이런 단점을 보완하기 위해 WSAEventSelect, IOCP와 같은 더 발전된 모델이 등장하게 되었다고 한다
728x90
반응형