dh_0e

[C++/Game Server] Select Model 본문

C++/Game Server

[C++/Game Server] Select Model

dh_0e 2026. 3. 22. 17:48

Select 모델

  • Select 함수가 핵심이 되는 모델로, 다양한 입출력 모델 중 가장 기초가 되는 모델
  • Windows, Linux에 모두 존재함
  • 소켓 함수 호출이 성공할 시점을 미리 알 수 있게됨
    • queue를 pop()할 때 if(!que.empty()) que.pop(); 이렇게 체크하는 것과 비슷하게 소켓이 send()나 recv() 준비가 되었는지 체크할 수 있음
  • Blocking, Non-Blocking 상관 없이 모두 쓸 수 있음
  • Problem1) 수신 버퍼에 데이터가 없는데, read 할 경우
    Problem2) 송신 버퍼가 꽉 찼는데, write 할 경우
    • 블로킹 소켓: 조건이 만족되지 않아서 블로킹되는 상황 예방
    • 논블로킹 소켓: 조건이 만족되지 않아서 불필요하게 반복 체크하는 상황을 예방
  • 게임 서버에선 통상적으로 IOCP 모델을 사용하지만, Select 모델도 중요함
    • 클라이언트는 굳이 IOCP를 쓸만큼 성능의 필요가 없어 Select 모델을 쓰기도 함
  • Select 과정
    1. socket set에 읽기, 쓰기, 예외(OOB) 관찰 대상 등록
      • OOB(Out of Band): send() 마지막 인자를 MSG_OOB로 설정하면 특별한 데이터가 됨
      • 받는 쪽에서도 recv OOB 세팅을 해야 읽을 수 있음
      • 긴급 상황 or 특이한 상황을 알리는데 사용됨
    2. select(readSet, writeSet, exceptSet, nullptr); 안 쓸거면 null 포인터 넣어도 됨
      • 관찰 시작
    3. select(): 적어도 하나의 소켓이 준비되면 읽기 준비된  소켓의 개수가 return 됨, 낙오자는 알아서 제거됨
      • ex) 읽기[ 1 2 3 ] 소켓중에 2의 recvBuffer에 데이터가 들어왔을 경우
        : select가 읽기 준비된(recvBuffer에 데이터가 있는) 소켓의 개수(1)를 return 해주고, 낙오자 소켓(1, 3)들을 알아서 제거해줌
    4. select에 남은 소켓을 대상으로 읽기를 시도하면, 읽기 준비가 된 소켓들만 읽기를 시도할 수 있음

 

Select 모델 명령어

Structure fd_set

  • FD_ZERO: set 초기화
    • ex) FD_ZERO(set);
  • FD_SET: 소켓 s를 넣는다
    • ex) FD_SET(s, &set);
  • FD_CLR: 소켓 s를 제거
    • ex) FD_CLR(s, &set);
  • FD_ISSET: 소켓 s가 set에 있는지 확인 (있으면 true, 없으면 false)
    • ex) FD_ISSET(s, &set);

 

GameServer.cpp

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <atomic>
#include <mutex>
#include <Windows.h>
#include <future>
#include "ThreadManager.h"

#include <urlmon.h>
#include <winSock2.h>
#include <mswsock.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

void HandleError(const char* funcName) {
	int32 errCode = ::WSAGetLastError();
	cout << "Causon Func: " << funcName << endl << "socket error : " << errCode << endl;
}

const int32 BUFFER_SIZE = 1000;

struct Session {
	SOCKET socket;
	char recvBuffer[BUFFER_SIZE] = {};
	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)
		return 0;

	u_long on = 1;
	if (::ioctlsocket(listenSocket, FIONBIO, &on) == SOCKET_ERROR)
		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;

	// Session을 만들어 클라이언트들을 관리한다 가정
	vector<Session> sessions;
	sessions.reserve(100);	// 불필요한 복사를 막기 위해 미리 공간을 할당

	fd_set reads, writes;

	while (true) {
		// 소켓 셋 초기화
		FD_ZERO(&reads);
		FD_ZERO(&writes);
		// 낙오자 소켓은 모두 제외되기 때문에 Loop 돌 때마다 초기화하고 소켓들을 다시 넣어줘야 함

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

		// Session 등록
		for (Session& s : sessions) {
			// 에코 서버이기 때문에 read or write 중 하나만 등록한 것임
			if (s.recvBytes <= s.sendBytes)
				FD_SET(s.socket, &reads);
			else
				FD_SET(s.socket, &writes);
		}

		int retVal = ::select(0, &reads, &writes, nullptr, nullptr);
		// timeout: timeout 설정 가능 (준비된 소켓이 없을 경우 무한 대기하지 않게 time 설정)

		if (retVal == SOCKET_ERROR)
			break;

		if (FD_ISSET(listenSocket, &reads)) {
			SOCKADDR_IN clientAddr;
			int32 addrLen = sizeof(clientAddr);
			SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
			if (clientSocket != INVALID_SOCKET) {
				cout << "New 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, BUFFER_SIZE, 0);
				cout << "Recv Len: " << recvLen << endl;
				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);
				// 블로킹 모드였으면 기본적으로 모든 데이터를 보내지만, 논-블로킹 모드는 상대방 수신 버퍼 상황에 따라 일부만 보낼 수 있음을 방지
				// 9 바이트(0~8)를 recv 했는데, 이전에 보낸 sendBytes가 5(0~4)면 5(s.recvBuffer[s.sendBytes])부터 4(s.recvBytes - s.sendBytes == 9 - 5)만큼의 데이터(5~8)를 send 함
                
				cout << "Send Len: " << sendLen << endl;
                
				if (sendLen == SOCKET_ERROR) {
					// TODO: 클라이언트(sessions에서) 제거
					continue;
				}

				s.sendBytes += sendLen;
				if (s.recvBytes == s.sendBytes) {
					s.recvBytes = 0;
					s.sendBytes = 0;
				}
			}
		}
	}

	// ---------------------------------------

	// 윈속 종료
	::WSACleanup();
	return 0;
}
  • TODO: 클라이언트(sessions에서) 제거
    • 클라이언트가 연결을 종료했으므로, sessions.erase() 등을 통해 죽은 소켓을 제외하지 않으면 select에 죽은 소켓들이 돌아다녀 에러를 유발할 수 있음

  • 장점
    • 간단하게 구현이 가능하며, 낭비하는 CPU 자원이 적음
    • 윈도우/리눅스 모두 가능하여 크로스 플렛폼을 지원할 수 있음
  • 단점
    • 매번 반복하며 set을 초기화하고 소켓을 등록하는 것이 한계가 있음
    • FD_SET의 크기가 생각보다 작음(단일 64개가 한계)
      • 640개 단일 클라이언트들을 관리하려면 10개의 set이 필요함