3 minute read

winsock2를 이용해서 만들어본 논블록 에코 서버 예제

winsock2를 통해 소켓을 생성하면 기본적으로 blocking socket이다. 아래와 같이 옵션을 줘서 non-blocking socket으로 바꿀 수 있다.

기존 blocking socket에서 만약 send를 하다가 block이 되는 상황이 발생하면 return이 될 때까지 다음 명령을 실행할 수 없다. 만약 서버가 핸들링하고 있는 클라이언트가 여럿일 때 이렇게 block이 빈번하게 일어나면 서비스의 질이 저하될 것이다. 각 클라이언트당 스레드 하나를 할당해서 처리하는 방법도 있지만 이렇게 되면 컨텍스트 스위칭 비용이 발생하기 때문에 또한 비효율적일 수 있다. 그래서 아래와 같이 non-blocking 방법을 써서 위의 문제를 일부 해소할 수 있다.

하지만 아래의 경우처럼 원래의 경우 block이 발생했어야하는 상황에서 return이 될 경우 무한루프를 돌면서 계속 정상적으로 실행이 되는지 체크하는 방식은 마치 spinlock의 경우처럼 cpu 사이클을 낭비하게 된다. 이를 해결하기 위해서 몇 가지 방법이 있다. 이는 추후에 올려봐야겠다.

server.cpp

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

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

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

	// 블로킹(Blocking)
	//	   accept -> 접속한 클라가 있을 때
	// 	   connect -> 서버 접속 성공했을 때 
	// 	   send, sedto -> 요청한 데이터를 송신 버퍼에 복사했을 때
	// 	   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)  // maximum queue length
		return 0;

	cout << "Listen" << endl;
	 
	SOCKADDR_IN clientAddr;
	int32 addrLen = sizeof(clientAddr);


	// 논블록 소켓에서 아래와 같은 식으로 무한루프를 돌면서 무한루프를 돌면서 WSAEWOURLDBLOCK인 경우 체크를 하는 것은 CPU를 낭비하는 비효율적인 상황이다. 이를 해결할 수 있는 여러 옵션이 존재.

	// Accept
	while (true)
	{
		SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
		if (clientSocket == INVALID_SOCKET)  
		// 앞선 예제에서 블록킹 소켓에서는 여기서 그냥 프로그램을 종료시키는 방법을 사용했지만 
		// 논블록킹 소켓에서는 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;
			}
		}
		
	}

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

client.cpp

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

#include <winsock2.h>
#include <mswsock.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

int main()
{
	// winsock 초기화 (ws2_32 라이브러리 초기화)
	// 관련 정보가 wsaData에 채워짐
	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;

	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);  // 사람이 알아보기 쉬운 텍스트(human-readable text)형태의 IPv4 와 IPv6 주소를 binary 형태로 변환 하는 기능
	serverAddr.sin_port = ::htons(7777);

	// Connect
	while (true)
	{
		if (::connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
		{

			// 원래 블록했어야 했는데... 너가 논블로킹으로 하라며?
			if (::WSAGetLastError() == WSAEWOULDBLOCK)
				continue;

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

			// Error
			break;
		}
	}

	cout << "Connectd 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;

		// 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;
			break;
		}
		
		this_thread::sleep_for(1s);
	}

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

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


Tags:

Categories:

Updated: