C++ select 모델 실습

정은성·2023년 5월 30일
0
post-thumbnail

강의 링크

게임 서버는 통상적으로 iocp 입출력 모델로 제작된다. 하지만 이번에 알아볼 모델은 가장 기본이 되는 select 모델이다.

Select 모델?

소켓 함수 호출이 성공할 시점을 미리 알 수 있다.

현재 문제상황은

  1. 수신 버퍼에 데이터가 없는데 Read하는 상황
  2. 송신 버퍼에 데이터가 꽉 찼는데 Write하는 상황

이다.

Select모델은 미리 관찰을 해 이런 상황을 예방하는 모델이다.

관찰을 하는 방식이기 때문에 블로킹 소켓, 논블로킹 소켓에 모두 사용가능하다.

  • 블로킹 소켓: 조건이 만족되지 않아서 블로킹 되는 상황 예방
  • 논블로킹 소켓: 조건이 만족되지 않아서 불필요하게 반복체크하는 상황을 예방

fd_set

select는 set에 있는 소켓들을 관찰한다. select를 작동하는 순서는 다음과 같다.

  1. socket set은 읽기[ ] 쓰기[ ] 예외(OOB) [ ] 관찰 대상을 등록한다.

여기서 예외(OutOfBend)란?

예외(OutOfBand)는 send() 마지막 인자 MSG_OOB로 보내는 특별한 데이터가 된다.

받는 쪽에서도 recv OOB 세팅을 해야 읽을 수 있음. 긴급상황을 알리는 등 특이한 상황에 쓴다.

  1. select(readSet,writeSet,exceptionSet) 실행 → 관찰 시작
  2. 적어도 한 개의 소켓이 준비 되면 리턴 → 준비가 안된 낙오자들은 모두 set에서 삭제됨
  3. 남은 소켓을 통해 동작 진행

관련함수

  • FD_ZERO(set): set을 비운다.
  • FD_SET(s, set): 소켓 s를 set에 넣는다.
  • FD_CLR(s, set): 소켓 s를 set에서 제거한다.
  • FD_ISSET(s, set): 소켓 s가 set에 있는지 체크한다.

구현

이를 통해 구현을 시작해보자.

select 모델은 데이터가 많다고 가정해야 보기쉽다.

Session을 정의해보자. session은 socket, 버퍼 등이 존재한다.

const int32 BUFSIZE = 1000;

struct Session {
	SOCKET socket = INVALID_SOCKET; // 소켓
	char recvBuffer[BUFSIZE] = {}; // recv된 데이터들
	int32 recvBytes = 0; // recv한 길이 -> recvBuffer에서 사용되고 있는 바이트 수
	int32 sendBytes = 0; // send한 길이 -> recvBufer에서 보낸 길이
}; 

소켓 생성

논블로킹 소켓 세팅까지의 과정이다.

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;

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;

변수 세팅

fd_set들을 정의해주자.

vector<Session> sessions; // clientSocket을 가지고 있을 session들의 묶음.
sessions.reserve(100); // 데이터가 많다고 가정. 불필요한 복사를 막기위해 저장소 100으로 설정

fd_set reads; // read할 socket들의 set
fd_set writes; // write할 socket들의 set

관찰 대상 등록

set을 초기화하는 과정이다.

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

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

	for (Session& s : sessions) {
		if (s.recvBytes <= s.sendBytes) // recv바이트가 sendbyte보다 크면 읽을 게 많다는 것. read에 등록
			FD_SET(s.socket, &reads);
		else
			FD_SET(s.socket, &writes);
	}

listenSocket의 경우 듣고 있는 것, 즉 accept 할 대상이 있는지 읽는 상황 → read에 넣어준다.

우리는 지금 에코서버를 만드는 중이므로 recvBytes가 sendBytes보다 크면 읽기만 하는 식으로 제작해준다.

관찰 시작

이제 본격적으로 select를 진행해보자.

int32 retVal = ::select(0, &reads, &writes, nullptr, nullptr);
if (retVal == SOCKET_ERROR)
	break;

파라미터의 첫 인자는 리눅스와 함수 인터페이스를 맞추기위한 것이므로 윈도우라면 신경 쓰지 않아도된다. → 0넣어주기

파라미터의 마지막인자는 timeout 인자이다.

안넣어주면 하나라도 준비된게 없으면 무한 대기한다.

사용법

	
	timeval timeout;
	timeout.tv_sec;  // 몇s
	timeout.tv_usec; // 몇ms

값을 넣어주고 마지막 인자에 넣어주면된다.

select를 실행한 후 set에 남아 있는 친구들은 준비가 완료된 친구들이다.

Accept

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 });
	}
}

readSet에 listenSocket이 있으면 accept를 진행하고, 그를 통해 Session을 만들어 sessions리스트에 넣어준다.

Recv

for (Session& s : sessions) {
		
	if (FD_ISSET(s.socket, &reads)) {
		int32 recvLen = ::recv(s.socket, s.recvBuffer, BUFSIZE, 0);
		if (recvLen <= 0) {
			// 연결이 끊어진 상태, session제거해주기
			continue;
		}
		s.recvBytes += recvLen;
	}

}

session의 recvBuffer에 온 데이터를 저장하고 recvBytes를 올려준다.

Send

if (FD_ISSET(s.socket, &writes)){
	int32 sendLen = ::send(s.socket, &s.recvBuffer[s.sendBytes], s.recvBytes - s.sendBytes, 0);
	if (sendLen == SOCKET_ERROR) {
			// 연결이 끊기
		continue;
	}
	s.sendBytes += sendLen;
	if (s.recvBytes == s.sendBytes) {
		s.recvBytes = 0;
		s.sendBytes = 0;
	}
}

블로킹 모드는 모든 데이터를 보내지만 논블로킹 모드의 경우 가끔 상대방 수신 버퍼 상황에 따라 데이터의 일부만 보낼 수 있다. 그러므로 sendLength를 정확히 받아서 늘려주자.

select모델은 내가 관찰하고 싶은 set들을 각각 만들고, 매번 루프를 돌 때 마다 초기화, 등록을 반복해야한다. 바로 select가 실행될 때 미대상자들이 알아서 제거 되기 때문이다.

장/단점

select 모델의 장점: queue를 사용할 때 pop을 바로 하지 않고 empty로 비어있는 지 검사를 하는 것 처럼, 호출 준비가 되어 있는지 한 번 검사를 하고 들어간다.

select 모델의 단점: FD_SETSIZE라는 변수를 한 번 보면 64로 되어있다. set의 최대 크기가 64라는 것이다. 검사 socket이 64개를 넘어간다면, set을 여러 개 만들어 주어야한다.. set이 늘어난 만큼 코드를 더 반복해야한다

0개의 댓글