| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | |||||
| 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 10 | 11 | 12 | 13 | 14 | 15 | 16 |
| 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| 24 | 25 | 26 | 27 | 28 | 29 | 30 |
| 31 |
Tags
- JavaScript
- PROJECT
- HTTP
- trie
- SCC
- Binary Lifting
- 2-SAT
- 그래프 탐색
- 이분 탐색
- Github
- map
- Prisma
- Behavior Design Pattern
- 트라이
- 자바스크립트
- reference counting
- 강한 연결 요소
- Overlapped Model
- Lock-free Stack
- 비트필드를 이용한 dp
- 최소 공통 조상
- Delete
- 비트마스킹
- Strongly Connected Component
- 게임 서버 아키텍처
- 벨만-포드
- ccw 알고리즘
- DP
- Spin Lock
- select 모델
Archives
- Today
- Total
dh_0e
[C++/Game Server] Select Model 본문
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 과정
- socket set에 읽기, 쓰기, 예외(OOB) 관찰 대상 등록
- OOB(Out of Band): send() 마지막 인자를 MSG_OOB로 설정하면 특별한 데이터가 됨
- 받는 쪽에서도 recv OOB 세팅을 해야 읽을 수 있음
- 긴급 상황 or 특이한 상황을 알리는데 사용됨
- select(readSet, writeSet, exceptSet, nullptr); 안 쓸거면 null 포인터 넣어도 됨
- 관찰 시작
- select(): 적어도 하나의 소켓이 준비되면 읽기 준비된 소켓의 개수가 return 됨, 낙오자는 알아서 제거됨
- ex) 읽기[ 1 2 3 ] 소켓중에 2의 recvBuffer에 데이터가 들어왔을 경우
: select가 읽기 준비된(recvBuffer에 데이터가 있는) 소켓의 개수(1)를 return 해주고, 낙오자 소켓(1, 3)들을 알아서 제거해줌
- ex) 읽기[ 1 2 3 ] 소켓중에 2의 recvBuffer에 데이터가 들어왔을 경우
- select에 남은 소켓을 대상으로 읽기를 시도하면, 읽기 준비가 된 소켓들만 읽기를 시도할 수 있음
- socket set에 읽기, 쓰기, 예외(OOB) 관찰 대상 등록
Select 모델 명령어

- 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이 필요함
'C++ > Game Server' 카테고리의 다른 글
| [C++/Game Server] Overlapped Model (Event 기반) (0) | 2026.03.24 |
|---|---|
| [C++/Game Server] WSAEventSelect Model (0) | 2026.03.23 |
| [C++/Game Server] Non-Blocking Socket (0) | 2026.03.22 |
| [C++/Game Server] Socket Option (setsockopt, getsockopt) (0) | 2026.03.22 |
| [C++/Game Server] UDP Server 실습 (0) | 2026.03.21 |
