네트워크, 서버

Overlapped 모델(콜백함수)

춤추는수달 2022. 4. 30. 06:08

콜백함수 기반 Overlapped 모델에 대해 알아보자.

이벤트 기반 Overlapped 모델과 달리 콜백함수를 통해 Send, Recv 동작 완료 통지를 받는 방식이다. 즉 이벤트 기반 Overlapped모델에서 이벤트 관련 부분을 떼고, 비동기 통신 함수의 마지막 매개변수에 콜백함수를 전달해주는 부분을 붙이면 그것이 콜백함수 기반 Overlapped 모델이다.

이벤트 기반 Overlapped모델이 비동기 블로킹 방식이었다면, 이 모델은 비동기 논블로킹 방식이라고 할 수 있다. 


콜백(Call back)함수

콜백함수 기반 Overlapped 모델에 대해 알아보기 전에, 우선 콜백함수에 대해 알아볼 필요가 있다.

콜백 함수란 어떤 이벤트가 발생했을 때 호출되는 함수이다. 여기서는 비동기 입출력 함수(WSASend, WSARecv)가 완료됐을 때 호출되는 함수이다. 

호출 타이밍

그런데, 이 콜백함수가 작업이 완료됐다고 바로 호출되는 것은 아니다. 만약 작업이 완료될 때마다 진행중이던 다른 중요한 작업들을 멈추고 즉시 콜백함수를 실행시킨다면 곤란한 상황이 일어날 수 있다. 그래서 운영체제는 실행해야 하는 콜백 함수들을 APC 큐라는 곳에 저장해둔다. 

APC(Asynchronouse Procedure Call) 큐

APC 큐란 스레드마다 존재하는 실행해야하는 콜백함수 모음 큐 이다. 이 APC 큐에 모인 콜백함수들은 스레드가 Alertable wait 상태가 되었을 때 한 번에 실행된다. 

클릭하면 출처

Alertable wait

위에 설명했듯 스레드의 APC 큐에 있는 콜백함수들이 실행될 수 있는 상태이다.

몇 가지 함수를 통해 스레드를 Alertable Wait 상태로 진입시킬 타이밍을 조절할 수 있다. 그 함수들이란 아래와 같다.

  • SleepEx()
  • WaitForSingleObjectEx()
  • WaitForMultipleObjectsEx()
  • WSAWaitForMultipleEvents()

방법

  1. 소켓생성
  2. 콜백함수 선언. 꼭 아래와 같은 형태의 함수를 선언해야한다. 매개변수들은 자동으로 넘겨서 호출해준다. 여기서 중요한 매개변수는 3번째 LPWSAOVERLAPPED 형 매개변수이다. 이 매개변수는 비동기 입출력 함수를 호출했으 때 전해주었던 WSAOVERLAPPED 구조체의 포인터를 받아온다. 하지만 Overlapeed 구조체의 포인터 형이라고 해서 꼭 Overlapped 구조체만 담을 필요는 없다. 예를 들어 여기서는 Session이라는 구조체의 첫 번째 멤버로 WSAOVERLAPPED 형 변수를 두고, 그 외 필요한 정보를 담아 이 Session 구조체를 비동기 입출력 함수에 매개변수로 전달해 주었다. 그리고 비동기 입출력 작업이 완료되었을 때 이 콜백함수서 3번째 매개변수를 Session 포인터 형으로 캐스팅하여 사용했다. 이런 식으로 Overlapped 구조체 이외의 필요한 정보를 담아서 콜백함수에서 활용할 수 있다.
  3. void CALLBACKL RecvCallBack(DWORD error, DWORD recvLen, LPWSAOVERLAPPED overlapped, DWORD flags) { cout <<"recv callback"<<recvLen<<endl; }
  4. 비동기 입출력 함수 호출(완료 루틴 콜백 넘겨줌)
  5. WSABUF wsaBuf; wsaBuf.buf = sessionl.recvBuffer; wsaBuf.len = BUFSIZE; WSARecv(clientSocket, &wsaBuf, 1, &recvLen, &flags, &session.overlapped, RecvCallback)
  6. 비동기 작업 바로 완료되지 않으면 WSA_IO_PENDING 오류코드 발생.
  7. 쓰레드를 Alertable Wait상태로 만듦(완료 루틴 호출 가능 상태)
  8. //두 번쨰 인자를 TRUE로 설정 시 Alertable Wait 상태로 전환됨. SleepEx(INFINITE,TRUE);
  9. 작업 완료 시 완료루틴 호출
  10. 완료 루틴 종료 후 Alertable Wait상태 종료
const int32 BUFSIZE = 1000;

struct Session
{
	WSAOVERLAPPED overlapped = {};
	SOCKET socket = INVALID_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()

	Session* session = (Session*)overlapped;

}

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

	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					
					::SleepEx(INFINITE, TRUE);
					//::WSAWaitForMultipleEvents(1, &wsaEvent, TRUE, WSA_INFINITE, TRUE);					
				}
				else
				{
					// TODO : 문제 있는 상황
					break;
				}
			}
			else
			{
				cout << "Data Recv Len = " << recvLen << endl;
			}			
		}

		::closesocket(session.socket);
		//::WSACloseEvent(wsaEvent);
	}
	
	// 윈속 종료
	::WSACleanup();
}

평가

단점1 : 모든 비동기 소켓 함수에서 사용가능은 아님(예 acceptex)

단점2 : 빈번한 Alertable wait 상태 진입은 성능 저하 유발

단점3 : 반드시 동일 쓰레드에서 콜백을 처리해주어야 함. 작업이 많아지면 결국 하나의 쓰레드만 열심히 일하고 다른 쓰레드들은 놀고있게 됨.

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

Google Protocol Buffer  (0) 2022.07.07
Completion Port 모델  (0) 2022.04.30
WSAEventSelect model  (0) 2022.04.29
Select model  (0) 2022.04.28
Socket Options  (0) 2022.04.28