0. 논 블로킹 소켓의 문제점
(1) 코드의 복잡성
여러가지 예외 사항을 여러번 체크해야하기 때문에 코드가 복잡하게 작성된다.
(2) Busy Waiting 문제
스핀락과 유사하게, 계속해서 상태를 체크하기 때문에 CPU 자원을 많이 사용하여 비효율적이다.
1. Select 모델
논 블로킹 소켓에서 발생한 문제를 해결하기 위해 나온 것이 입출력 모델이다.
Select 모델은 select 함수가 중요하게 사용되므로 이렇게 이름지어졌다.
(1) 순서
- 읽기, 쓰기, 예외 중 관찰 대상 등록
- 관찰 시작
select(readSet, writeSet, exceptSet);
- 읽기, 쓰기, 예외 중에 하나라도 준비가 되면 return 한다.
- 준비 되지 않은 Set은 제거된다.
- 남은 소켓을 체크한다.
(2) select(read) 코드 - Server
// Select 모델
const int32 BUF_SIZE = 1000;
// 1클라 1세션, 세션 ; 서버 쪽의 클라이언트 정보
struct Session
{
SOCKET socket = INVALID_SOCKET;
char recvBuffer[BUF_SIZE] = {};
int32 recvBytes = 0;
};
int main()
{
각 클라이언트 별로 세션을 만든다.
세션은 클라이언트와 서버 사이의 연결을 담당한다. 각 세션은 소켓으로 구별된다.
세션 구조체 이후의 코드는 기존의 논 블로킹 소켓의 Listen 까지 동일하다.
// select 모델 ; fd_set
vector<Session> sessions;
sessions.reserve(100);
fd_set reads;
fd_set writes;
이후 100개의 세션을 만들고 reads와 writes를 모니터링하는 fd_set을 만든다.
while (true)
{
// 소켓 Set 초기화
FD_ZERO(&reads);
// 관찰대상으로 리슨 소켓 지정, reads set로 모니터링
FD_SET(listenSocket, &reads);
// 소켓 등록
for (Session& s : sessions)
FD_SET(s.socket, &reads);
이제 각 소켓을 대상으로 reads를 모니터링 할 것이다.
// 마지막 매개변수는 timeout
int32 retVal = ::select(0, &reads, nullptr, nullptr, nullptr);
if (retVal == SOCKET_ERROR)
break;
// read 이벤트가 발생함(listenSocket의 read는 accept에 해당함)
if (FD_ISSET(listenSocket, &reads))
{
SOCKADDR_IN clientAddr;
int32 addrLen = sizeof(clientAddr);
SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
if (clientSocket != INVALID_SOCKET)
{
if (::WSAGetLastError() == WSAEWOULDBLOCK)
continue;
cout << "클라이언트가 연결됨" << endl;
sessions.push_back(Session{ clientSocket });
}
}
select 함수를 이용하여 read가 일어나는 세션을 찾아낸다.
listenSocket에서 read가 발생했다면 이는 즉 accept 된다는 뜻이므로 accept 절차를 진행한다.
이후 예외 체크를 통해 클라이언트의 연결 여부를 확인하고 연결되었다면 세션에 밀어넣는다.
// 나머지 소켓 체크
for (Session& s : sessions)
{
if (FD_ISSET(s.socket, &reads))
{
int32 recvLen = ::recv(s.socket, s.recvBuffer, BUF_SIZE, 0);
if (recvLen <= 0)
continue;
cout << "RecvData = " << s.recvBuffer << endl;
cout << "RecvLen = " << recvLen << endl;
}
}
이제 연결된 세션에 한해서 데이터와 길이를 받아서 출력한다.
(3) 특징
- FD_SET 매크로를 사용하여 매번 모든 세션을 제거하고 체크하는 방법을 사용하게 때문에 성능면에서 비효율적이다.
- FD_SET 매크로는 한 번에 64개의 세션만 감시할 수 있기 때문에 더 큰 규모에서는 비효율적이다.
2. WSAEventSelect 모델
윈도우에서 제공하는 select 모델의 종류이다.
기본적으로 select 모델과 유사하지만, 소켓과 관련된 네트워크 이벤트를 '이벤트 객체'를 통해 감지한다는 차이가 있다.
(1) 순서
- 이벤트 생성 ; WSACreateEven(이벤트 수동 리셋, 신호 없음 상태에서 시작)
- 신호 상태 감지 ; WSAWaitForMultipleEvents
- 구체적인 네트워크 이벤트 알아내기 ; WSAEnumNetworkEvents
- 이벤트 삭제 ; WSACloseEvent
(2) 코드
버퍼 사이즈를 정의하고 세션 구조체를 작성 후에 소켓 생성, Bind, Listen하는 과정까지 동일하다.
// WSASelectEvent 모델
vector<WSAEVENT> wsaEvents;
vector<Session> sessions;
sessions.reserve(100);
// 소켓과 이벤트 연결
if (::WSAEventSelect(listenSocket, listenEvent, FD_ACCEPT | FD_CLOSE) == SOCKET_ERROR)
return 0;
이제 WSAEventSelect 함수를 이용하여 리슨 소켓에 이벤트를 연결한다.
세번째 매개 변수로 관심있는 이벤트의 비트마스크( 여기서는 accept 또는 close )를 지정한다.
while (true)
{
int32 index = ::WSAWaitForMultipleEvents(wsaEvents.size(), &wsaEvents[0], FALSE, WSA_INFINITE, FALSE);
if (index == WSA_WAIT_FAILED)
continue;
index -= WSA_WAIT_EVENT_0;
WSAWaitForMultipleEvents 함수의 매개 변수는 순서대로
1) 이벤트의 갯수
2) 이벤트 핸들 배열의 포인터
3) 하나라도 이벤트 개체가 신호를 받았다면(FALSE)
4) Time Out, 무한정 대기
5) 경고 대기 안함
이다.
신호를 받은 첫 인덱스를 반환한다.
WSANETWORKEVENTS networkEvents;
if (::WSAEnumNetworkEvents(sessions[index].socket, wsaEvents[index], &networkEvents) == SOCKET_ERROR)
continue;
이제 각 세션의 소켓을 확인하여 해당 이벤트가 발생했는지 체크한다.
위의 코드를 통과했다면 리슨 상태의 세션에서 이벤트가 발생했다는 뜻이므로 accept 해야한다.
if (networkEvents.lNetworkEvents & FD_ACCEPT)
{
// 에러 체크
if (networkEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
continue;
SOCKADDR_IN clientAddr;
int32 addrLen = sizeof(clientAddr);
SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
if (clientSocket != INVALID_SOCKET)
{
cout << "클라이언트가 연결됨." << endl;
WSAEVENT clientEvent = ::WSACreateEvent();
wsaEvents.push_back(clientEvent);
sessions.push_back(Session{ clientSocket });
if (::WSAEventSelect(clientSocket, clientEvent, FD_READ | FD_WRITE | FD_CLOSE) == SOCKET_ERROR)
return 0;
}
}
이제 클라이언트의 주소와 주소 길이를 받아와서 accept 한다.
이후에 클라이언트의 읽기, 쓰기, close 상태를 관찰하기 위해 event 배열과 sessions 배열에 추가하고 이벤트 관찰을 시작한다.
반복문의 처음으로 돌아가 WSAWaitForMultipleEvents에 의해 이벤트가 발생한 첫 인덱스를 가져와서 해당 인덱스로 어떤 이벤트가 발생했는지 확인하게 된다.
여기까지 리슨 소켓의 연결이 처리되었으므로 이제 서버의 나머지 소켓에 대한 체크를 해주면 된다.
// 클라이언트 세션 소켓 체크
if (networkEvents.lNetworkEvents & FD_READ)
{
// 에러 체크
if (networkEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
continue;
Session& s = sessions[index];
// read
int32 recvLen = ::recv(s.socket, s.recvBuffer, BUF_SIZE, 0);
if (recvLen == SOCKET_ERROR && ::WSAGetLastError() != WSAEWOULDBLOCK)
{
if (recvLen <= 0)
continue;
}
cout << "Recv Data = " << s.recvBuffer << endl;
cout << "Recv Len = " << recvLen << endl;
}
(3) 특징
- Select 함수와 다르게 Event와 Session을 매칭한다.
- 각 이벤트에 대해 탐지하고 원하는 이벤트가 발생하면 해당 세션을 처리한다.
'서버 프로그래밍 > 네트워크' 카테고리의 다른 글
2-7. 소켓 입출력 모델 (3) - IOCP(Input/Output Completion Port) (0) | 2023.12.04 |
---|---|
2-6. 소켓 입출력 모델 (2) - Overlapped 모델 (0) | 2023.12.01 |
2-4. 소켓 옵션 설정, 논-블로킹 소켓(Non-Blocking Socket) (0) | 2023.11.30 |
2-3. TCP와 UDP (0) | 2023.11.29 |
2-2. 소켓 통신 - WinSock API (0) | 2023.11.29 |