Coding Memo

WSAEventSelect 본문

Game Server (C++)

WSAEventSelect

minttea25 2022. 11. 23. 15:16

본 포스팅은 인프런에 등록되어 있는 Rockiss 님의 강의를 보고 간단하게 정리한 글입니다.


지난번에 사용했던 select의 단점으로는

 

1. 최대 개수가 얼마 안됨(64)

2. 매번 사용할 때마다 리셋하여 새로운 set을 만들어주어야함

 

이 있었다.

 

이번에는 WSAEventSelect를 사용하여 두 번째 단점을 해결할 수 있다.

 

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

 

먼저 WSAEventSelect 함수를 살펴보자.

int WSAAPI WSAEventSelect(
  [in] SOCKET   s,
  [in] WSAEVENT hEventObject,
  [in] long     lNetworkEvents
);

s: 이벤트를 확인할 소켓

hEventObject: 이벤트 오브젝트 (WSAEVENT)

lNetworkEvents: 네트워크 이벤트를 감지할 셋의 bitmask 조합(FD_XXX)

 

3번째 인자로 네트워크 이벤트를 감지할 셋의 bitmask를 넣는데 이벤트의 종류는 다음과 같다.

value value(bit-mask) meaning re-enabling functions
FD_ACCEPT 1 << 3 접속한 client가 있을 경우  accept
FD_READ 1 << 0 데이터가 수신 가능  recv, recvfrom
FD_WRITE 1 << 1 데이터가 송신 가능  send, sendto
FD_CONNECT 1 << 4 통신을 위한 연결이 완료 되었을 경우 .
FD_CLOSE 1 << 5 소켓이 닫혔을 경우 .

이 외에도 FD_OOB, FD_GROUP_QOS, FD_ROUTING_INTERFACE_CHANGE, FD_ADDRESS_LIST_CHANGE, FD_ALL_EVENTS가 있다.

 

반환 값은 성공적일 때 0, 그렇지 않을 때는 SOCKET_ERROR(-1) 반환을 한다. 또한 특정 에러가 있을 때는 WSAGetLastError()함수를 통해 가져올 수 있다.

 

관련 함수들은 다음과 같다.

이벤트 생성 WSACreateEvent
이벤트 삭제 WSACloseEvent
신호 상태 감지 WSAWaitForMultipleEvents
네트워크 이벤트 가져오기 WSAEnumNetworkEvents

참고: WSACreateEvent를 통해 생성된 event는 수동으로 리셋을 해주어야하고 non-signaled 상태로 시작된다.

 

 

또한 몇가지 주의 사항들이 있다.

1. WSAEventSelect 함수를 호출하면 해당 소켓은 자동으로 non-blocking 모드로 전환된다.

2. accept() 함수가 반환하는 소켓은 listenSocket과 동일한 속성을 가져서 clientSocket은 FD_READ, FD_WRTIE 등을 다시 등록할 필요가 있다.

3. WSAGetLastError() 값으로 WSAEWOULDBLOCK 에러가 뜰 수 있으니 확인이 필요하다.

4. 이벤트 발생 시에 반드시 적절한 소켓에 대한 함수 호출이 필요하다. 만약 호출되지 않는다면 다음 번에 동일한 네트워크이벤트가 발생하지 않는다. (ex - FD_READ 이벤트가 발생했다면 반드시 recv() 등의 적절한 함수가 호출이 되어야만 한다.)


한 가지 더 확인하고 넘어가야할 함수는 WSAWaitForMultipleEvents 이다.

이 함수는 여러개의 이벤트를 등록하고 그 이벤트들을 1개 또는 모두 기다릴지 정할 수있는 함수로 이 함수를 통해 신호 상태(이벤트 발생 상태)를 탐지 할 수 있다.

DWORD WSAAPI WSAWaitForMultipleEvents(
  [in] DWORD          cEvents,
  [in] const WSAEVENT *lphEvents,
  [in] BOOL           fWaitAll,
  [in] DWORD          dwTimeout,
  [in] BOOL           fAlertable
);

cEvents: 이벤트 개수

*lpEvents: 이벤트 시작 주소 값

fWaitAll: true 모든 이벤트를 기다리고 false면 순서상관없이 하나만 기다림

dwTimeout: timeout 시간

fAlertable: alertable wait state로 만들 수 있음 (이 경우 WSA_WAIT_IO_COMPLETION을 return하고 이후 WSAWaitForMultipleEvents를 다시 호출해야 된다고 나와있다.)

(I/O completion routine에 대해서 좀 더 찾아보아야 겠다...)

 

반환 값은 좀 복잡할 수 있다.

위에 언급했던 것과 같이 fAlertable 값이 true면 WSA_WAIT_IO_COMPLETION을 return한다.

그렇지 않으면 -WSA_WAIT_EVENT_0 값을 return 하는데 이 값은 이벤트가 발생한 이벤트 오브젝트의 lphEvents 배열 index 값이다. 만약 2개 이상의 이벤트가 발생했다면 해당 index중 가장 작은 index를 반환한다.

timout이 일어나면 WSA_WAIT_TIMEOUT을 반환한다.


마지막으로 WSAEnumNetworkEvents 함수이다. 간단히만 보겠다.

 

이 함수는 해당 소켓에 대해 발생한 이벤트를 확인하는데 사용된다.

즉, WSAEventSelect과 WSAWaitForMultipleEvents 호출 이후 이벤트를 확인할 때 사용된다.

추가적으로 eventobject가 이 함수를 통해 reset이 되기 때문에 WSAResetEvent로 따로 초기화할 필요가 없다.

