C++ WSAEventSelect 모델 실습

정은성·2023년 5월 30일
1
post-thumbnail

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

select모델은 초기화와 등록을 계속 반복해야한다는 아쉬움이 있다.

이번에 알아볼 WSAEventSelect 모델은 이벤트를 기반으로한 모델이다.

WSAEventSelect?

⇒ 소켓과 관련된 네트워크 이벤트를 이벤트 객체를 통해 감지한다.

소켓과 이벤트 객체를 연동시켜야한다. ⇒ 소켓 개수만큼 이벤트 함수를 만들어주어야함.

연동 함수: WSAEventSelect(socket,event,networkEvents)

이벤트 객체 관련 함수

  • WSACreateEvent() → 이벤트를 생성한다. Manual-Reset(수동리셋), Non-Signaled(접근 불가)상태에서 시작한다.
  • WSACloseEvent() → 이벤트를 삭제한다.
  • WSAWaitForMultipleEvents() → 신호 상태를 감지한다.
  • WSAEnumNetworkEvents() → 이벤트가 발동했긴했지만 구체적인 네트워크 이벤트를 알아낸다.

네트워크 이벤트

현재 사용할 네트워크 이벤트는 5가지 정도 있다.

  1. FD_ACCEPT: 접속한 클라이언트가 있다면 accept 해주기
  2. FD_READ: 데이터 수신이 가능하면 recv,recvfrom해주기
  3. FD_WRITE: 데이터 송신이 가능하면 send,sendto 해주기
  4. FD_CLOSE: 상대가 접속 종료 시
  5. FD_CONNECT: 통신을 위한 연결 절차 완료 시

주의사항

  • WSAEventSelect 함수를 호출하면, 해당 소켓은 자동으로 논블로킹 소켓으로 전환됨
  • accept() 함수에서 리턴하는 소켓은 listenSocket과 동일한 속성을 가지게됨 ⇒ 그래서 네트워크 이벤트를 수정해주는 등의 작업이 필요
  • 드물게 WSAEWOULDBLOCK 오류가 뜰 수 있으니 예외처리가 필요하다
  • 이벤트 발생 시, 적절한 소켓 함수를 호출해야함. ⇒ 하지않으면 다음 번엔 동일 네트워크가 다시는 발생하지않음. ex) FD_READ가 왔을 때 recv() 함수를 진행해야함. 하지않으면 FD_READ 이벤트가 두 번 다시 발생 하지않음.

구현

소켓 세팅

소켓 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;

cout << "Accept" << endl;

Listen 소켓 이벤트 세팅

vector<WSAEVENT> wsaEvents;
vector<Session> sessions;
sessions.reserve(100);

WSAEVENT listenEvent = ::WSACreateEvent(); // 이벤트 생성
wsaEvents.push_back(listenEvent);
sessions.push_back(Session{ listenSocket });

// listenSocket은 연결되었을 때, 연결을 끊었을 때 이벤트가 활성화 된다.
if (::WSAEventSelect(listenSocket, listenEvent, FD_ACCEPT | FD_CLOSE) == SOCKET_ERROR)
	return 0;

listenSocket은 클라이언트와 연결된 세션은 아니지만 WSAEvnetSelect모델 특성상 동일한 인덱스를 활용되게끔 해야 관리가 쉬우니 일단 session을 만들어 sessions에 등록해준다.

신호 감지

이제 WSAWaitForMultipleEvents함수를 통해 이벤트 신호를 감지해보자.

파라미터: WSAWaitForMultipleEvents(이벤트 수, 이벤트 주소, waitAll 유무, 지금은 false)

리턴값: 완료된 첫번째 인덱스 + WSA_WAIT_EVENT_0

