C++ Event 기반 Overlapped 모델 실습

정은성·2023년 5월 31일
0
post-thumbnail

※ Rookiss님의 [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 강의를 보고 정리한 글입니다.

overlapped는 non-blocking + aync 방법으로 진행된다. 이번엔 Send와 Recv만 Overlapped 모델을 적용 시켜볼 것이다.

Event 기반 Overlapped 모델 진행 순서

  1. 비동기 입출력을 지원하는 소켓 생성 + 통지 받기 위한 이벤트 생성\

  2. 비동기 입출력 함수 실행(WSASend, WSARecv) ⇒ 만든 이벤트 객체를 넘겨줌

    ⇒ 실행 한 이후 바로 비동기 작업이 완료되지 않으면, WSA_IO_PENDING 오류 코드나 나옴.

    ⇒ 운영체제는 작업이 완료되면 이벤트 객체를 signaled 상태로 만들어서 완료 상태를 알려줌

  3. WSAWaitForMultipleEvents함수 호출해서 이벤트 객체의 signal 판별

  4. WSAGetOverlappedResult 호출해서 비동기 입출력 결과 확인 및 데이터 처리

관련 함수

  • WSAGetOverlappedResult 파라미터 1) 비동기 소켓 2) 넘겨준 overlapped 구조체 3) 송/수신된 바이트 수 4) 비동기 입출력 작업이 끝날때까지 대기 여부 ⇒ WSAWaitForMultipleEvents로 signal이 되었을 때 실행 되므로 무조건 바로 실행됨 → 일단 False 5) 비동기 입출력 작업 관련 부가 정보. 거의 사용 안 함.
  • WSASend, WSARecv 파라미터 1) 비동기 입출력 소켓 2) WSABUF 배열의 시작주소 WSABUF는 buffer와 보낼 바이트 수를 가지고 있다. 3) WSABUF 배열의 개수 WSABUF를 하나가 아닌 배열로 보낼 수 있다. 그 때 그 배열의 개수를 뜻한다. 4) 상세 옵션 → 추후 조사 (하지만 지금은 0) 5) WSAOVERLAPPED 구조체 주소 값 6) 입출력이 완료되면 OS가호출할 콜백 함수 ⇒ 이벤트 방식으로 구현할 것이기 때문에 아직 사용 하지 않음.

구현

overlapped활용을 위해 Session 구조체에 Overlapped를 추가해준다.

struct Session {
	SOCKET socket = INVALID_SOCKET;
	char recvBuffer[BUFSIZE] = {};
	int32 recvBytes = 0;
	int32 sendBytes = 0;
	WSAOVERLAPPED overlapped = {};
};

소켓 설정

소켓 실행부터 listen까지의 과정이다.

WSAData wsaData;
if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	return 0;

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;

Accept

accept 처리는 non-blocking + sync 방법으로 진행해준다. (이번엔 Recv와 Send만 aync로 처리)

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;
	}

Recv

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 = BUFSIZE;
	DWORD recvLen = 0;
	DWORD flags = 0;
	if (::WSARecv(clientSocket, &wsaBuf, 1, &recvLen, &flags, &session.overlapped, nullptr) == SOCKET_ERROR) {
		if (::WSAGetLastError() == WSA_IO_PENDING) {
			// Pending
			::WSAWaitForMultipleEvents(1, &wsaEvent, TRUE, WSA_INFINITE, FALSE);
			::WSAGetOverlappedResult(session.socket, &session.overlapped, &recvLen, FALSE, &flags);
		}
		else {
			//문제 상황
			break;
		}

	}

}

Pending

WSARecv를 실행 했을 때 바로 실행이 성공하는 경우가 드물게 있을 것 이다.

하지만 그렇지 않는 상황에선 WSA_IO_PENDING 이라는 오류가 뜨게된다.

그 상황에선 WSAWaitForMultipleEvents를 이용해 이벤트가 signaled가 될 때 까지 기다려주고

WSAGetOverlappedResult를 통해 결과 값을 받아준다.

Send

Client에선 Send를 진행해보자.

char sendBuffer[100] = "Hello World";
WSAEVENT wsaEvent = ::WSACreateEvent();
WSAOVERLAPPED overlapped = {};
overlapped.hEvent = wsaEvent;
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 {
			//ERROR
			break;
		}
	}

Send도 마찬가지로 진행한다.

주의 사항

현재 송/수신을 WSABUF를 통해 Buffer를 지정하고 사용해 주고 있다. 송/수신이 진행 중일 땐 WSABUF를 건드리는 것은 괜찮지만 Buffer를 건드려선 안된다. Buffer를 건들이게 될 경우 의도한 것과 달리 송/수신 될 수 있기 때문이다.

0개의 댓글