공부

WSAEventSelect 모델

글로벌디노 2020. 10. 20. 22:10

WSAEventSelect 모델

MSDN

 

 

소켓과 관련된 네트워크 이벤트를 이벤트 객체를 통해 감지한다. 아래 그림과 같이 이벤트 객체를 소켓당 하나씩 생성하고 이벤트 객체들을 관찰하면 멀티스레드를 사용하지 않고도 소켓을 처리할 수 있다.

 

 

WSAEventSelect 모델의 동작 원리

WSAEventSelect 모델을 사용하면 소켓 함수 호출이 성공할 수 있는 시점을 이벤트 객체를 통해 알 수 있다. 

위 그림과 같이 소켓에 대해 이벤트 객체를 생성하여 짝지어두면, 네트워크 이벤트가 발생할 때마다 이벤트 객체가 신호 상태가 된다. 따라서 이벤트 객체의 상태 변화를 관찰함으로써 네트워크 이벤트 발생을 감지할 수 있다. 

 

[표 1] WSAEventSelect 모델의 필요 기능과 관련 함수

필요 기능 관련 함수
이벤트 객체 생성과 제거 WSACreateEvent(), WSACloseEvent()
소켓과 이벤트 객체 짝짓기 WSAEventSelect()
이벤트 객체의 신호 상태 감지하기 WSAWaitForMultipleEvents()
구체적인 네트워크 이벤트 알아내기 WSAEnumNetworkEvents()

 

WSAEventSelect 모델을 이용한 소켓 입출력 절차

  1. 소켓을 생성할 때마다 WSACreateEvent() 함수를 이용해 이벤트 객체를 생성한다.
  2. WSAEventSelect() 함수를 이용해 소켓과 이벤트 객체를 짝지음과 동시에, 처리할 네트워크 이벤트를 등록한다. 예를 들면, 소켓을 통해 데이터를 받을 수 있는 상황이 되면 이벤트 객체를 신호 상태로 변경하라는 내용을 등록한다.
  3. WSAWaitForMultipleEvents() 함수를 호출해 이벤트 객체가 신호 상태가 되기를 기다린다. 등록한 네트워크 이벤트가 발생하면 해당 소켓과 연관된 이벤트 객체가 신호 상태가 된다.
  4. WSAEnumNetworkEvents() 함수를 호출해 발생한 네트워크 이벤트를 알아내고, 적절한 소켓 함수를 호출해 처리한다.

 

이벤트 객체 생성과 제거하기

WSACreateEvent() 함수는 이벤트 객체를 생성하는 역할을 한다. 이때 생성되는 이벤트 객체는 항상 수동 리셋(manual-reset) 이벤트며 비신호 상태로 시작한다. 사용을 마친 이벤트 객체는 WSACloseEvent() 함수를 호출해 제거한다.

WSAEVENT WSAAPI WSACreateEvent();

성공: 이벤트 객체 핸들

실패: WSA_INVALID_EVENT

 

BOOL WSAAPI WSACloseEvent(
  WSAEVENT hEvent
);

성공: TRUE

실패: FALSE

 

 

소켓과 이벤트 객체 짝짓기

WSAEventSelect() 함수는 소켓과 이벤트 객체를 짝지음과 동시에, 처리할 네트워크 이벤트를 등록하는 열할을 한다.

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

성공: 0

실패: SOCKET_ERROR

 

  • s : 네트워크 이벤트를 처리하고자 하는 소켓.
  • hEventObject : 소켓과 연관시킬 이벤트 객체의 핸들.
  • lNetworkEvents : 관심 있는 네트워크 이벤트를 비트 마스크 조합으로 나타낸다.

[표 2] 네트워크 이벤트를 나타내는 상수

네트워크 이벤트 의미
FD_ACCEPT 접속한 클라이언트가 있다
FD_READ 데이터 수신이 가능하다
FD_WRITE 데이터 송신이 가능하다
FD_CLOSE 상대가 접속을 종료했다
FD_CONNECT 통신을 위한 연결 절차가 끝났다
FD_OOB OOB 데이터가 도착했다

 

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

성공: WSA_WAIT_EVENT_0 ~ (WSA_WAIT_EVENT_0 + cEvents - 1) 또는 WSA_WAIT_TIMEOUT

