KoreanFoodie's Study
[C++ 게임 서버] 3-10. Overlapped 모델 (이벤트 기반) 본문
[C++ 게임 서버] 3-10. Overlapped 모델 (이벤트 기반)
핵심 :
1. 블로킹 방식을 사용하면 함수 호출 후, 작업 완료시까지 대기를 하며 논블로킹 방식을 사용하면 다른 작업을 한다.
2. 논블로킹/비동기 방식의 모델이 AIO(Overlapped IO) 모델로, 운영체제는 이벤트 객체를 Signaled 상태로 만들어 완료 상태를 알려준다.
3. WSAWaitForMultipleEvents 함수를 호출해서 이벤트 객체의 Signal 을 판별 후, WSAGetOverlappedResult 를 호출해 비동기 입출력 결과를 확인한다.
이전 글에서 정리했던 Select 모델 말고, 실제로 사용할 IOCP 모델을 위한 기초 작업을 해 보자.
이번 글에서는 Overlapped 모델에 대해 다룰 것인데, 일단 그 전에 기초적인 개념을 다시 한 번 짚고 넘어가자.
이전에 사용했던 위 개념들에 대해 다시 짚고 넘어갈 필요가 있다.
먼저 출처를 알 수 없는(?) 위 사진을 보면, 두 가지 개념이 짬뽕될 때 어떻게 되는지가 잘 나와 있다.
사실 Blocking/NonBlocking 의 차이는 간단하다. Blocking 방식일 때는, 특정 호출을 시도를 한 후, 끝나기 전까지 다른 일을 하지 못하고 대기를 한다. 반면 NonBlocking 방식일 때는 특정 호출을 시도하고 그 동안(완료 Callback 이 올 때 까지) 다른 일을 한다.
이렇게 풀어 써 보니, 당연히 NonBlocking 을 써야 할 것만 같은 기분이 든다 😅
동기와 비동기는, 이미 잘 알듯이 해당 동작이 동기적으로 이루어지는지, 비동기적으로 이루어지는지의 차이다.
위의 표에 따른 모델의 분류를 그려보면...
위처럼 나오는데, 사실 Blocking/Asynchronous 조합은... 거의 안쓴다고 생각하면 된다.
대체로 고민해 봐야 할 것은 Blocking/Synchronous 와 Non-Blocking/Asynchronous 조합일 것이다 🙃
실제로, System Call 을 AIO 방식으로 하면 위처럼 UML 을 간단히 그려볼 수 있을 것이다 😊
그럼 이제 본격적으로 Overlapped IO (비동기 + 논블로킹) 모델을 알아보자. 사실 요약하면 다음과 같이 정리된다 :
- 비동기 입출력 지원하는 소켓 생성 + 통지 받기 위한 이벤트 객체 생성
- 비동기 입출력 함수 호출 (1에서 만든 이벤트 객체를 같이 넘겨줌)
- 비동기 작업이 바로 완료되지 않으면, WSA_IO_PENDING 오류 코드
- 운영체제는 이벤트 객체를 signaled 상태로 만들어서 완료 상태 알려줌
- WSAWaitForMultipleEvents 함수 호출해서 이벤트 객체의 signal 판별
- WSAGetOverlappedResult 호출해서 비동기 입출력 결과 확인 및 데이터 처리
조금 더 구체적으로 파 보자.
2번을 보면, 비동기 입출력 함수를 호출하는데, Overlapped 함수로는 WSASend 와 WSARecv 가 있다.
인자로는 다음과 같은 것들이 필요하다 :
한국말로 잘 풀어 쓰면... 다음과 같다 :
1) 비동기 입출력 소켓
2) WSABUF 배열의 시작 주소 + 개수 // Scatter-Gather
3) 보내고/받은 바이트 수
4) 상세 옵션인데 0
5) WSAOVERLAPPED 구조체 주소값
6) 입출력이 완료되면 OS가 호출할 콜백 함수
5번에서... 아직 일이 끝나지 않은(Pending) 상태이면, WSAWaitForMultipleEvents 함수를 호출해서 Signal 을 판별하고, 6번에서 결과를 확인한다.
WSAGetOverlappedResult 를 통해 결과를 호출할 때, 필요한 인자는 다음과 같다 :
한국말로 풀어쓰면 다음과 같다 🤣 :
1) 비동기 소켓
2) 넘겨준 overlapped 구조체
3) 전송된 바이트 수
4) 비동기 입출력 작업이 끝날때까지 대기할지?
5) 비동기 입출력 작업 관련 부가 정보. 거의 사용 안 함.
이제 코드를 보자.
일단, 기존에 사용했던 Session 에서 WSAOVERLAPPED 구조체를 추가해 줄 것이다.
struct Session
{
SOCKET socket = INVALID_SOCKET;
char recvBuffer[BUFSIZE] = {};
int32 recvBytes = 0;
WSAOVERLAPPED overlapped = {};
};
일단, 클라이언트 소켓을 만드는 부분과 이벤트 객체 생성은 사실 거의 비슷하다.
// 클라이언트 소켓 생성
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();
session.overlapped.hEvent = wsaEvent;
그럼 이제 비동기 통신을 아래와 같이 하면 된다 :
while (true)
{
WSABUF wsaBuf;
wsaBuf.buf = session.recvBuffer;
wsaBuf.len = BUFSIZE;
DWORD recvLen = 0;
DWORD flags = 0;
// 2번 : 비동기 입출력 함수 호출
if (::WSARecv(clientSocket, &wsaBuf, 1, &recvLen, &flags, &session.overlapped, nullptr) == SOCKET_ERROR)
{
// 3번 : 비동기 작업이 바로 완료되지 않으면...
if (::WSAGetLastError() == WSA_IO_PENDING)
{
// 4~6번 : 이벤트 객체가 Signaled 될 때까지 기다린 후, Signal 판별하고, 결과 확인
::WSAWaitForMultipleEvents(1, &wsaEvent, TRUE, WSA_INFINITE, FALSE);
::WSAGetOverlappedResult(session.socket, &session.overlapped, &recvLen, FALSE, &flags);
}
else
{
// TODO : 문제 있는 상황
break;
}
}
cout << "Data Recv Len = " << recvLen << endl;
}
// 소켓 사용이 완료되었으면 잘 닫아준다
::closesocket(session.socket);
::WSACloseEvent(wsaEvent);
이때, ::WSAGetOverlappedResult 함수에서 &session.overlapped 를 사용하는데, 이때 오염이 발생해서는 안된다.
위의 작업들은 전부 서버에서 진행한 것으로, 이제 클라이언트 쪽도 약간의 수정을 해 주어야 한다. 크게 다른 것은 없으니, 참고로 보고 넘어가도록 하자 😄
char sendBuffer[100] = "Hello World";
WSAEVENT wsaEvent = ::WSACreateEvent();
WSAOVERLAPPED overlapped = {};
overlapped.hEvent = wsaEvent;
// Send
while (true)
{
WSABUF wsaBuf;
wsaBuf.buf = sendBuffer;
wsaBuf.len = 100;
DWORD sendLen = 0;
DWORD flags = 0;
if (::WSASend(clientSocket, &wsaBuf, 1, &sendLen, flags, &overlapped, nullptr) == SOCKET_ERROR)
{
if (::WSAGetLastError() == WSA_IO_PENDING)
{
// Pending
::WSAWaitForMultipleEvents(1, &wsaEvent, TRUE, WSA_INFINITE, FALSE);
::WSAGetOverlappedResult(clientSocket, &overlapped, &sendLen, FALSE, &flags);
}
else
{
// 진짜 문제 있는 상황
break;
}
}
cout << "Send Data ! Len = " << sizeof(sendBuffer) << endl;
this_thread::sleep_for(1s);
}
'Game Dev > Game Server' 카테고리의 다른 글
[C++ 게임 서버] 3-12. Completion Port 모델 (0) | 2023.10.18 |
---|---|
[C++ 게임 서버] 3-11. Overlapped 모델 (콜백 기반) (0) | 2023.10.18 |
[C++ 게임 서버] 3-9. WSAEventSelect 모델 (0) | 2023.10.16 |
[C++ 게임 서버] 3-8. Select 모델 (0) | 2023.09.25 |
[C++ 게임 서버] 3-7. 논블로킹 소켓 (0) | 2023.09.25 |