서버/네트워크

5. 논블로킹(Non-Blocking) 소켓

광란의슈가슈가룬 2024. 9. 3. 22:58

우리가 지금까지 사용해왔던 소켓은 블로킹(Blocking) 방식 소켓이다. 우리가 통신을 하면서 사용했던 함수들이 언제 완료되는지 한 번 보자.

 

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

 

이렇게 어떠한 행위가 끝날 때 비로소 함수가 종료된다. 함수가 실행되는 동안에는 대기를 해야한다는 뜻이다. 말그대로 블로킹(Blocking) 당하는 것이다. 게임을 한 번 생각해보자. 몇 천 명이 동시에 서버에 접속을 한다고 할 때 저렇게 대기를 하면 당연히 문제가 생길 수 밖에 없다. 그렇다고 접속을 시도하는 클라이언트마다 스레드를 만들어 몇 천 개의 스레드를 사용하는 것은 또 컨텍스트 스위칭이 그만큼 일어난다는 뜻이므로 그 또한 비효율적이다. 

 

DummyClient.cpp

#include "pch.h"
#include <iostream>

#include <WinSock2.h>
#include <MSWSock.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

void HandleError(const char* cause)
{
	int32 errCode = ::WSAGetLastError();
	cout << cause << " ErrorCode : " << errCode << endl;
}

int main()
{
	// 윈속 라이브러리 초기화 (ws2_32 라이브러리 초기화)
	// 관련 정보가 wsaData에 채워짐
	WSAData wsaData;
	if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		return 0;

	SOCKET clientSocket = ::socket(AF_INET, SOCK_STREAM, 0); // TCP
	if (clientSocket == INVALID_SOCKET)
	{
		HandleError("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)
		{
			// 원래 블록했어야 함, But 논블로킹
			if (::WSAGetLastError() == WSAEWOULDBLOCK)
				continue;
			// 이미 연결된 상태라면
			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)
		{
			// 원래 블록했어야 함, But 논블로킹
			if (::WSAGetLastError() == WSAEWOULDBLOCK)
				continue;

			// Error
			break;
		}

		cout << "Send Data! Len = " << sizeof(sendBuffer) << endl;

		while (true)
		{
			char recvBuffer[1000];
			int32 recvLen = ::recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0);
			if (recvLen == SOCKET_ERROR)
			{
				// 원래 블록했어야 함, But 논블로킹
				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);

	// 윈속 종료, WSAStartup이 호출된 수만큼 실행해줘야 함
	::WSACleanup();
}

Server.cpp

#include "pch.h"
#include "CorePch.h"
#include <Windows.h>
#include "ThreadManager.h"

#include <WinSock2.h>
#include <MSWSock.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

void HandleError(const char* cause)
{
	int32 errCode = ::WSAGetLastError();
	cout << cause << " ErrorCode : " << errCode << endl;
}

int main()
{
	// 윈속 라이브러리 초기화 (ws2_32 라이브러리 초기화)
	// 관련 정보가 wsaData에 채워짐
	WSAData wsaData;
	if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		return 0;

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

	
	SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0); // TCP
	if (listenSocket == INVALID_SOCKET)
	{
		HandleError("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)
	{
		HandleError("Bind");
		return 0;
	}

	if (::listen(listenSocket, SOMAXCONN) == SOCKET_ERROR)
	{
		HandleError("Listen");
		return 0;
	}

	cout << "Accept" << endl;

	SOCKADDR_IN clientAddr;
	int32 addrLen = sizeof(clientAddr);

	// Accept
	while (true)
	{
		SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
		if (clientSocket == INVALID_SOCKET)
		{
			// 원래 블록했어야 함, But 논블로킹
			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)
			{
				// 원래 블록했어야 함, But 논블로킹
				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)
				{
					// 원래 블록했어야 함, But 논블로킹
					if (::WSAGetLastError() == WSAEWOULDBLOCK)
						continue;

					// Error
					break;
				}

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


	// 윈속 종료, WSAStartup이 호출된 수만큼 실행해줘야 함
	::WSACleanup();
}

논블로킹 방식에서는 ioctlsocket함수 하나만 추가하면 된다. 그렇다고 바로 정상적으로 돌아가는 것이 아니라 논블로킹이기 때문에 생길 수 있는 문제들에 대해서 예외처리를 해주어야 한다. 코드를 보면 루프를 돌면서 소켓을 연결할 때, 데이터를 주고 받을 때 등 계속해서 확인을 해준다. 마치 스핀락에서 무한루프를 도는 것처럼 말이다. 언뜻 보면 무한루프를 돌면서 CPU 자원을 낭비하는 것 같다. 물론 논블로킹의 이점을 살리면서 CPU 낭비를 줄이는 방법 또한 존재한다. 이는 다음 소켓 모델에서 자세히 알아보도록 하자.

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

7. WSAEventSelect 모델  (0) 2024.09.09
6. Select 모델  (0) 2024.09.05
4. 소켓 옵션  (3) 2024.09.01
3. UDP 서버  (2) 2024.08.30
2. TCP 서버  (0) 2024.08.29