int WSAAPI WSAEnumNetworkEvents(
  [in]  SOCKET             s,
  [in]  WSAEVENT           hEventObject,
  [out] LPWSANETWORKEVENTS lpNetworkEvents
);

s는 말그대로 소켓이고, hEventObject는 reset될 이벤트객체, lpNetworkEvents는 발생한 네트워크 이벤트와 오류를 포함한 구조체에 대한 포인터이다.

 

제대로 실행이 되었다면 0을 반환하고 그렇지 않으면 SOCKET_ERROR(-1)을 반환한다. 마찬가지로 WSAGetLastError()로 오류 코드를 확인할 수 있다.


const int BUFSIZE = 1000;

struct Session
{
	SOCKET socket;
	char recvBuffer[BUFSIZE] = {};
	int recvBytes = 0;
	int sendBytes = 0;
};

int main()
{
	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;

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

	WSAEVENT listenEvent = ::WSACreateEvent();
	wsaEvents.push_back(listenEvent);
	sessions.push_back(Session{ listenSocket });
	if (::WSAEventSelect(listenSocket, listenEvent, FD_ACCEPT | FD_CLOSE) == SOCKET_ERROR)
	{
		return 0;
	}

	while (true)
	{
		// 먼저 완료되는 이벤트의 index
		int index = ::WSAWaitForMultipleEvents(wsaEvents.size(), &wsaEvents[0], FALSE, WSA_INFINITE, FALSE);
		if (index == WSA_WAIT_FAILED)
		{
			// error
			continue;
		}
		index -= WSA_WAIT_EVENT_0; // return 값 참고

		WSANETWORKEVENTS networkEvents;
		if (::WSAEnumNetworkEvents(sessions[index].socket, wsaEvents[index], &networkEvents) == SOCKET_ERROR)
		{
			// error
			continue;
		}

		// Listener socket 체크
		if (networkEvents.lNetworkEvents & FD_ACCEPT)
		{
			// Error-Check
			if (networkEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
			{
				continue;
			}

			SOCKADDR_IN clientAddr;
			int 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 });
				if (::WSAEventSelect(clientSocket, clientEvent, FD_READ | FD_WRITE | FD_CLOSE) == SOCKET_ERROR)
				{
					return 0;
				}
			}
		}

		// Client Session socket 체크
		if (networkEvents.lNetworkEvents & FD_READ || networkEvents.lNetworkEvents & FD_WRITE)
		{
			// Error-Check
			if ((networkEvents.lNetworkEvents & FD_READ) && networkEvents.iErrorCode[FD_READ_BIT] != 0)
			{
				continue;
			}

			// Error-Check
			if ((networkEvents.lNetworkEvents & FD_WRITE) && networkEvents.iErrorCode[FD_WRITE_BIT] != 0)
			{
				continue;
			}

			Session& s = sessions[index];

			// Read
			if (s.recvBytes == 0)
			{
				int recvLen = ::recv(s.socket, s.recvBuffer, BUFSIZE, 0);
				if (recvLen == SOCKET_ERROR && ::WSAGetLastError() != WSAEWOULDBLOCK)
				{
					// TODO : Remove Session
					continue;
				}

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

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

				s.sendBytes += sendLen;
				if (s.recvBytes == s.sendBytes)
				{
					s.recvBytes = 0;
					s.sendBytes = 0;
				}
				cout << "Send Data = " << sendLen << endl;
			}
		}

		// FD_CLOSE 처리
		if (networkEvents.lNetworkEvents & FD_CLOSE)
		{
			// TODO : Remove Socket

		}
	}

	// 윈속 종료
	::WSACleanup();
}

vector로 session과 이벤트를 관리한다.

 

간단히 돌아가는 순서를 정리하자면

1. FD_ACCEPT를 확인할 이벤트를 생성

2. WSAEventSelect로 1번 이벤트를 확인

3. WSAWaitForMultipleEvents로 이벤트 발생에 대한 wsaEvents의 index값을 가져옴

4. WSAEnumNEtworkEvents를 통해 해당 index에 대한 event 내용 확인

5. 4번에서 문제가 없다면, bitmask로 발생한 이벤트와 확인하려고 했던 이벤트(1번 참고)를 확인

6. FD_ACCEPT에 대한 이벤트 accept() 함수 실행 후, 해당 세션과 이벤트를 벡터(wsaEvents, sessions)에 각각 추가

7. 2번과 마찬가지로 WSAEVentSelect로 이번에는 FD_READ, FD_WRITE에 대한 이벤트를 확인

8. 5번과 동일하게 에러 확인 후, FD_READ에 해당하는 recv() 함수 실행, FD_WRITE에 해당하는 send() 함수 실행

9. 해당 작업 완료되었을 경우 세션 삭제

 


WSAEventSelect 모델의 아쉬운점은 select 모델과 마찬가지로 wait할 이벤트를 한번에 최대 64개까지 밖에 등록이 안된다. 

(WSA_MAXIMUM_WAIT_EVENTS = 64)

 

결론적으로 client에서는 wait할 이벤트가 많지 않으면 사용해도 되지만, 많은 처리를 담당하는 서버에서는 무리가 있을 수 있다.

'Game Server (C++)' 카테고리의 다른 글

Asynchronous Socket IO - IOCP  (0) 2022.11.29
Asynchronous Socket IO - Overlapped (event, callback)  (0) 2022.11.26
Socket IO - Select  (0) 2022.11.18
Non-blocking Socket  (0) 2022.11.18
Socket Option - (get/set)sockopt()  (0) 2022.11.07