네트워크, 서버

NonBlocking Socket

춤추는수달 2022. 11. 7. 05:06

NonBlocking Socket이란

사실 우리가 아무런 작업 없이 소켓을 생성하면 Blocking Socket이 만들어진다. (winsock2 기준) 

Blocking Socket이라고 불리우는 이유는 send, receive, accept 등 동작을 할 때 상황이 여의치 않으면 제대로 동작할 수 있을 상황까지 프로세스가 대기(Block)하게 되기 때문이다. 반대로 NonBlocking Socket은 여의치 않으면 일단 Return하고 에러 메세지를 통해 상황을 알려준다.


Block 되는 상황, NonBlock 되는 상황

그렇다면 Blocking Socket이 어떤 상황에서 Block 되는지 알아보자.

  1. Send : socket의 SendBuffer 가 가득 차있으면 Block된다.
  2. Recv : socket의 ReadBuffer가 비어있으면 Block된다.
  3. Accept: 대기중인 연결 요청이 없으면 연결 요청이 생길 때 까지 Block된다.
  4. Connect: 서버에 연결이 성사될 때 까지 Block 된다.

NonBlocking Socket은 위의 상황들에서 Block되는 대신 에러를 Return 한다. 따라서 에러 메세지를 해석해서 적절한 조치를 취해주어야 제대로 동작할 수 있다.


NonBlocking Socket 사용하기

만들기

NonBlocking Socket을 사용하기 위해선 먼저 NonBlocking Socket을 만들어야 한다. NonBlocking Socket은 ioctlsocket 이라는 함수를 사용해 만들 수 있다. 단순히 만들어진 Blocking 소켓을 ioctlsocket 함수를 통해 NonBlocking 소켓으로 만들어주면 된다.

  1. Blocking Socket을 만드는 것 과 같이 socekt() 함수를 사용해 소켓을 생성한다.
  2. ioctlsocket() 함수를 사용해 만들어진 소켓을 NonBlocking socket으로 만든다.
WSAData wsaData;
	if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		return 0;

	// 블로킹(Blocking) 소켓
	// accept -> 접속한 클라가 있을 때
	// connect -> 서버 접속 성공했을 때
	// send, sendto -> 요청한 데이터를 송신 버퍼에 복사했을 때
	// recv, recvfrom -> 수신 버퍼에 도착한 데이터가 있고, 이를 유저레벨 버퍼에 복사했을 때
	
	// 논블로킹(Non-Blocking)

	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;

	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;

그 외 Bind, Listen 작업은 Blocking Socket과 차이가 없다. 

 

NonBlocking 처리

NonBlocking Socket은 Send, Recv, Accept 작업 시 별도의 에러 처리가 필요하다고 했다. 어떻게 하면 좋을지 알아보자.

  • Accept 동작이 실패하면 INVALID_SOCKET이 반환된다. 그런데 원래 블록되었어야 하지만 논블로킹 소켓이라 그냥 반환되었을 경우도 INVALID_SOCKET이 반환된다. 따라서 INVALID_SOCKET이 반환됐을 경우 ::WSAGetLastError() == WSAEWOULDBLOCK를 통해 원래 블록되어야 했는지 확인해야한다. 이런 경우 다시 Accept 함수를 호출해주던가 적절한 조치를 취하고, WSAEWOULDBLOCK상황이 아닌 경우 그냥 실패한 것이므로 역시나 이에 적절한 처리를 해주면 된다.
  • Send, Recv 동작도 Accept와 매우 비슷하다. SOCKET_ERROR가 반환되었을 경우  ::WSAGetLastError() == WSAEWOULDBLOCK를 통해 원래 블로킹되었어야 했는지 확인하고, 경우에 따라 적절한 조치를 취해주면 된다.
SOCKADDR_IN clientAddr;
	int32 addrLen = sizeof(clientAddr);

	// Accept
	while (true)
	{
		SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
		if (clientSocket == INVALID_SOCKET)
		{
			// 원래 블록했어야 했는데... 너가 논블로킹으로 하라며?
			if (::WSAGetLastError() == WSAEWOULDBLOCK)
				continue;

			// Error
			break;
		}

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

		// Recv
		while (true)
		{
			char recvBuffer[1000];
			int32 recvLen = ::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);
			if (recvLen == SOCKET_ERROR)
			{
				// 원래 블록했어야 했는데... 너가 논블로킹으로 하라며?
				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;
			}
		}
	}

코드를 보면 알겠지만 while(ture)로 묶인 코드가 많다. 즉 의미없이 반복되는 NonBlocking 함수들이 많다. NonBlocking이면 Blocking보다 더 효율적이라고 생각했을지 모르지만 코드를 위와 같이 짜면 오히려 더 비효율적으로 동작하게 된다. 이런 문제를 해결하기 위해 여러가지 네트워크 모델이 필요하다. 

'네트워크, 서버' 카테고리의 다른 글

Overlapped 모델(Event 기반)  (0) 2022.11.11
소켓 프로그래밍  (0) 2022.11.02
JobTimer  (0) 2022.07.19
JobQueue  (0) 2022.07.16
Google Protocol Buffer  (0) 2022.07.07