Non-blocking 소켓을 사용하되, 준비되었을 때를 미리 파악할 수 있도록 하자!
Multiplexing I/O Model
select 모델은 Multiplexing I/O 모델로 분류된다.
Multiplexing 모델은 먼저 소켓 리스트들을 등록하고, 등록된 소켓 리스트들 중에 send나 recv와 같은 I/O 작업이 수행 가능한 준비 상태의 소켓들을 알려주는 API를 제공한다.
I/O가 가능한 소켓에 대해서만 I/O 함수를 호출하기 때문에 스레드가 blocking되지 않고 실행 결과를 바로 받을 수 있다.
따라서, 적은 수의 스레드로도 다중 접속을 처리할 수 있다.
Multiplexing I/O Model에는 select, poll, epoll이 있다.
이 중 windows와 linux 두 운영체제 모두에서 사용가능한 것이 select 모델이다.
Select I/O Model
Select I/O 모델은 select 함수가 핵심이 되는 모델이다.
select 함수를 통해 소켓 함수 호출이 성공할 시점을 미리 알 수 있다.
select 함수가 소켓이 send, recv를 할 수 있는지 체크해준다.
select 모델은 blocking socket, non-blocking socket 각각이 가진 문제점을 해결해준다.
기존 socket 프로그래밍의 경우 다음과 같은 문제 상황이 나타날 수 있다.
1. 수신 버퍼에 데이터가 없는데, read를 하는 경우
2. 송신 버퍼가 꽉 찼는데, write 하는 경우
이 경우 select 함수를 사용하면 문제가 해결된다.
blocking socket의 경우에는 조건이 만족되지 않아서 블로킹되는 상황을 예방할 수 있고,
non-blocking socket의 경우에는 조건이 만족되지 않아서 불필요하게 반복 체크해야 하는 상황을 방지할 수 있다.
select 함수는 동기방식의 함수다.
따라서, 결과물이 나올 때까지 대기 후, 결과물이 하나라도 나오면 리턴한다.
select 함수는 다음과 같다.
int WSAAPI select(
[in] int nfds,
[in, out] fd_set *readfds,
[in, out] fd_set *writefds,
[in, out] fd_set *exceptfds,
[in] const timeval *timeout
);
select 함수에 fd_set들을 인자로 전달해야 한다.
fd_set를 통해 select함수와 같은 다양한 windows 소켓 함수에 소켓의 set을 전달할 수 있다.
typedef struct fd_set{
u_int fd_count;
SOCKET fd_array[FD_SETSIZE];
} fd_set, FD_SET, *PFD_SET, *LPFD_SET;
fd_set과 관련되어 주로 사용하는 매크로들은 FD_ZERO, FD_SET, FD_CLR, FD_ISSET이 있다.
- FD_ZERO : set을 비운다.
ex) FD_ZERO(set); - FD_SET : 관찰대상 set에 소켓 s를 넣는다.
ex) FD_SET(s, &set); - FD_CLR : 소켓 s를 제거한다.
ex) FD_CLR(s, &set); - FD_ISSET : 소켓 s가 set에 들어있으면 0이 아닌 값을 리턴함
select 모델의 전체적인 매커니즘을 살펴보면 다음과 같다.
- 읽기[] 쓰기[] 예외(OOB)[] 관찰 대상 등록
: OOB(Out of band)는 send의 마지막 인자 MSG_OOB로 보내는 특별한 데이터로 받는 쪽에서도 recv OOB 세팅을 해야 읽을 수 있다. - select(readSet, writeSet, exceptSet);
: 관찰 시작 - 적어도 하나의 소켓이 준비되면 리턴
: 낙오자는 알아서 제거됨 - 남은 소켓 체크해서 진행
#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;
char recvBuffer[BUFSIZE] = {};
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) == 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;
cout << "Accept" << endl;
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);
}
// select
// [옵션] 마지막 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;
}
}
}
}
// WinSock 종료
::WSACleanup();
}
select 모델은 다음과 같은 단점이 있다.
- FD_SETSIZE가 64로 작다
: fd_set에 최대 64개의 소켓을 설정할 수 있으며, 따라서 큰 set이 필요하다면 set을 여러개 만들어줘야 한다. - 매번 소켓 set을 초기화 해야 한다.
: 따라서 단순하지만 최대 성능을 보인다고는 할 수 없다.