Overlapped 방식
- Overlapped 방식은 이벤트 기반, 콜백 기반 두 종류로 나눌 수 있다.
- Overlapped는 비동기 + 논 블로킹 방식으로 이전의 Select 방식보다 성능적으로 뛰어나다고 알려져 있다.
- 비동기는 비동기 함수를 실행한 후 대기하는게 아니라 시간이 지난 후 통지를 받는 개념으로, 반환값을 통해 데이터가 왔는지 여부를 확인한 후 오지 않았을 때 각 방식의 로직을 수행한다.
- 이벤트 기반은 WSAEVENT를 통해 이벤트를 등록하고 이벤트의 상태를 체크해서 recv를 처리하는 방식
이벤트 기반
/* Server.cpp */
#include "pch.h"
#include "ThreadManager.h"
// WSAEventSelect = WSAEventSelect가 메인
// 소켓과 관련된 네트워크 이벤트를 [이벤트 객체]를 통해 감지
// 생성 : WSACreateEvent (수동 리셋 Manual-Reset + Non-Signaled 상태 시작)
// 삭제 : WSACloseEvent
// 신호 상태를 감지 : WSAWaitForMultipleEvents
// 구체적인 네트워크 이벤트 알아내기 : WSAEnumNetworksEvents
const int32 BUF_SIZE = 1000;
struct Session //접속한 클라당 하나의 세션을 갖는다
{
SOCKET socket = INVALID_SOCKET;
char recvBuffer[BUF_SIZE] = {}; // 모든 세션마다 고유의 버퍼가 있어야 한다
int32 recvBytes = 0; // 버퍼를 받는 크기를 설정
WSAOVERLAPPED overlapped = {};
};
int main()
{
SocketUtils::Init();
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;
// 이전에 사용했던 주소를 사용하기 위함
SocketUtils::SetReuseAddress(listenSocket, true);
if (SocketUtils::BindAnyAddress(listenSocket, 7777) == false)
return 0;
if (SocketUtils::Listen(listenSocket) == false)
return 0;
// - Overlapped 함수를 건다
// - Overlapped 함수가 성공했는지 확인
// -> 성공 시 결과를 얻어서 처리
// -> 실패 시 사유 확인
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();
session.overlapped.hEvent = wsaEvent; // 새로 추가된 점, 이벤트 방식, 콜백방식이 있는데 이번엔 이벤트 방식
cout << "Client Connected!" << endl;
while (true)
{
WSABUF wsaBuf;
wsaBuf.buf = session.recvBuffer;
wsaBuf.len = BUF_SIZE;
DWORD recvLen = 0;
DWORD flags = 0;
// 순서가 뒤바뀌었다! 원래 recv를 할 상황인지를 먼저 체크하고 recv를 호출했는데 여기선 recv를 먼저 해놓고 완료되었는지를 확인한다.
if (WSARecv(clientSocket, &wsaBuf, 1, &recvLen, &flags, &session.overlapped, nullptr) == SOCKET_ERROR)// 스캐터 게더, 모아서 보낼 수 있다.
{
if (WSAGetLastError() == WSA_IO_PENDING) // 상대방이 보낸 게 없다.
{
WSAWaitForMultipleEvents(1, &wsaEvent, TRUE, WSA_INFINITE, FALSE);
WSAGetOverlappedResult(session.socket, &session.overlapped, &recvLen, FALSE, &flags);
}
else
{
break;
}
}
cout << "Data Recv : " << session.recvBuffer << endl;
}
WSACloseEvent(wsaEvent);
}
SocketUtils::Clear();
}
- 이벤트 방식은 여전히 이벤트가 Signal될때까지 기다려야 한다.
- WSARecv 함수의 결과가 0이라면 Recv되었다는 것
- SOCKET_ERROR가 났다면, 에러값이 WSA_IO_PENDING인지 확인하고 맞다면 이벤트가 시그널될때까지 기다린다.
- 이벤트가 시그널되면 다음 코드가 실행된다.
콜백 기반
/* Server.cpp */
#include "pch.h"
#include "ThreadManager.h"
// Overlapped I/O (논블로킹 + 비동기)
const int32 BUF_SIZE = 1000;
struct Session //접속한 클라당 하나의 세션을 갖는다
{
WSAOVERLAPPED overlapped = {}; // 맨위로 올려놔야 한다.
SOCKET socket = INVALID_SOCKET;
char recvBuffer[BUF_SIZE] = {}; // 모든 세션마다 고유의 버퍼가 있어야 한다
int32 recvBytes = 0; // 버퍼를 받는 크기를 설정
};
void CALLBACK RecvCallback(DWORD error, DWORD recvLen, LPWSAOVERLAPPED overlapped, DWORD flags)
{
cout << "Data Recv Len Callback = " << recvLen << endl;
Session* pSession = reinterpret_cast<Session*>(overlapped);
cout << pSession->recvBuffer << endl;
}
int main()
{
SocketUtils::Init();
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;
// 이전에 사용했던 주소를 사용하기 위함
SocketUtils::SetReuseAddress(listenSocket, true);
if (SocketUtils::BindAnyAddress(listenSocket, 7777) == false)
return 0;
if (SocketUtils::Listen(listenSocket) == false)
return 0;
// - Overlapped 함수를 건다
// - Overlapped 함수가 성공했는지 확인
// -> 성공 시 결과를 얻어서 처리
// -> 실패 시 사유 확인
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();
//session.overlapped.hEvent = wsaEvent; // 새로 추가된 점, 이벤트 방식, 콜백방식이 있는데 이번엔 이벤트 방식
cout << "Client Connected!" << endl;
while (true)
{
WSABUF wsaBuf;
wsaBuf.buf = session.recvBuffer;
wsaBuf.len = BUF_SIZE;
DWORD recvLen = 0;
DWORD flags = 0;
// 등록해놓는 개념, 낚싯대를 던진다
// 오류가 발생하지 않고 즉시 수신이 완료되면 0을 반환, 그렇지 않으면 SOCKET_ERROR를 반환한다.
// 주의할 점은 여기 넘겨준 wsaBuf의 버퍼가 삭제되지 않도록 해야 한다.
// 완료통지가 되었을 때 등록된 콜백 함수를 호출해달라는 요청
if (WSARecv(clientSocket, &wsaBuf, 1, &recvLen, &flags, &session.overlapped, RecvCallback) == SOCKET_ERROR)// 스캐터 게더, 모아서 보낼 수 있다.
{
if (WSAGetLastError() == WSA_IO_PENDING) // 상대방이 보낸 게 없다.
{
// Alertable wait
::SleepEx(INFINITE, TRUE);
// 모든 스레드마다 APC 큐가 있는데, 큐에 일감이 들어가면 Alertable wait에서 깨어나게 된다.
// 말그대로 반수면상태인데, 여기서 깨어나자마자 콜백 함수를 호출하도록 유도해준다.
// 이 사실은 콜백 함수가 반환되고 아래 중괄호 닫는 괄호에 걸리는 것으로 확인할 수 있다.
}
else
{
break;
}
}
//cout << "Data Recv : " << session.recvBuffer << endl;
}
}
SocketUtils::Clear();
}
- 콜백 기반의 경우 WSARecv를 호출할 때 콜백함수를 등록할 수 있는데 현재 APC큐에 작업이 등록되면 Alertable Wait 상태일 때 기상해서 함수를 호출하게 된다.
- 큐에 작업이 등록되었다는 것은 콜백 함수가 등록되었다는 것을 의미하고, 콜백 함수가 등록되는 조건은 WSARecv가 0을 반환했을 때, 즉, 수신에 성공했을때를 의미한다.
'게임 서버 > [Inflearn_rookiss]올인원_클라&서버 연동' 카테고리의 다른 글
15. IocpCore (0) | 2023.12.11 |
---|---|
14. IOCP (0) | 2023.12.11 |
12. Select 방식 (0) | 2023.12.11 |
11. 논 블로킹 소켓 (1) | 2023.12.11 |
10. TCP vs UDP (0) | 2023.12.11 |
댓글