Coding Memo
Socket IO - Select 본문
본 포스팅은 인프런에 등록되어 있는 Rockiss 님의 강의를 보고 간단하게 정리한 글입니다.
socket을 blocking모드와 non-blocking 모드로 사용할 때, 장단점이 각각 존재했다.
blocking socket은 조건이 만족되지 않아서 blocking되는 상황이 발생하고
non-blocking socket은 조건이 만족되지 않아서 불필요하게 반복체크하는 상황이 발생한다.
그렇다면 반복체크를 계속하지 않고 non-blocking으로 해당 함수의 return 값을 받아서 사용할 수 없을까?
이에 대한 해답은 여러가지가 있는데, 그 중 하나가 select를 이용하는 것이다.
select는 non-blocking(synchronous) 수행을 위해 여러개의 소켓을 관리하고 return 가능한 소켓을 결정해준다.
select는 읽기, 쓰기, 예외 소켓들을 가지고 있는 fd_set을 각각 두는데, 그 fd_set들에서 어떤 디스크립터가 먼저 읽기가 가능한지, 쓰기가 가능한지 그리고 연결이 끊겨서 에러가 발생했는지(connection attempt failed)를 선별하여 set에 남긴다. 해당되지 않는 소켓들은 이 fd_set에서 삭제가 된다.
select는 blocking함수로 적어도 하나의 소켓이 위 조건에 만족하면(read/write 가능한 소켓이 나타나면) return한다.return value =>
-1: 에러
0 : timeout (timeout 지정 했을 경우)
0< : fd_set에 포함되어 있고 준비되어있는 소켓 핸들 전체 수
(the total number of socket handles that are ready and contained in the fd_set structures)
참고: exceptfds(예외 set)에서 OOB(Out Of Band)는 send()시 마지막 인자 MSG_OOB로 보내는 특별한 데이터를 처리한다. 따라서 SO_OOBINLINE이 가능할 때, OOB 데이터를 포함하고 있을 때 return 한다.
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
);
반드시 모든 set을 설정할 필요는 없다. 필요한 set만 넣어 사용하자.
nfds는 window에서 무시해도 된다.
readfds는 read할 소켓의 fd_set
writefds는 write할 소켓의 fd_set
exceptfds는 에러 체크용 소켓의 fd_set
timeout은 말 그대로 타임아웃 지정 (timeval 구조체)
FD_ZERO()로 fd_set을 초기화
FD_SET(socket, &set)으로 socket을 set에 넣기
FD_CLR(socket &set)으로 socket을 set에서 제거
FD_ISSET(socket, &set)으로 socket이 set에 있는지 확인(if set doesn't include the socket, returns 0)
참고!
fd_set struct
typedef struct fd_set {
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
FD_SETSIZE가 64이기 때문에 하나의 fd_set당 64개의 socket만 사용할 수 있다는 단점이 있긴하다.
참고2: select는 준비된 socket 자체를 알려주지 않기 때문에 직접 어떤 소켓인지 확인해야 한다.
참고3: timeval을 nullptr로 설정하면 timeout이 없어서 조건이 충족될 때까지 blocking 된다.
자세한 내용은 요기...
https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-select
위 참고2에서 언급했던 것에 대해, 다시 말하자면 select는 준비된 소켓이 어떤 소켓인지 알려주지 않기 때문에 FD_ISSET으로 직접확인을 해야한다.
또한 그 socket에 대한 read/recv를 위해 해당 데이터를 넣을 수 있는 버퍼까지 들어간 구조체를 직접 만들어 사용한다. (socket에 대한 session이라고 생각하면 편하다.)
const int BUFSIZE = 1000;
struct Session
{
SOCKET socket;
char recvBuffer[BUFSIZE] = {};
int recvBytes = 0;
int sendBytes = 0;
};
full codes
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;
}
// 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)
{
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);
}
}
int retVal = ::select(0, &reads, &writes, nullptr, nullptr);
if (retVal == SOCKET_ERROR)
{
break;
}
// 누가 준비가 완료되었는지는 알 수가 없어서 FD_ISSET으로 확인 해야함
// Listener 소켓 체크
if (FD_ISSET(listenSocket, &reads))
{
SOCKADDR_IN clientAddr;
int addrLen = sizeof(clientAddr);
SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
// ISSET이 true면 성공했다는 의미라서 오류를 확인할 필요는 없음!
// 그러나 일단 확인
if (clientSocket != INVALID_SOCKET)
{
cout << "Client Connected" << endl;
sessions.push_back(Session{ clientSocket });
}
}
// 나머지 소켓 체크
for (Session& s : sessions)
{
// reads 체크
if (FD_ISSET(s.socket, &reads))
{
int32 recvLen = ::recv(s.socket, s.recvBuffer, BUFSIZE, 0);
if (recvLen <= 0)
{
// TODO : sessions 제거
continue;
}
s.recvBytes = recvLen;
}
// writes 체크
if (FD_ISSET(s.socket, &writes))
{
// send return value : total number of bytes sent
// blocking -> 모든 데이터 다 보냄
// non-blocking -> 일부만 보낼 수가 있음 (상대방 수신 버퍼 상황에 따라)
// (즉, 요청한 것보다 작을 수 있음)
int sendLen = ::send(s.socket, &s.recvBuffer[s.sendBytes], s.recvBytes - s.sendBytes, 0);
if (sendLen <= 0)
{
// TODO : sessions 제거
continue;
}
s.sendBytes += sendLen;
if (s.recvBytes == s.sendBytes)
{
s.recvBytes = 0;
s.sendBytes = 0;
}
}
}
}
// 윈속 종료
::WSACleanup();
}
Select는 Linux 계열에서도 사용가능하여 뛰어난 이식성을 가졌지만 성능은 좋지 않다.
또한 64개의 소켓만 한번에 사용가능 하다는 점과 매번 set를 만들어 줘야하는 단점들을 가지고 있다.
'Game Server (C++)' 카테고리의 다른 글
Asynchronous Socket IO - Overlapped (event, callback) (0) | 2022.11.26 |
---|---|
WSAEventSelect (0) | 2022.11.23 |
Non-blocking Socket (0) | 2022.11.18 |
Socket Option - (get/set)sockopt() (0) | 2022.11.07 |
Socket Programming - UDP (0) | 2022.11.03 |