네트워크, 서버

Overlapped 모델(Event 기반)

춤추는수달 2022. 11. 11. 08:04

Overlapped 모델은 동기 방식의 Send, Recv 등의 함수를 사용하던 Select, EventSelect 모델과 달리, 비동기 방식의 함수(WSARecv, WSASend)를 호출하는 모델이다. 

그리고 Event 기반이라는 말은 그 비동기 함수가 완료되었을 때 이벤트를 통해 통지받는 방식이다. 다른 완료 통지 방식으로는 Callback 함수 기반이 있다.

블로킹 - 논블로킹 / 동기 - 비동기 차이
정확한 개념을 모르면 블로킹이 동기와 같은 말이고, 논블로킹이 비동기와 같은 말이라고 생각할 수 있다. 그러나 이는 엄연히 다른 뉘앙스의 용어들이다.
'블로킹 - 논블로킹' 은 말 그대로 함수가 작업을 완료할 때 까지 대기하느냐 아니냐의 관점이다.
반면 '동기 - 비동기' 는 함수를 호출한 시점과 실제 작업을 하는 시점이 같느냐(동시냐) 아니냐의 관점이다. 
즉 동기 방식이면 무조건 블로킹이고 비동기 방식이면 논블로킹인 것이 아니다. 동기이면서 논블로킹일 수도 있고, 비동기이면서 블로킹일 수 있다. 
하지만 블로킹이 동기 방식과 잘 어울리고, 논블로킹이 비동기 방식과 잘 어울리는 것은 사실이다.

함수들

이번엔 저번에 쓴 글인 EventSelcet 모델에서 사용하는 함수와 겹치는 함수가 많다. 참고하시길.

  • WSACreateEvent : 이벤트 객체 만들기
  • WSARecv, WSASend : 비동기 통신 함수. 매개변수가 일반 send, recv 함수와 다르다. 
    • SOCKET s: 통신할 소켓 
    • LPWSABUF lpBuffers: WSABUFF 배열의 주소. WSABUFF는 읽거나 쓸 버퍼와 해당 버퍼의 길이를 담은 struct이다.
    • DWORD dwBufferCount: lpBuffers의 배열 길이.
    • LPDWORD lpNumberOfBytesRecvd: 만약 비동기 함수가 바로 완료되었을 경우, 읽거나 쓴 데이터의 크기를 반환한다. 
    • LPDWORD lpFlags: 각종 옵션 Flags.
    • LPWSAOVERLAPPED lpOverlapped: WSAOVERLAPPED 객체의 포인터. WSAOVERLAPPED 객체는 이벤트 객체를 포함하는 struct 이다. 그 외에도 여러 정보를 포함하지만 우리가 신경쓸 것은 이벤트 객체 뿐이다.
    • LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine: 비동기 작업이 완료될 경우 호출할 Callback 함수의 포인터이다. 이 글에서는 Event 기반 완료 통지에 대해 다루므로 사용하지 않는다.
  • WSAWaitForMultipleEvents : 이벤트가 Signal 되는지 감시하는 함수. 감시하는 동안 블로킹 됨.
  • WSAGetOverlappedResult : overlapped 작업의 완료 결과를 얻는 함수. 
    • SOCKET s : 통신에 사용된 소켓
    • LPWSAOVERLAPPED lpOverlapped : 비동기 함수를 호출 할 때 넘겨주었던 WSAOVERLAPPED 객체의 포인터.
    • LPDWORD lpcbTransfer : 통신한 바이트 수를 받는 변수.
    • BOOL fWait : 통신이 완료될 때 까지 대기할지 여부. 
    • LPDWORD lpdwFlags : 부가 옵션. 보통 사용 안함.

참고로 여기서 말하는 비동기 함수는 WSARecv, WSASend 말고도 AcceptEx 등 많이 있지만, 여기선 기본적인 동작을 설명하기 위해 상기한 두 함수만 언급하겠다.


방법

