Select Model

Date:     Updated:

카테고리:

태그:

인프런에 있는 루키스님의 게임 서버 강의를 듣고 정리한 내용입니다.


Select Model

데이터를 주고 받기 전에 소켓의 유효성을 체크

  • 읽기[ ] 쓰기[ ] 예외[ ] 관찰 대상 등록
    • 예외(OOB)는 send 함수 마지막에 OOB 옵션 적용. recv도 OOB로만 받을 수 있음 (근데 잘 안씀)
  • select(readSet, writeSet, exceptSet, timeout) : 관찰 시작
  • Set 중 적어도 하나의 소켓이 준비되면 준비된 소켓 갯수 리턴 (낙오된 소켓을 set에서 제거됨)
  • 살아남은 소켓들로만 데이터 처리


// server side
const int32 BUFSIZE = 1000;

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

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

    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 << "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, BUFSIZE, 0);
            if (recvLen <= 0)
            {
                // 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);
            if (sendLen == SOCKET_ERROR)
            {
                // sessions 제거
                continue;
            }

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


1. FD_SET

// set 만들기 (read/write)
fd_set set;
SOCKET s;

// 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에 들어있는지 확인
ex) FD_ISSET(s, &set);


const int32 BUFSIZE = 1000;

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

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

// ...
  • 한 서버에 여러 클라가 붙을 경우 각 소켓을 세션으로 관리
  • set의 경우 소켓의 유효성 검사를 통해 while문 돌때마다 변경됨 -> 매번 초기화&등록
  • listenSocket의 경우 accept 요청 데이터를 받아와 읽어야 하므로 read set에 등록
  • 일반적인 session들은 해당 상황에 맞춰 read / write set에 등록


Select


while (true)
{
    // 소켓 셋 초기화
    // 소켓 등록

    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 << "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, BUFSIZE, 0);
            if (recvLen <= 0)
            {
                // 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);
            if (sendLen == SOCKET_ERROR)
            {
                // sessions 제거
                continue;
            }

            s.sendBytes += sendLen;
            if (s.recvBytes == s.sendBytes)
            {
                s.recvBytes = 0;
                s.sendBytes = 0;
            }
        }
    }
}
  • select(readSet, writeSet, exceptSet, timeout) : 관찰 시작 (낙오자들 set에서 제거)
  • FD_ISSET 함수를 통해 살아남은 소켓들에 대해서만 로직 처리
  • write set의 경우 상대방 상태에 따라 일부만 잘려서 보내질 수 있음 (거의 없다고는 하는데..)
    • 데이터를 전부 보냈을 때(sendBytes == sendLen) bytes 크기 초기화


Result

Pasted image 20240711013209

  • Select 모델 장점) 무작정 accept, recv하는 것이 아니라 준비된 소켓들에 대해서만 수행
  • 매번 set 등록 & 관리..
    • 단일 set에 들어갈 수 있는 소켓은 최대 64개
    • 세션이 매우 많은 경우 set 여러 개로 관리해야 함



맨 위로 이동하기

Server 카테고리 내 다른 글 보러가기

댓글 남기기