지난 이벤트 기반 overlapped 모델은 이벤트 받을 수 있는 개수가 64개로 제한되어 있었다.
이번에 알아볼 콜백기반 overlapped 모델은 이벤트 기반 모델의 단점을 해결한 모델이다.
GameServer.cpp
const int32 BUFSIZE = 1000;
struct Session
{
WSAOVERLAPPED overlapped = {};
SOCKET 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 : 에코 서버를 만든다면 WSASend()
// param 중 유용한 정보는 overlapped.
// 구조체를 변형하여 원하는 구조체로 casting하여 사용한다.
Session* session = (Session*)overlapped;
}
int main()
{
WSAData wsaData;
if (::WSAStartup(MAKEWORD(2, 2), &wsaData))
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 IO (비동기 + 논블록킹)
// - Overlapped 함수를 건다 (WSARecv, WSASend)
// - Overlapped 함수가 성공했는지 확인 후,
// -> 성공했으면 결과 얻어서 처리
// -> 실패했으면 사유를 확인
// 1) 비동기 입출력 소켓
// 2) WSABUf 배열의 시작 주소 + 개수 // Scatter-Gather
// char sendBuffer[100];
// WSABUF wsaBuf[2];
// wsaBuf[0].buf = sendBuffer;
// wsaBuf[0].len = 100;
// char sendBuffer2[100];
// wsaBuf[1].buf = sendBuffer2;
// wsaBuf[1].len = 100;
// 3) 보내고/받은 바이트 수
// 4) 상세 옵션 0
// 5) WSAOVERLAPPED 구조체 주소값
// 6) 입출력이 완료되면 OS가 호출할 콜백 함수
// WSASend
// WSARecv
// Overlapped 모델(Completion Routine 콜백 기반)
// - 비동기 입출력 지원하는 소켓 생성
// - 비동기 입출력 함수 호출(완료 루틴의 시작 주소를 넘겨준다)
// - 비동기 작업이 바로 완료되지 않으면, WSA_IO_PENDING 오류 코드
// - 비동기 입출력 함수 호출한 쓰레드를 -> Alertable Wait 상태로 만든다
// ex) WaitForSingleObjectEx, WaitForMultipleObjectsEx, SleepEx, WSAWaitForMultipleEvents
// - 비동기 IO 완료되면, 운영체제는 완료 루틴 호출
// - 완료 루틴 호출이 모두 끝나면, 쓰레드는 Alertable Wait 상태에서 빠져나온다.
// 1) 오류 발생시 0 아닌 값
// 2) 전송 바이트 수
// 3) 비동기 입출력 함수 호출 시 넘겨준 WSAOVERLAPPED 구조체의 주소값
// 4) 0
// void CompletionRoutine()
// Select 모델
// - 장점) 윈도우/리눅스 공통.
// - 단점) 성능 최하 (매번 등록 비용), 64개 제한
// WSAEventSelect 모델
// - 장점) 비교적 뛰어난 성능
// - 단점) 64개 제한
// Overlapped (이벤트 기반)
// - 장점) 성능
// - 단점) 64개 제한
// Overlapped (콜백 기반)
// - 장점) 성능
// - 단점) 모든 비동기 소켓 함수에서 사용 가능하진 않음(accept). 빈번한 Alertable Wait으로 인한 성능 저하
// pattern 용어
// Reactor Pattern (~뒤늦게. 논블록킹 소켓. 소켓 상태 확인 후 -> 뒤늦게 recv send 호출)
// Proactor Pattern (~미리, 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
// alertable wait apc 검색해보기
// 코드 진행
// - alertable wait 상태 > apc 큐 모두 비우기 > alertable wait 해줬던 이후부터 다시 진행
::SleepEx(INFINITE, TRUE);
// 아래 방식은 기다릴 수 있는 event 개수가 64개로 정해져 있어 불편
// socket과 event를 1:1로 매핑해줘야 해서 불편
//::WSAWaitForMultipleEvents(1, &wsaEvent, TRUE, WSA_INFINITE, TRUE);
}
else
{
// TODO : 문제 있는 상황
break;
}
}
cout << "Data Recv Len = " << recvLen << endl;
}
::closesocket(session.socket);
//::WSACloseEvent(wsaEvent);
}
// 윈속 종료
::WSACleanup();
}
콜백기반 역시 blocking 부분이 존재한다. alertable wait 상태에서 thread가 기다리는 부분이 있다.
콜백 함수 RecvCallback에서 WSAOVERLAPPED 포인터를 Session 포인터로 바꾸기 위해 Session 구조체의 첫 변수를 WSAOVERLAPPED로 배치하여 사용한 부분 잘 기억해두자.
1. 이벤트방식보다 나은 점은 무엇?
- 소켓과 이벤트 1:1 매칭해서 기억해야 할 필요 없음.
2. 운영체제가 작업이 완료되었다고 판단하고 콜백함수를 알아서 부르지 않는데, 운영체제가 콜백함수를 호출하기 위해 필요한 작업은?
- 비동기 입출력 함수 호출한 쓰레드를 Alertable Wait 상태로 만들면 그때 완료 루틴이 호출된다.
- 완료 루틴 호출 모두 끝나면 쓰레드는 Alertable Wait 상태에서 빠져나온다.
3. Alertable wait 상태로 만들어주기 위한 함수는 어떤 것이 있나?
- SleepEx(INFINITE, TRUE), WSAWaitForMultipleEvents(1, &wsaEvent, TRUE, WSA_INFINITE, TRUE);
4. Alertable wait 상태가 종료되면 다음 함수 흐름은 무엇?
- Alertable wait 상태 만든 함수 다음 함수가 실행됨.
5. 콜백함수에 overlapped 구조체 포인터가 들어와도 소켓에 관해 알 수 있는 정보가 없는데, 해결방법은?
- 내가 정의한 구조체에 overlapped 구조체 멤버변수가 첫번째로 위치하게 한 후, 콜백함수에서 내가 정의한 구조체로 캐스팅하여 사용한다.
6. 지금까지 본 모델 중 리눅스환경에도 동작하는 모델은?
- Select 모델
참조 : [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 - 인프런 | 강의 (inflearn.com)
네트워크 라이브러리 제작(Socket Utils) (0) | 2021.11.27 |
---|---|
Completion Port 모델 (0) | 2021.11.19 |
Overlapped 모델(이벤트기반) (0) | 2021.11.17 |
WSAEventSelect 모델 (0) | 2021.11.16 |
Select 모델 (0) | 2021.11.11 |
댓글 영역