Select 방식이란
- Select 방식이란 논 블로킹 방식 중 하나로 논 블로킹 방식의 단점인 스핀락처럼 무한히 시도하는 문제와, 이중으로 에러를 체크해줘야 하는 문제를 해결하기 위한 방법 중 하나이다.
- accept, recv와 같은 함수들을 호출하기 전에 해당 이벤트가 일어났는지를 체크하고 일어났으면 처리하는 식으로 로직이 구성되어 있다.
1. 읽기/쓰기/예외 중 몇 개를 관찰 대상으로 등록한다.
2. select 함수 호출 -> 관찰 시작
3. 적어도 하나의 소켓이 준비가 되면 리턴 -> 낙오자 제거
4. 남은 소켓 제거한 후 진행
/* Server.cpp */
#include "pch.h"
#include "ThreadManager.h"
const int32 BUF_SIZE = 1000;
struct Session //접속한 클라당 하나의 세션을 갖는다
{
SOCKET socket = INVALID_SOCKET;
char recvBuffer[BUF_SIZE] = {}; // 모든 세션마다 고유의 버퍼가 있어야 한다
int32 recvBytes = 0; // 버퍼를 받는 크기를 설정
};
int main()
{
SocketUtils::Init();
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;
// 이전에 사용했던 주소를 사용하기 위함
SocketUtils::SetReuseAddress(listenSocket, true);
if (SocketUtils::BindAnyAddress(listenSocket, 7777) == false)
return 0;
if (SocketUtils::Listen(listenSocket) == false)
return 0;
vector<Session> sessions;
sessions.reserve(100);
fd_set reads;
fd_set writes;
while (true)
{
FD_ZERO(&reads);
// Listen Socket 등록
FD_SET(listenSocket, &reads);
for (Session& s : sessions)
{
FD_SET(s.socket, &reads);
}
// 남을 애들만 남아라!
int32 retVal = ::select(0, &reads, nullptr, nullptr, nullptr);
if (retVal == SOCKET_ERROR)
break;
// 연결이 안된 소켓들은 reads에서 다 튕겨져나가게 됨
// 그래서 reads에 남아있는 애들은 read를 할 수 있는 상태라는 것을 알 수 있다.
// 남아 있는 애들에 대한 처리
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 << "Client Connected!" << endl;
sessions.push_back(Session{ clientSocket });
}
}
// 블로킹 방식과 다른 점은 select 호출 후 FD_ISSET의 인자로 listenSocket과 reads 안에 연결된 소켓이 있는지 검사하고 하나 이상이 연결되었다면 true를 반환
// 그리고 기존과 마찬가지로 소켓을 생성한 후 해당 소켓을 세션에 집어넣는다.
}
SocketUtils::Clear();
}
- select 함수에서 받는 인자중 첫번째 인자는 무시된다.
- 두 번째 인자는 readable 여부를 확인하고자 하는 소켓들의 집합을 넘기는데, 클라이언트로부터 연결 요청이 수신되어서, accept 함수가 문제 없이 완료될 수 있는 경우, 즉 클라이언트와의 연결 요청을 처리할 수 있는 경우를 나타낸다.
- select의 반환값은 위 조건을 충족하는 소켓의 개수를 반환한다.
- FD_ISSET은 reads안의 listenSocket의 flag가 세팅되었는지를 확인하고 세팅되었다면 아래 코드들을 실행하게 된다.
- 이후 코드는 기존 서버에서 클라이언트를 받는 코드와 동일하다.
WSAEventSelect
- 이 방식은 Select 방식이지만 Windows에서만 동작한다는 차이점이 있다.
/* Server.cpp */
#include "pch.h"
#include "ThreadManager.h"
// WSAEventSelect = WSAEventSelect가 메인
// 소켓과 관련된 네트워크 이벤트를 [이벤트 객체]를 통해 감지
// 생성 : WSACreateEvent (수동 리셋 Manual-Reset + Non-Signaled 상태 시작)
// 삭제 : WSACloseEvent
// 신호 상태를 감지 : WSAWaitForMultipleEvents
// 구체적인 네트워크 이벤트 알아내기 : WSAEnumNetworksEvents
const int32 BUF_SIZE = 1000;
struct Session //접속한 클라당 하나의 세션을 갖는다
{
SOCKET socket = INVALID_SOCKET;
char recvBuffer[BUF_SIZE] = {}; // 모든 세션마다 고유의 버퍼가 있어야 한다
int32 recvBytes = 0; // 버퍼를 받는 크기를 설정
};
int main()
{
SocketUtils::Init();
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;
// 이전에 사용했던 주소를 사용하기 위함
SocketUtils::SetReuseAddress(listenSocket, true);
if (SocketUtils::BindAnyAddress(listenSocket, 7777) == false)
return 0;
if (SocketUtils::Listen(listenSocket) == false)
return 0;
// WSAEVENT 구조체를 담는 벡터를 만든다
vector<WSAEVENT> wsaEvents;
// 클라이언트당 하나씩 부여하는 세션이라는 구조체를 담는 벡터를 만든다.
vector<Session> sessions;
sessions.reserve(100);
// 서버쪽 이벤트를 하나 만들어서 wsaEvents 벡터에 담는다.
WSAEVENT listenEvent = WSACreateEvent();
wsaEvents.push_back(listenEvent);
sessions.push_back(Session{ listenSocket }); // 인덱스를 맞춰주기 위해서 더미 세션을 만듦
// 서버쪽 소켓에 에러가 있는지 확인
if (WSAEventSelect(listenSocket, listenEvent, FD_ACCEPT | FD_CLOSE) == SOCKET_ERROR)
return 0;
while (true)
{
// 다수의 이벤트 감시
int32 index = WSAWaitForMultipleEvents(wsaEvents.size(), &wsaEvents[0], FALSE, WSA_INFINITE, FALSE);
if (index == WSA_WAIT_FAILED)
continue;
index -= WSA_WAIT_EVENT_0; // 이렇게 하면 이벤트가 일어난 소켓의 인덱스 위치를 알 수 있다.
// 구체적인 네트워크 이벤트 알아내기
WSANETWORKEVENTS networkEvents;
if (WSAEnumNetworkEvents(sessions[index].socket, wsaEvents[index], &networkEvents) == SOCKET_ERROR)
continue;
// accept 이벤트가 발생했는지 체크
if (networkEvents.lNetworkEvents & FD_ACCEPT) // 해당 이벤트가 발생했는지 비트플래그로 체크
{
// msdn과 맞게 2차 체크
if (networkEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
continue;
SOCKADDR_IN clientAddr;
int32 addrLen = sizeof(clientAddr);
// 여기까지 들어왔다는건 accept가 일어났다는걸 사전에 인지한 상태였기 때문에 블로킹되는 상태가 발생하지 않는다.
SOCKET clientSocket = accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
if (clientSocket != INVALID_SOCKET)
{
cout << "Client Connected!" << 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;
}
}
// recv 이벤트가 발생했는지 체크
if (networkEvents.lNetworkEvents & FD_READ)
{
// msdn에 맞게 2차 체크
if (networkEvents.iErrorCode[FD_READ_BIT] != 0)
continue;
Session& s = sessions[index];
// 여기서의 recv도 recv가 발생했다는 이벤트를 받고 분기에 들어온 것이기 때문에 블로킹되지 않는다.
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;
}
}
SocketUtils::Clear();
}
'게임 서버 > [Inflearn_rookiss]올인원_클라&서버 연동' 카테고리의 다른 글
14. IOCP (0) | 2023.12.11 |
---|---|
13. Overlapped 방식 (0) | 2023.12.11 |
11. 논 블로킹 소켓 (1) | 2023.12.11 |
10. TCP vs UDP (0) | 2023.12.11 |
9. 소켓 프로그래밍 (0) | 2023.12.11 |
댓글