들어가며
소켓의 I/O 를 다루는 방법중 하나인 Select model 입니다.
select 함수를 중심으로 I/O가 이루어지기 때문에 Select model 이라는 이름이 붙여졌습니다.
이 모델을 정말정말 초 단순화 하면 이렇습니다.
어떻게 보면 "if문으로 "소켓에 변화가 있나요? → 변화한 소켓에 작업" 의 추가일 뿐입니다.
SOCKET listenSock;
vector<SOCKET> client;
while (true)
{
if (listenSock에 누군가 접근했나요 ? == true)
{
client.push_back(접근한 클라이언트 소켓)
}
for (SOCKET& s : client)
{
if (소켓 s에 변화가 있나요 ? == true)
{
소켓 s를 이용한 작업
}
}
}
기존 단순한 블로킹 모드의 소켓의 문제점은 함수가 성공할때까지 스레드가 무한정으로 대기한다는것이 문제였습니다.
Select모델을 사용할 경우 if문의 추가로 recv, send가 성공할때까지 대기하지 않아도 되었습니다.
기존 단순한 논블로킹 모드 소켓의 문제점은 불필요한 recv, send등 요청을 계속 하는게 문제였습니다.
Select모델을 사용할 경우 if문의 추가로 recv, send가 필요한 소켓이 나타날때 작업을 할수 있게 되었습니다.
그럼 코드로 넘어가겠습니다.
fd_set 설정
먼저 연결된 클라이언트의 소켓을 관리하기위해 clients라는 벡터를 생성했습니다.
그리고 fd_set을 정의했는데, fd_set이란 간단히 select 함수등 다양한 소켓 함수에 사용되는 소켓의 배열입니다.
fd_set은 사용되는 함수가 네 가지가 있습니다. 이를 이용해 fd_set을 이용할 것 입니다.
1. FD_ZERO(set) : fd_set인 set의 모든 비트 값을 0 으로 만듭니다.
2. FD_SET(s, &set) : 소켓 s를 fd_set인 set에 추가합니다.
3. FD_CLR(s, &set) : 소켓 s를 fd_set인 set에서 제거합니다.
4. FD_ISSET(s, set) : 소켓 s가 fd_set인 set에 들어있으면 0이 아닌 값을 리턴합니다.
vector<SOCEKT> clients;
// recv가 가능한지 확인할 용도의 fd_set
fd_set receivers;
// send가 가능한지 확인할 용도의 fd_set
fd_set senders;
fd_set 초기화 작업
while (true)
{
// fd_set 0으로 초기화
FD_ZERO(&receivers);
FD_ZERO(&senders);
// 클라이언트의 접속을 얻기 위해 listening중인 소켓 등록
FD_SET(listenSock, receivers);
// 연결된 클라이언트들을 각 fd_set에 저장
for (auto& c : clients)
{
FD_SET(c.socket, &receivers);
FD_SET(c.socket, &senders);
}
Select함수 호출
Select함수는 fd_set에 있는 소켓의 변화를 체크할 수 있습니다.
변화는 총 세 가지로 체크할 수 있습니다.
1. 읽기 가능한 소켓 : 체크하고 싶은 fd_set을 두 번째 매개변수에 지정
2. 쓰기 가능한 소켓 : 체크하고 싶은 fd_set을 세 번째 매개변수에 지정
3. 예외 상태 소켓 : 체크하고 싶은 fd_set을 네 번째 매개변수에 지정
또 하나 중요한 것은 체크한 fd_set에서 변화가 없는 소켓은 fd_set에서 모두 제거 된다는게 특징입니다.
아까 만들었던 fd_set은 읽기와 쓰기용을 만들었으니 각 fd_set을 해당 매개변수에 넣습니다.
반환 값으로는 남은 소켓의 총 개수 입니다.
그래서 변화가 있는 소켓이 0개라면 다시 처음으로 돌아갑니다.
int retCnt = select(0, &receivers, &senders, nullptr, nullptr);
if (retCnt == 0)
{
continue;
}
접근 클라이언트 소켓 저장
누군가 접근했다는것은 소켓에 데이터가 생겼단 뜻이므로 listening중이였던 소켓을 receivers fd_set에서 검색합니다.
정상적으로 소켓을 받아오면 클라이언트 소켓의 배열에 저장합니다.
if (FD_ISSET(listenSocket, &receivers))
{
SOCKADDR_IN clientAddr;
int32 addrLen = sizeof(clientAddr);
SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
if (clientSocket != INVALID_SOCKET)
{
clients.push_back(clientSocket);
}
}
모든 연결된 소켓 처리
마지막 부분입니다. 연결된 클라이언트의 모든 소켓을 FD_ISSET으로 확인하여 각 목적에 맞게 처리합니다.
for (SOCEKT& s : clients)
{
/////////////////////////////////////////////////////////////
// RECV
/////////////////////////////////////////////////////////////
// 클라이언트 소켓이 receviers FD_SET에 들어있는지 확인
if (FD_ISSET(s, &reads) == true)
{
// 들어있다면 recv처리
recv(s, buffer, size, 0);
}
/////////////////////////////////////////////////////////////
// SEND
/////////////////////////////////////////////////////////////
// 클라이언트 소켓이 senders FD_SET에 들어있는지 확인
if (FD_ISSET(s, &senders))
{
// 들어있다면 send처리
send(s.socket, &s.recvBuffer[s.sendBytes], s.recvBytes - s.sendBytes, 0);
}
}
}
정리
장점
- 위의 코드처럼 간단하게 구현이 가능합니다.
- 하나의 스레드를 이용하여 여러 소켓을 처리할수 있습니다.
단점
- fd_set의 사이즈가 최대 64로 하나의 fd_set에 64개의 소켓만을 넣을수 있습니다.