블로킹 소켓을 논블로킹으로 바꾼다고 다 좋은 것이 아니라는 것을 실습을 통해 알아보았다. 제대로 완료가 되지 않은 함수를 논블로킹 방식에서 그냥 리턴을 해주어서 예외처리를 해주어야 하고 그로 인해 무한루프를 돌며 CPU를 더 잡아먹는 불편함이 있었다. 그렇다면 논블로킹 모델을 사용하되 준비된 상태를 미리 파악하게 하면 어떨까하는 생각을 할 수 있는데 이가 바로 Select 모델의 컨셉이다.
Server.cpp
#include "pch.h"
#include "CorePch.h"
#include <Windows.h>
#include "ThreadManager.h"
#include <WinSock2.h>
#include <MSWSock.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
void HandleError(const char* cause)
{
int32 errCode = ::WSAGetLastError();
cout << cause << " ErrorCode : " << errCode << endl;
}
const int32 BUFSIZE = 1000;
struct Session
{
SOCKET socket = INVALID_SOCKET;
char recvBuffer[BUFSIZE] = {};
int32 recvBytes = 0;
int32 sendBytes = 0;
};
int main()
{
// 윈속 라이브러리 초기화 (ws2_32 라이브러리 초기화)
// 관련 정보가 wsaData에 채워짐
WSAData wsaData;
if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
return 0;
// 블로킹(Blocking)
// accept -> 접속한 클라이언트가 있을 때
// connect -> 서버에 접속 성공했을 때
// send, sendto -> 요청한 데이터를 송신 버퍼에 복사했을 때
// recv, recvfrom -> 수신 버퍼에 도착한 데이터가 있고, 이를 유저레벨 버퍼에 복사했을 때
SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0); // TCP
if (listenSocket == INVALID_SOCKET)
{
HandleError("Socket");
return 0;
}
// 논블로킹(Non-Blocking)
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)
{
HandleError("Bind");
return 0;
}
if (::listen(listenSocket, SOMAXCONN) == SOCKET_ERROR)
{
HandleError("Listen");
return 0;
}
cout << "Accept" << endl;
// Select 모델(select 함수가 핵심!)
// 소켓 함수 호출이 성공할 시점을 미리 알 수 있다!
// 문제 상황)
// 수신버퍼가 비어있는데 read,
// 송신버퍼가 가득 찼는데 write
// - 블로킹 소켓 : 조건이 만족되지 않아서 블로킹되는 상황 예방
// - 논블로킹 소켓 : 조건이 만족되지 않아서 불필요하게 루프하는 상황 예방
// socket set
// 1) 읽기[ ] 쓰기[ ] 예외(OOB)[ ] 관찰대상 등록
// OutOfBand는 send() 마지막 인자를 MSG_OOB로 보내는 특별한 데이터
// 받는 쪽에서도 OOB 세팅을 해야 읽을 수 있음
// 2) select(readSet, writeSet, exceptionSet); -> 관찰 시작
// 3) 적어도 하나의 소켓이 준비되면 리턴 -> 낙오자 알아서 제거
// 4) 남은 소켓 체크해서 진행
// fd_set set;
// FD_ZERO : 비운다
// ex) FD_ZERO(set);
// FD_SET : 소켓 s를 넣는다
// ex) FD_SET(s, &set);
// FD_CLR : 소켓 s 제거
// ex) FD_CLR(s, &set);
// FD_ISSET : 소켓 s가 set에 들어있으면 0이 아닌 값 리턴
// ex) FD_ISSET(s, &set);
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 설정 가능
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)
{
// 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);
if (sendLen == SOCKET_ERROR)
{
// TODO : sessions에서 제거
continue;
}
s.sendBytes += sendLen;
if (s.recvBytes == s.sendBytes)
{
s.recvBytes = 0;
s.sendBytes = 0;
}
}
}
}
// 윈속 종료, WSAStartup이 호출된 수만큼 실행해줘야 함
::WSACleanup();
}
코드를 보면 불필요한 반복문 없이 읽고 쓸 준비가 되어있는 소켓만 골라서 데이터를 주고 받는 것을 볼 수 있다.
단점이라고 한다면 매번 set에 등록을 해주어야 한다는 것과 fd_set의 사이즈가 64밖에 되지 않아 64개 이상의 소켓을 set에 담을려면 여러개의 set을 만들어야 한다는 것 정도가 있다.
'서버 > 네트워크' 카테고리의 다른 글
8. Overlapped 모델 (이벤트 방식) (8) | 2024.10.11 |
---|---|
7. WSAEventSelect 모델 (0) | 2024.09.09 |
5. 논블로킹(Non-Blocking) 소켓 (2) | 2024.09.03 |
4. 소켓 옵션 (3) | 2024.09.01 |
3. UDP 서버 (2) | 2024.08.30 |