위 함수들을 사용해 Overlapped 모델을 어떻게 구현할지 알아보자.

  1. 소켓 만들기.
  2. 이벤트 객체 만들기.
  3. Overlapped 객체 만들기.(2. 에서 만든 이벤트 객체를 넣어서)
  4. 비동기 함수(WSARecv, WSASend 등) 호출.
  5.  4. 에서 호출한 비동기 함수가 SOCKET_ERROR를 반환했다면(작업이 즉시 완료되지 않았다면), Pending(비동기 작업이 아직 시작되지 않음) 상태인지 확인하기.
    1. 만약 Pending 상태라면 WaitForMultipleEvents 함수를 통해 이벤트가 Signal 될 때 까지 대기.
    2. 이벤트가 Signal 되었다면 (WaitForMultipleEvents 함수가 반환되면) WSAGetOverlappedResult 함수를 사용해 완료 결과를 얻기. 
    3. 얻은 결과를 사용해 적절한 작업 하기.
    4. 만약 Pending 상태가 아니라면 에러처리 하기.
  6. 4.에서 호출한 비동기 함수가 성공했다면(즉시 작업이 완료되었다면) 그냥 동기 send 함수와 동일함.

 


코드

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

// Overlapped IO (비동기 + 논블로킹)
	// - Overlapped 함수를 건다 (WSARecv, WSASend)
	// - Overlapped 함수가 성공했는지 확인 후
	// -> 성공했으면 결과 얻어서 처리
	// -> 실패했으면 사유를 확인

	// 1) 비동기 입출력 소켓
	// 2) WSABUF 배열의 시작 주소 + 개수 // Scatter-Gather
	// 3) 보내고/받은 바이트 수
	// 4) 상세 옵션인데 0
	// 5) WSAOVERLAPPED 구조체 주소값
	// 6) 입출력이 완료되면 OS가 호출할 콜백 함수
	// WSASend
	// WSARecv
	
	// Overlapped 모델 (이벤트 기반)
	// - 비동기 입출력 지원하는 소켓 생성 + 통지 받기 위한 이벤트 객체 생성
	// - 비동기 입출력 함수 호출 (1에서 만든 이벤트 객체를 같이 넘겨줌)
	// - 비동기 작업이 바로 완료되지 않으면, WSA_IO_PENDING 오류 코드
	// 운영체제는 이벤트 객체를 signaled 상태로 만들어서 완료 상태 알려줌
	// - WSAWaitForMultipleEvents 함수 호출해서 이벤트 객체의 signal 판별
	// - WSAGetOverlappedResult 호출해서 비동기 입출력 결과 확인 및 데이터 처리

	// 1) 비동기 소켓
	// 2) 넘겨준 overlapped 구조체
	// 3) 전송된 바이트 수
	// 4) 비동기 입출력 작업이 끝날때까지 대기할지?
	// false
	// 5) 비동기 입출력 작업 관련 부가 정보. 거의 사용 안 함.
	// WSAGetOverlappedResult

	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 = 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
				{
					// TODO : 문제 있는 상황
					break;
				}
			}

			cout << "Data Recv Len = " << recvLen << endl;
		}

		::closesocket(session.socket);
		::WSACloseEvent(wsaEvent);
    }

평가

Event 기반 Overlapped모델은 EventSelect모델과 마찬가지로 WaitForMultipleEvents 함수를 사용하므로 EventSelect모델과 같은 단점이 있다.

이벤트가 Signal 될 때 까지 대기해야하며, 한 번에 64개의 이벤트만 감시할 수 있는 등의 단점들이 있다. 

그냥 이벤트 기반으로 뭔가 한다는게 비효율적인 것 같다.

'네트워크, 서버' 카테고리의 다른 글

NonBlocking Socket  (0) 2022.11.07
소켓 프로그래밍  (0) 2022.11.02
JobTimer  (0) 2022.07.19
JobQueue  (0) 2022.07.16
Google Protocol Buffer  (0) 2022.07.07