실패: WSA_WAIT_FAILED

 

  • cEvents, lphEvents : WSAWaitForMultipleEvents() 함수를 사용할 때는 이벤트 객체 핸들을 모두 배열에 넣어 전달해야 한다. cEvents는 배열 원소 개수, lphEvents는 배열의 시작 주소를 나타낸다. cEvents의 최댓값은 WSA_MAXIMUM_WAIT_EVENTS(64) 이다.
  • fWaitAll : TRUE이면 모든 이벤트 객체가 신호 상태가 될 때까지 기다린다. FALSE면 이벤트 객체 중 하나가 신호 상태가 되는 즉시 리턴한다.
  • dwTimeout : 대기 시간으로 밀리초 단위를 사용한다. 네트워크 이벤트가 발생하지 않아도 이 시간이 지나면 WSAWaitForMultipleEvents() 함수가 리턴한다. 대기 시간으로 WSA_INFINITE 값을 사용하면 조건이 만족될 때까지 무한히 기다린다.
  • fAlertable : 입출력 완료 루틴(I/O completion routine)과 관련된 부분이다. WSAEventSelect 모델에서는 사용하지 않으므로 항상 FALSE를 전달한다.

 

구체적인 네트워크 이벤트 알아내기

WSAWSAEnumNetworkEvents() 함수는 소켓과 관련하여 발생한 구체적인 네트워크 이벤트를 알려주는 역할을 한다.

int WSAAPI WSAEnumNetworkEvents(
  SOCKET             s,
  WSAEVENT           hEventObject,
  LPWSANETWORKEVENTS lpNetworkEvents
);

성공 : 0

실패 : SOCKET_ERROR

 

  • s : 대상 소켓이다.
  • hEventObject : 대상 소켓 s와 짝지어둔 이벤트 객체 핸들을 넘겨주면 이벤트 객체가 자동으로 비신호 상태로 된다. 이 인자는 선택 사항이므로 사용하지 않으려면 NULL 값을 넘겨주면 된다.
  • lpNetworkEvents : WSANETWORKEVENTS 구조체 변수의 주소 값을 전달하면, 발생한 네트워크 이벤트와 오류 정보가 이 변수에 저장된다. 구조체 정의는 다음과 같다.