while (true) {
	
int32 index = ::WSAWaitForMultipleEvents(wsaEvents.size(), &wsaEvents[0], FALSE, WSA_INFINITE,FALSE);
// 공식 문서를 보면 WSA_WAIT_EVENT_0라는 걸 더해줌. 빼줘야 정상적으로 사용가능
if (index == WSA_WAIT_FAILED)
	continue;

index -= WSA_WAIT_EVENT_0;

// WSAEnumNetworkEvents에 포함되어있음.
	//::WSAResetEvent(wsaEvents[index]);

모두 기다리지 않고, 무한히 기다리는 wsaEvents의 이벤트 신호를 감지하는 함수로 사용하고 있다.

리턴값으론 완료된 이벤트의 인덱스가 나오는데 이상하게 WSA_WAIT_EVENT_0라는것이 더해져있다. 그러니 우리가 활용해 주기위해서 저 값을 빼주자.

그 후 이 이벤트는 Manual-Reset(수동 리셋) 이기 때문에 WSAReserEvent() 함수를 통해 리셋해 주어야하지만, 다음 과정에 활용하는WSAEnumNetworkEvents()에서 이것을 내부적으로 작동해주니 일단 해주지않는다!

신호 판별

WSAEnumNetworkEvents함수를 사용해 어떤 네트워크 이벤트인지 판별해준다.

WSANETWORKEVENTS networkEvents;
if (::WSAEnumNetworkEvents(sessions[index].socket, wsaEvents[index], &networkEvents) == SOCKET_ERROR)
	return 0;

그럼 네트워크 이벤트와 에러코드를 담고있는 WSANETWORKEVENTS형의 데이터를 통해 우리가 어떤 이벤트인지 확인 할 수 있다.

lNetworkEvents: 이벤트 종류

iErrorCode: 에러 코드

처리

이제 어떤 이벤트인지 판별까지했으니 그에 따라 후처리를 해주자. 먼저 FD_ACCEPT 이벤트 부터 처리 해주자.

Accept

현재 우리는 ACCEPT를 하는 소켓은 listening 소켓밖에 없으므로 그에 맞게 처리해주면 된다.

if (networkEvents.lNetworkEvents & FD_ACCEPT) {

	if (networkEvents.iErrorCode[FD_ACCEPT_BIT] != 0) // FD_ACCEPT에 맞는 에러
		continue;

	//ACCEPT
	SOCKADDR_IN clientAddr;
	int32 addrLen = sizeof(clientAddr);
	SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);

	if (clientSocket != INVALID_SOCKET) {
		cout << "Client COnnected" << endl;

		// 새로운 소켓 세팅
		WSAEVENT clientEvent = ::WSACreateEvent();
		wsaEvents.push_back(clientEvent);
		sessions.push_back(Session{ clientSocket });

		//클라이언트 소켓은 READ와 WRITE에 활용할 것!
		if (::WSAEventSelect(clientSocket, clientEvent, FD_READ | FD_WRITE | FD_CLOSE) == SOCKET_ERROR)
			return 0;

	}
}

recv/send

현재상황에선 FD_READ와 FD_WRITE는 ClientSocket에서만 처리한다. 그를 통해 처리해주자.

if (networkEvents.lNetworkEvents & FD_READ || networkEvents.lNetworkEvents & FD_WRITE) {
	// Error-Check: READ인 상황인데 READ 오류라면
	if ((networkEvents.lNetworkEvents & FD_READ) && networkEvents.iErrorCode[FD_READ_BIT] != 0)
		continue;
	// Error-Check: WRITE인 상황인데 WRITE오류라면
	if ((networkEvents.lNetworkEvents & FD_WRITE) && networkEvents.iErrorCode[FD_WRITE_BIT] != 0)
		continue;

	// 세션 받아오기
	Session& s = sessions[index];

	//recvByte가 0? -> 모두 send함 -> recv받아야함.
	if (s.recvBytes == 0) {
		int32 recvLen = ::recv(s.socket, s.recvBuffer, BUFSIZE, 0);
		if (recvLen == SOCKET_ERROR && ::WSAGetLastError() != WSAEWOULDBLOCK) {
			continue;
		}

		s.recvBytes = recvLen;
		cout << "Recv Data= " << recvLen << endl;
	}

	// Send
	if (s.recvBytes > s.sendBytes) {
		int32 sendLen = ::send(s.socket, &s.recvBuffer[s.sendBytes], s.recvBytes - s.sendBytes, 0);
		if (sendLen == SOCKET_ERROR && ::WSAGetLastError() != WSAEWOULDBLOCK) {
			continue;
		}

		s.sendBytes += sendLen;
		if (s.recvBytes == s.sendBytes) {
			s.recvBytes = 0;
			s.sendBytes = 0;
		}

		cout << "Send Data= " << sendLen << endl;
	}
}

중간에 SOCKET_ERROR가 난 부분에서 주의사항에서 말했던 가끔 뜨는 WSAEWOULDBLOCK인지 체크해주는 것을 볼 수 있다.

마무리

WSAEventSelect는 전체 리셋을 계속 해주지 않아도 된다는 장점을 가지고있다.

event 객체를 만들고 연동만 잘 시켜준다면? 모두 잘 매핑되어 있기 때문에 잘 동작한다.

물런 WSAResetEvent를 통해 리셋을 해주긴 해야하지만 그건 EnumNetworkEvents에서 간접적으로 해준다.

이 모델도 최대 갯수가 64개라는 단점이 있다. 그래서 많은 양을 처리해야하는 서버에선 쓸만하진않다. 하지만 자신만 처리해주는 클라이언트라면 쓸만하다!

1개의 댓글

comment-user-thumbnail
2023년 5월 30일

좋은 글 잘 읽고 갑니다~

답글 달기