회사에 복직해서 따로 공부할 시간이 없다. 몰아서 쓰는 것을 피하고 싶지만 어쩔 수 없다.. 개강 전까지 책이랑 병행하면서 기초를 다지고 네트워크 라이브러리를 짜보려고 한다
논블로킹 소켓은 블로킹에 빠지지 않고 송수신을 기다리는 동안 다른 작업을 할 수 있고 하나의 스레드에서 여러 개의 소켓을 다룰 수 있다는 장점이 있다. 하지만 이 완벽할 것만 같은 논블로킹 소켓에도 단점이 있다. 다음과 같다.
위 두 문제를 해결하기 위해 Overlapped I/O를 사용한다. 소켓이 Overlapped I/O에 액세스하기 위해선 상태를 보관하는 구조체가 필요하다. 소켓이 송수신을 하면 즉시 결과가 반환되는데 송수신 중이라면 Pending값이 반환된다. 이후부터 Overlapped의 구조체를 통해 송수신 데이터를 볼 수 있다. 즉, recv나 send 함수를 재시도하지 않으며 메모리를 직접 주고 받기 때문에 복사 연산에 대한 리소스도 발생하지 않는다.
주의해야 할 점이 있다. 메모리를 직접 주고 받기 때문에 비동기로 처리되는 작업이 완료될 때까지 송수신에 사용된 데이터와 Overlapped 구조체는 없애거나 변경하면 안된다.
만약 동시에 여러 개의 Overlapped I/O 작업을 하고 싶다면, 새 객체를 선언해야 한다. 당연하게도 입력에 사용되는 데이터의 메모리도 서로 달라야 한다.
#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>
#include <atomic>
#include <mutex>
#include <windows.h>
#include <future>
#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;
}
const int32 BUFSIZE = 1000;
struct Session
{
// 시작주소는 Overlapped 구조체를 가리킴
WSAOVERLAPPED overlapped = {};
SOCKET socket = INVALID_SOCKET;
char recvBuffer[BUFSIZE] = {};
int32 recvBytes = 0;
};
void CALLBACK RecvCallBack(DWORD error, DWORD recvLen, LPWSAOVERLAPPED overlapped, DWORD flags)
{
cout << "Data Recv Len Callback = " << recvLen << endl;
// TODO : 에코 서버를 만든다면
// 시작값이 overlapped 구조체이므로, 이렇게 캐스팅해서 보낼 수 있음.
Session* session = (Session*)overlapped;
}
int main()
{
WSAData wasData;
if (::WSAStartup(MAKEWORD(2, 2), &wasData) != 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;
cout << "Accept" << endl;
// Overlapped 모델 (이벤트 기반)
// - 비동기 입출력 지원하는 소켓 생성
// - 비동기 입출력 함수 호출 (1에서 만든 이벤트 객체를 같이 넘겨줌)
// - 비동기 작업이 바로 완료되지 않으면, WSA_IO_PENDING 오류 코드
// - 비동기 입출력 함수 호출한 쓰레드를 -> Alertable Wait 상태로 만든다
// ex) WaitForSingleObjectEx, WaitForMultipleObjectsEx, SleepEx, WSAWaitForMultipleEvents
// - 비동기 IO 완료되면, 운영체제는 완료 루틴 호출
// - 완료 루틴 호출이 모두 끝나면, 쓰레드는 Alertable Wait 상태에서 빠져나온다.
// Reactor Pattern (뒤늦게 논블로킹 소켓. 소켓 상태 확인 후 -> 뒤늦게 recv send 호출)
// Proactor Pattern (미리 recv send 호출. Overlapped WSA)
while (true)
{
SOCKADDR_IN clientAddr;
int32 addrLen = sizeof(clientAddr);
SOCKET clientSocket;
while (true)
{
clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
if (clientSocket != INVALID_SOCKET)
break;
if (::WSAGetLastError() == WSAEWOULDBLOCK)
continue;
// 그외 오류 상황 -> 종료
return 0;
}
Session session = Session{ clientSocket };
WSAEVENT wsaEvent = ::WSACreateEvent();
cout << "Client Connected" << endl;
while (true)
{
WSABUF wsaBuf;
wsaBuf.buf = session.recvBuffer;
wsaBuf.len = BUFSIZE;
DWORD recvLen = 0;
DWORD flags = 0;
if (::WSARecv(clientSocket, &wsaBuf, 1, &recvLen, &flags, &session.overlapped, RecvCallBack) == SOCKET_ERROR)
{
if (::WSAGetLastError() == WSA_IO_PENDING)
{
// Pending
//Alertable Wait
::SleepEx(INFINITE, TRUE);
//::WSAWaitForMMultipleEvents(1, &wsaEvent, TRUE, WSA_INFINITE, TRUE);
}
else
{
// TODO : 문제있는 상황
break;
}
}
cout << "Data Recv Len = " << recvLen << endl;
}
}
// 윈속 종료
::WSACleanup();
}
게임서버 프로그래밍 교과서(저:배현직),
[C++과 언리얼로 만드는 MMORPG 게임개발 시리즈] Part4: 게임서버(강사 : 루키스)를 학습하고 정리한 내용입니다.