Coding Memo

Non-blocking Socket 본문

Game Server (C++)

Non-blocking Socket

minttea25 2022. 11. 18. 13:59

본 포스팅은 인프런에 등록되어 있는 Rockiss 님의 강의를 보고 간단하게 정리한 글입니다.


지금까지 했던 socket은 blocking으로 작동하는 소켓이었다.

 

accept : 접속한 클라이언트가 있을 때

connect : 서버가 접속에 성공했을 때

send, sendto : 요청한 데이터를 sendBuffer에 복사 했을 때

recv, recvfrom : recvBuffer에 도착한 데이터가 존재하고, 이를 유저레벨 buffer에 복사했을때

다음 코드가 실행이 된다.

 

다시 말해, 위 조건이 실행되거나 에러가 발생할 때까지 다음 코드 실행은 block되는 것이다.

 

곰곰히 생각해보면 서버를 실행할 때 어떤 송수신이나 connect에 대한 응답을 무한히 대기하여 block된 상태로 있을 수는 없을 것이다. 따라서 non-blocking을 사용한다.

 

non-blocking 소켓을 이용할 시 주의할 점은 명령 실행이 끝났는지 끝나지 않았는지 알 수 없다는 점이다.

그러므로 accept, connect등의 함수를 실행시켜도 결과가 바로 나오지 않았을 수 있음을 인지해야한다. -> infinite loop 이용...

 

non-blocking 소켓으로 echo 서버를 만들어보자.

 

bind, listen까지는 blocking과 동일하다.

 

bind - listen - accept 확인 - recv 대기 - send 실행 확인 - 반복

(에코 서버기 때문에 recv를 한 후에 send를 해야한다.)

(딱 봐도 보기 좋지 않고 비효율 적으로 보인다...)


- non-blocking 설정

1번째 인자는 설정할 소켓을 넣어주면 된다.

2번째 인자는 FIONBIO를 넣어주면 되는데 socket I/O mode라는 뜻이다.

3번째 인자가 0이면 blocking 그외 값이면 non-blocking 모드 이다.

int ioctlsocket(
  [in]      SOCKET s,
  [in]      long   cmd,
  [in, out] u_long *argp
);
// non-blocking 사용
u_long on = 1;
if (::ioctlsocket(listenSocket, FIONBIO, &on) == INVALID_SOCKET)
{
	return 1;
}

 

자세한 내용은... 여기

https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-ioctlsocket

 

ioctlsocket function (winsock.h) - Win32 apps

The ioctlsocket function (winsock.h) controls the I/O mode of a socket.

learn.microsoft.com

 

- full code

server

int main()
{
	WSAData wsaData;
	if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		return 1;
        
	SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0);
	if (listenSocket == INVALID_SOCKET)
	{
		return 1;
	}

	// non-blocking 사용
	u_long on = 1;
	if (::ioctlsocket(listenSocket, FIONBIO, &on) == INVALID_SOCKET)
	{
		return 1;
	}

	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;

	SOCKADDR_IN clientAddr;
	int addrLen = sizeof(clientAddr);
	
	while (true)
	{
		SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
		// 즉 연결을 하려면 계속 확인해야됨
		if (clientSocket == INVALID_SOCKET)
		{
			// 원래 blocking으로 실행되었어야 하는데 지금은 non-blocking 상태라
			// INVALID_SOCKET이어도 잘못된 상황이 아닐 수 있음(아직 실행이 완료가 안되었을 때)
			if (::WSAGetLastError() == WSAEWOULDBLOCK)
			{
				continue;
			}

			// Error
			break;
		}

		cout << "Client Connected!" << endl;

		//recv
		while (true)
		{
			char recvBuffer[1000];
			int recvLen = ::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);
			if (recvLen == SOCKET_ERROR)
			{
				// 위와 마찬가지로 non-blocking이라 recvLen 값이 0 이하일 수 있음
				// 그러나 문제는 아닐 수 있음 (데이터를 아직 못받은 상태)
				if (::WSAGetLastError() == WSAEWOULDBLOCK)
				{
					continue;
				}

				// Error
				break;
			}
			else if (recvLen == 0)
			{
				// 연결 끊김
				break;
			}

			cout << "Recv Data Len = " << recvLen << endl;

			// send
			while (true)
			{
				if (::send(clientSocket, recvBuffer, recvLen, 0) == SOCKET_ERROR)
				{
					// 마찬가지...
					if (::WSAGetLastError() == WSAEWOULDBLOCK)
					{
						continue;
					}

					// Error
					break;
				}

				cout << "Send Data ! Len = " << recvLen << endl;
				break;
			}
		}
	}

	// 윈속 종료
	::WSACleanup();
}

 

client

int main()
{
	WSAData wsaData;
	if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		return 0;
	}

	SOCKET clientSocket = ::socket(AF_INET, SOCK_STREAM, 0);
	if (clientSocket == INVALID_SOCKET)
	{
		return 0;
	}

	// non-blocking 사용
	u_long on = 1;
	if (::ioctlsocket(clientSocket, FIONBIO, &on) == INVALID_SOCKET)
	{
		return 0;
	}

	SOCKADDR_IN serverAddr;
	::memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	::inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
	serverAddr.sin_port = ::htons(7777);

	// Connect
	while (true)
	{
		if (::connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
		{
			// non-blocking
			if (WSAGetLastError() == SOCKET_ERROR)
			{
				return 0;
			}

			// 이미 연결된 상태라면 break
			if (::WSAGetLastError() == WSAEISCONN)
			{
				break;
			}

			// Error
			break;
		}
	}

	cout << "Connected to Server!" << endl;

	char sendBuffer[100] = "Hello World";

	// send
	while (true)
	{
		if (::send(clientSocket, sendBuffer, sizeof(sendBuffer), 0) == SOCKET_ERROR)
		{
			// 마찬가지...
			if (::WSAGetLastError() == WSAEWOULDBLOCK)
			{
				continue;
			}

			// Error
			break;
		}

		cout << "Send Data ! Len = " << sizeof(sendBuffer) << endl;
		
		while (true)
		{
			char recvBuffer[1000];
			int recvLen = ::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);
			if (recvLen == SOCKET_ERROR)
			{
				// 위와 마찬가지로 non-blocking이라 recvLen 값이 0 이하일 수 있음
				// 그러나 문제는 아닐 수 있음 (데이터를 아직 못받은 상태)
				if (::WSAGetLastError() == WSAEWOULDBLOCK)
				{
					continue;
				}

				// Error
				break;
			}
			else if (recvLen == 0)
			{
				// 연결 끊김
				break;
			}
			cout << "Recv Data Len = " << recvLen << endl;
			break;
		}

		this_thread::sleep_for(1s);
	}

	// 소켓 리소스 반환
	::closesocket(clientSocket);

	// 윈속 종료
	::WSACleanup();
}

매번 상태를 확인하는 무한 while문이 있기 때문에 효율적이지 않고 자원이 낭비될 수가 있다.

 

이 문제를 해결하기 위한 여러가지 대안이 있다.

 

select

https://minttea25.tistory.com/86

 

'Game Server (C++)' 카테고리의 다른 글

WSAEventSelect  (0) 2022.11.23
Socket IO - Select  (0) 2022.11.18
Socket Option - (get/set)sockopt()  (0) 2022.11.07
Socket Programming - UDP  (0) 2022.11.03
Socket Programming - TCP  (0) 2022.10.31