C++ 논블로킹 소켓 실습

정은성·2023년 5월 30일
1
post-thumbnail

※ Rookiss님의 [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 강의를 보고 정리한 글입니다.

현재 사용하고 있는 소켓은 블로킹(Blocking) 소켓이다. 일부 함수에서 빠져나가지못하고 블로킹 상태가 된다.

블로킹 상태란 현재 스레드가 더 이상 코드를 진행하지 않고 block 상태로 멈춰 있는 상태를 말합니다.

블로킹 탈출 조건

  • accept → 접속한 클라가 있을 때
  • connect → 서버 접속 성공 했을 때
  • send, sendto → 요청한 데이터를 송신 버퍼에 복사 했을 때
  • recv, recvfrom → 수신 버퍼에 도착한 데이터가 있고, 이 것을 유저 레벨 버퍼에 복사 했을 때

이러한 조건들이 만족할 때 까지 코드를 블로킹 하는 것은 게임서버에선 효율적이지 않다.

우리는 논블로킹(Non-Blocking) 소켓을 통해 에코 서버를 제작해볼 것이다.

소켓 생성

소켓 생성 과정은 동일하다.

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;

논블로킹 소켓으로 설정

u_long on = 1;
if (::ioctlsocket(listenSocket, FIONBIO, &on) == INVALID_SOCKET)
	return 0;

ioctlsocket이라는 함수로 소켓의 모드를 변경할 수있다. 우리는 FIONBIO라는 명령으로 소켓의 블로킹 유무를 설정할 수있다. 0이면 블로킹, 이외의 값은 논 블로킹이다.

주솟값 설정, bind, listen

주솟값 설정, bind, listen은 다를 것 없다.

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;

// accept, send, recv를 위한 준비
SOCKADDR_IN clientAddr;
int32 addrLen = sizeof(clientAddr);

Accept

논 블로킹 소켓은 기본적으로 바로 빠져나가기 때문에 연결이 안된 상태로 빠져나올 수 있다. 그러니 while문을 통해 무한 반복 시켜주어야한다.

while (true){

	SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
	if (clientSocket == INVALID_SOCKET) {

		if (::WSAGetLastError() == WSAEWOULDBLOCK)
			continue;
		// 진짜 에러
		break;
	}
}

블로킹 소켓의 경우 INVALID_SOCKET은 모주건 오류 상황이다.

하지만 논 브로킹 소켓에선 꼭 오류상황이 아니다. 기다리지 않고 빠져나오기 때문이다.

그래서 우리는 한 번더 예외처리를 해주어야한다.

WSAWOULDBLOCK: 논블로킹 소켓이 아직 완료가 안됐을 때 뜸.

Connect

이번엔 접속을 위한 코드를 작성해보자

while (true) {
	if (::connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
		
		if (::WSAGetLastError() == WSAEWOULDBLOCK)
			continue;
		
		// connect에서 wouldblock 에러가 뜨면 그냥 connect를 진행중인것일수도있다.
		// 하지만 그 상태에서 한 번 더 connect를 하면 이 오류가 나온다.
		if (::WSAGetLastError() == WSAEISCONN)
			break;

		//ERROR
		HandleError("Connect");
		break;

	}
}

Connect는 다른 작업들과 달리 한 가지를 더 체크해주어야한다. Connect에서 wouldblock에러 코드가 뜨면 connect를 진행중인 것이다. 하지만 while문 때문에 그 상태에서 한 번 더 connect하게되면

WSAEISCONN 라는 에러 코드가 나온다. 이경우엔 그냥 while문을 빠져나가주면된다.

Recv

Recv도 while의 무한루프로 동작된다.

//Recv
	while (true) {
		char recvBuffer[1000];
		int32 recvLen = ::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);
		if (recvLen == SOCKET_ERROR) {

			if (::WSAGetLastError() == WSAEWOULDBLOCK)
				continue;

			// 에러
			break;
		}
		else if (recvLen == 0) {
			// 무조건 끊긴 상황
			break;
		}

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

Recv도 마찬가지로 WASWOULDBLOCK을 통해 이것이 진짜 오류인지, 아직 소켓이 완료가 안된건지 파악해주어야한다.

Send

받은 것을 send로 다시 되돌려주는 코드를 작성했다.

while (true) {
	if (::send(clientSocket, recvBuffer, recvLen, 0) == SOCKET_ERROR) {
		if (::WSAGetLastError() == WSAEWOULDBLOCK)
			continue;

		//ERROR
		break;
	}

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

Send도 마찬가지로 WASWOULDBLOCK을 통해 이것이 진짜 오류인지, 아직 소켓이 완료가 안된건지 파악해주어야한다.

이것만 봐도 논블로킹 소켓에 의한 프로그래밍은 while문도 중첩되어 매우 난잡하고, 계속 while문을 돌며 체크를 하는 것에서 cpu사이클 소모도 매우 크다는 것을 알 수 있다.

Lock에서도 같은 문제가 발생하는데 SpinLock의 경우 누군가 사용을 중지하기 전까지 계속 체크를 해주면서 대기한다.

결국 논 블로킹으로만 바꿔주면 원래 존재하던 문제들이 모두 해결이 되지않는구나를 알 수 있다.

0개의 댓글