우리가 지금까지 사용해왔던 소켓은 블로킹(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 |