typedef struct _WSANETWORKEVENTS {
  long lNetworkEvents;
  int  iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, *LPWSANETWORKEVENTS;

lNetworkEvents : [표 2]의 상숫값이 조합된 형태로 저장되어, 발생한 네트워크 이벤트를 알려준다.

iErrorCode[ ] : 네트워크 이벤트와 연관된 오류 정보가 저장된다. 오류 정보를 참조하려면 배열 인덱스 값을 사용해야 한다. [표 3]의 왼쪽은 네트워크 이벤트를, 오른쪽은 배열 인덱스를 나타낸다.

 

[표 3] 네트워크 이벤트와 iErrorCode[ ]에 사용할 수 있는 배열 인덱스 값

네트워크 이벤트 배열 인덱스
FD_ACCEPT FD_ACCEPT_BIT
FD_READ FD_READ_BIT
FD_WRITE FD_WRITE_BIT
FD_CLOSE FD_CLOSE_BIT
FD_CONNECT FD_CONNECT_BIT
FD_OOB FD_OOB_BIT

 

WSAEnumNetworkEvents( ) 함수 사용 예는 다음과 같다.

SOCKET s;
WSAEVENT hEvent;
WSANETWORK NetworkEvents;
...
WSAEnumNetworkEvents(s, hEvent, &NetworkEvents);
// FD_ACCEPT 이벤트 처리
if(NetworkEvents.lNetworkEvents & FD_ACCEPT) 
{
    if(NetworkEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
    {
        // 오류출력 및 처리
    }
    else
    {
        // accept() 함수 호출
    }
}
// FD_READ 이벤트 처리
if(NetworkEvents.lNetworkEvents & FD_READ)
{
    if(NetworkEvents.iErrorCode[FD_READ_BIT] != 0)
    {
        // 오류출력 및 처리
    }
    else
    {
        // recv() 함수 호출
    }
}

 

 

WSAEventSelect 모델 서버 작성

 

서버 코드

더보기
#pragma comment(lib, "ws2_32")
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <stdio.h>
#include <tchar.h>
#include <locale.h>

#define SERVERPORT 9000
#define BUFSIZE 512

// 소켓 정보 저장을 위한 구조체와 변수
struct SOCKETINFO
{
	SOCKET sock;
	char buf[BUFSIZE + 1];
	int recvBytes;
	int sendBytes;
};

int totalSockets = 0;
SOCKETINFO* socketInfoArray[WSA_MAXIMUM_WAIT_EVENTS];
WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS];

// 소켓 관리 함수
BOOL AddSocketInfo(SOCKET sock);
void RemoveSocketInfo(int index);

int _tmain()
{
	_tsetlocale(LC_ALL, TEXT(""));

	int retVal;

	// 윈속 초기화
	WSADATA wsa;
	retVal = WSAStartup(MAKEWORD(2, 2), &wsa);
	if (retVal != 0)
	{
		_tprintf(TEXT("error WSAStartup() %d\n"), retVal);
		return 1;
	}

	// listen socket()
	SOCKET listenSock = socket(AF_INET, SOCK_STREAM, 0);
	if (listenSock == INVALID_SOCKET)
	{
		_tprintf(TEXT("error listen socket()\n"));
		return 1;
	}

	// bind()
	SOCKADDR_IN serverAddr;
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serverAddr.sin_port = htons(SERVERPORT);
	retVal = bind(listenSock, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
	if (retVal == SOCKET_ERROR)
	{
		_tprintf(TEXT("error bind() %d\n"), WSAGetLastError());
		return 1;
	}

	// listen()
	retVal = listen(listenSock, SOMAXCONN);
	if (retVal == SOCKET_ERROR)
	{
		_tprintf(TEXT("error listen() %d\n"), WSAGetLastError());
		return 1;
	}

	// 소켓 정보 추가 & WSAEventSelect()
	AddSocketInfo(listenSock);
	retVal = WSAEventSelect(listenSock, eventArray[totalSockets - 1], 
		FD_ACCEPT | FD_CLOSE);
	if (retVal == SOCKET_ERROR)
	{
		_tprintf(TEXT("error WSAEventSelect() %d\n"), WSAGetLastError());
		return 1;
	}

	// 데이터 통신에 사용할 변수
	WSANETWORKEVENTS networkEvents;
	SOCKET clientSock;
	SOCKADDR_IN clientAddr;
	int i, addrLen;

	while (1)
	{
		// 이벤트 객체 관찰하기
		i = WSAWaitForMultipleEvents(totalSockets, eventArray, 
			FALSE, WSA_INFINITE, FALSE);
		if (i == WSA_WAIT_FAILED) 
			continue;
		i -= WSA_WAIT_EVENT_0;

		// 구체적인 네트워크 이벤트 알아내기
		retVal = WSAEnumNetworkEvents(socketInfoArray[i]->sock, 
			eventArray[i], &networkEvents);
		if (retVal == SOCKET_ERROR)
			continue;

		// FD_ACCEPT 이벤트 처리
		if (networkEvents.lNetworkEvents & FD_ACCEPT)
		{
			if (networkEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
			{
				_tprintf(TEXT("error FD_ACCEPT_BIT on\n"));
				continue;
			}

			addrLen = sizeof(clientAddr);
			clientSock = accept(socketInfoArray[i]->sock, 
				(SOCKADDR*)&clientAddr, &addrLen);
			if (clientSock == INVALID_SOCKET)
			{
				_tprintf(TEXT("error accept() %d\n"), WSAGetLastError());
				continue;
			}

			TCHAR ipBuf[16];
			InetNtop(AF_INET, &clientAddr.sin_addr, ipBuf, _countof(ipBuf));
			_tprintf(TEXT("[TCP 서버] 클라이언트 접속: IP 주소=%s, 포트 번호=%d\n"),
				ipBuf, ntohs(clientAddr.sin_port));

			if (totalSockets >= WSA_MAXIMUM_WAIT_EVENTS)
			{
				closesocket(clientSock);
				_tprintf(TEXT("[오류] 더 이상 접속을 받아들일 수 없습니다!\n"));
				continue;
			}

			// 소켓 정보 추가 & WSAEventSelect()
			AddSocketInfo(clientSock);
			retVal = WSAEventSelect(clientSock, eventArray[totalSockets - 1],
				FD_READ | FD_WRITE | FD_CLOSE);
			if (retVal == SOCKET_ERROR)
			{
				_tprintf(TEXT("error WSAEventSelect() %d\n"), WSAGetLastError());
				return 1;
			}
		}

		// FD_READ & FD_WRITE 이벤트 처리
		if (networkEvents.lNetworkEvents & FD_READ
			|| networkEvents.lNetworkEvents & FD_WRITE)
		{
			if (networkEvents.lNetworkEvents & FD_READ 
				&& networkEvents.iErrorCode[FD_READ_BIT] != 0)
			{
				_tprintf(TEXT("error FD_READ_BIT on\n"));
				continue;
			}
			if (networkEvents.lNetworkEvents & FD_WRITE
				&& networkEvents.iErrorCode[FD_WRITE_BIT] != 0)
			{
				_tprintf(TEXT("error FD_WRITE_BIT on\n"));
				continue;
			}

			SOCKETINFO* ptr = socketInfoArray[i];
			if (ptr->recvBytes == 0)
			{
				// 데이터 받기
				retVal = recv(ptr->sock, ptr->buf, BUFSIZE, 0);
				if (retVal == SOCKET_ERROR)
				{
					if (WSAGetLastError() != WSAEWOULDBLOCK)
					{
						_tprintf(TEXT("error recv()\n"));
						RemoveSocketInfo(i);
					}
					continue;
				}
				ptr->recvBytes = retVal;

				// 받은 데이터 출력
				ptr->buf[retVal] = '\0';
				addrLen = sizeof(clientAddr);
				getpeername(ptr->sock, (SOCKADDR*)&clientAddr, &addrLen);
				TCHAR ipBuf[16];
				InetNtop(AF_INET, &clientAddr.sin_addr, ipBuf, _countof(ipBuf));
				_tprintf(TEXT("[TCP/%s:%d] "), ipBuf, ntohs(clientAddr.sin_port));
				printf("%s\n", ptr->buf);
			}

			if (ptr->recvBytes > ptr->sendBytes)
			{
				// 데이터 보내기
				retVal = send(ptr->sock, ptr->buf + ptr->sendBytes,
					ptr->recvBytes - ptr->sendBytes, 0);
				if (retVal == SOCKET_ERROR)
				{
					if (WSAGetLastError() != WSAEWOULDBLOCK)
					{
						_tprintf(TEXT("error send()\n"));
						RemoveSocketInfo(i);
					}
					continue;
				}
				ptr->sendBytes += retVal;
				
				// 받은 데이터를 모두 보냈는지 체크
				if (ptr->recvBytes == ptr->sendBytes)
					ptr->recvBytes = ptr->sendBytes = 0;
			}
		}

		// FD_CLOSE 이벤트 처리
		if (networkEvents.lNetworkEvents & FD_CLOSE)
		{
			if (networkEvents.iErrorCode[FD_CLOSE_BIT] != 0)
				_tprintf(TEXT("error FD_CLOSE_BIT on\n"));

			RemoveSocketInfo(i);
		}
	}

	// 윈속 종료
	WSACleanup();
	return 0;
}

// 소켓 정보 추가
BOOL AddSocketInfo(SOCKET sock)
{
	SOCKETINFO* ptr = new SOCKETINFO;
	if (ptr == NULL)
	{
		_tprintf(TEXT("[오류] 메모리가 부족합니다!\n"));
		return FALSE;
	}

	WSAEVENT hEvent = WSACreateEvent();
	if (hEvent == WSA_INVALID_EVENT)
	{
		_tprintf(TEXT("error WSACreateEvent()\n"));
		return FALSE;
	}

	ptr->sock = sock;
	ptr->recvBytes = 0;
	ptr->sendBytes = 0;
	socketInfoArray[totalSockets] = ptr;
	eventArray[totalSockets] = hEvent;
	++totalSockets;

	return TRUE;
}

// 소켓 정보 삭제
void RemoveSocketInfo(int index)
{
	SOCKETINFO* ptr = socketInfoArray[index];

	SOCKADDR_IN clientAddr;
	int addrLen = sizeof(clientAddr);
	getpeername(ptr->sock, (SOCKADDR*)&clientAddr, &addrLen);
	TCHAR buf[16];
	InetNtop(AF_INET, &clientAddr.sin_addr, buf, _countof(buf));
	_tprintf(TEXT("[TCP 서버] 클라이언트 종료: IP 주소=%s, 포트 번호=%d\n"),
		buf, ntohs(clientAddr.sin_port));

	closesocket(ptr->sock);
	delete ptr;
	WSACloseEvent(eventArray[index]);

	if (index != totalSockets - 1)
	{
		socketInfoArray[index] = socketInfoArray[totalSockets - 1];
		eventArray[index] = eventArray[totalSockets - 1];
	}
	--totalSockets;
}

 

클라이언트 코드

참조

 

'공부' 카테고리의 다른 글

복습) 문제 만들기 20201020  (0) 2020.10.20
WSAAsyncSelect 모델 서버 프로그래밍  (0) 2020.10.19
Select모델 에코서버 프로그래밍  (0) 2020.10.17
TCP 파일전송 프로그램  (0) 2020.10.13
TCP 에코서버 프로그래밍  (0) 2020.10.10