KoreanFoodie's Study
[C++ 게임 서버] 3-7. 논블로킹 소켓 본문
[C++ 게임 서버] 3-7. 논블로킹 소켓
핵심 :
1. 논블로킹 소켓은, 블로킹 소켓과 다르게 연결 및 송수신이 일시적으로 불가능한 경우, SpinLock 처럼 while 문을 돌며 다시 시도를 한다.
2. 논블로킹 소켓을 만드는 것은 간단하다. ioctlsocket 을 사용하면 된다.
3. 논블로킹 소켓 사용시, WSAEWOULDBLOCK 에러가 나올 경우, 다시 시도한다. 이는 송수신 버퍼의 상태가 비어 있거나 가득 차 있어 송수신이 불가능한 임시적인 상태이기 때문이다.
우리가 이전 글에서 활용했던 것은 소켓은 블로킹 소켓이다. 따로 옵션을 설정한 것 같지는 않은데? 그럼에도 불구하고, 우리가 만든 소켓이 기본적으로 블로킹 소켓으로 잡힌 것인데.. 논블로킹 소켓을 이야기하기 전에 먼저 블로킹 소켓에 대해 이야기해야 한다.
사실 핵심은 간단하다. 클라든 서버든, 데이터를 주고 받을 수 없는 상황(연결이 정상적으로 되지 않았다거나, 송수신 버퍼가 가득 찼다거나)이 되면, 해당 소켓은 '블록' 된다. 뭐... 소켓을 단말기라고 가정하면, 데이터가 끊겼을 때 웹 페이지가 먹통이 되었다가, 데이터 연결이 되면 다시 로드 되는 방식이라고 생각해도 된다.
그런데 논블로킹 소켓은 조금, 아주 약간 다르다. 즉, 데이터를 주고 받을 수 없는 상황에서도 보내려는 시도를 계속 한다. 그게 논블로킹 소켓이다. 이름처럼, 작은 논블로킹 소켓은 아무도 막을 수 없다. 🤣
기존의 블로킹 소켓을 논블로킹 소켓처럼 사용하려면, 사실 아래 옵션 정도만 주면 된다(GameServer.cpp 에서 정의) :
// listenSocket 생성. 기존과 동일하다
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 이 바로 그것인데, MS 문서에는 다음과 같이 나와 있다 :
즉, 기존에는 accept 를 위해서는 접속한 클라가 있어야 했고, connect 를 위해서는 접속이 성공해야 했으며, send/sendto 를 위해서는 요청한 데이터가 송신 버퍼에 복사되어야 했고, recv/recvfrom 이 되려면 수신 버퍼에 도착한 데이터가 있고, 그것이 유저 레벨 버퍼에 복사되어야 했다.
하지만 논블로킹 소켓은 just doesn't care. 그냥 될 때까지 계속 시도를 한다. 서버쪽 코드를 보자.
일단 server 주소를 만들고, 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;
cout << "Accept" << endl;
SOCKADDR_IN clientAddr;
int32 addrLen = sizeof(clientAddr);
그럼 이제 connect, recv, send 를 하는 부분을 보자.
// 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 문이 많은 것 같은데, 사실 핵심은 이 부분이다 :
// 원래 블록했어야 했는데... 너가 논블로킹으로 하라며?
if (::WSAGetLastError() == WSAEWOULDBLOCK)
continue;
사실 이전까지는 socket 이 INVALID_SOCKET 이거나, recvLen 이 SOCKET_ERROR 거나, send 가 SOCKET_ERROR 면 바로 return 을 해줬었다. 블로킹 소켓의 경우에는.
그런데 논블로킹 소켓의 경우, 작은 논블로킹 소켓을 건드리면 안되기 때문에, 작은 논블로킹 소켓은 될때까지 계속 츄라이를 한다. 그러다 보니 에러가 WSAEWOULDBLOCK 일 경우에는, 계속 시도를 한다. 참고로 WSAEWOULDBLOCK 이란...
단순히 소켓 버퍼에 데이터가 없거나 가득 차 있는 일시적인 상황을 의미한다.
따라서 각 단계마다 while loop 이 추가되어 각 단계를 계속 시도하는 것이다.
참고로 클라이언트 쪽도 별반 다르지 않다. 각각 Connect, Send/Recv 부분으로 쪼개 보자.
먼저 Connect 쪽은 다음과 같다.
// Connect
while (true)
{
if (::connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
{
// 원래 블록했어야 했는데... 너가 논블로킹으로 하라며?
if (::WSAGetLastError() == WSAEWOULDBLOCK)
continue;
// 이미 연결된 상태라면 break
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)
{
// 원래 블록했어야 했는데... 너가 논블로킹으로 하라며?
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);
}
1초 간격으로 송수신을 시도하는 것을 알 수 있다 😁
'Game Dev > Game Server' 카테고리의 다른 글
[C++ 게임 서버] 3-9. WSAEventSelect 모델 (0) | 2023.10.16 |
---|---|
[C++ 게임 서버] 3-8. Select 모델 (0) | 2023.09.25 |
[C++ 게임 서버] 3-6. 소켓 옵션 (0) | 2023.09.22 |
[C++ 게임 서버] 3-5. UDP 서버 실습 (0) | 2023.09.22 |
[C++ 게임 서버] 3-4. TCP vs UDP (0) | 2023.09.21 |