Coding Memo

Socket IO - Select 본문

Game Server (C++)

Socket IO - Select

minttea25 2022. 11. 18. 20:32

본 포스팅은 인프런에 등록되어 있는 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

 

select function (winsock2.h) - Win32 apps

The select function determines the status of one or more sockets, waiting if necessary, to perform synchronous I/O.

learn.microsoft.com


위 참고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