본문 바로가기

윈도우즈 소켓 프로그래밍 기초

WSAEventSelect 입출력 모델

동기(Synchronous)

여러 개의 작업을 순차적으로 진행합니다.

함수를 호출하고 함수의 결과를 호출한 쪽에서 처리합니다.

 

비동기(Asynchronous)

함수를 호출 했을 때 이 함수의 결과를 호출한 쪽에서 처리하지 않습니다.

 

블로킹(Blocking)

자신의 수행 결과가 끝날 때 까지 제어권을 갖고 있습니다.

그래서 블로킹 함수는 프로세스가 붙잡혀있습니다.

 

논 블로킹(Non-Blocking)

호출되면 제어권을 자신을 호출한 쪽으로 넘깁니다.

그래서 프로세스가 붙잡혀있지 않고 다른 일을 할 수 있습니다.

 

WSAEventSelect 

윈속2 버전 이상의 핵심은 비동기 시스템이라고 합니다.

WSA는 Windows Socket Async의 약자입니다.

 

그래서 WSAEventSelect 모델은 비동기 입출력으로 생각할 수 있지만,

비동기 입출력 모델이 아닙니다. 여전히 동기 입출력 방식으로 소켓을 처리합니다.

 

그러나 입출력 함수를 안전하게 호출할 수 있는 시점을

운영체제가 알려주기때문에 단순한 입출력 방식보다

편리하게 여러개의 소켓을 처리할 수 있습니다.

 

운영체제에서 함수 호출의 시점을 알려주기 때문에

비동기처럼 처리할 수 있어서 WSA의 이름이 붙여졌다고합니다.

윈속의 대표적인 비동기 입출력 모델은 Overlapped I/O, IOCP 입니다.

 

WSAEventSelect 모델 서버 C++ 소스 코드

#include <WinSock2.h>
#include <iostream>

using namespace std;

#pragma comment(lib, "ws2_32.lib")

#define PORT			7890
#define PACKET_LENGTH		1024

int main()
{
	WSADATA wsaData;
	WSAStartup(MAKEWORD(2, 2), &wsaData);

	// WSAEventSelect 모델은 이벤트 객체를 활용하여 네트워크의 이벤트 상태를 감지한다.
	// WSA_MAXIMUM_WAIT_EVENTS(64) 만큼 소켓 배열을 만든다.
	SOCKET hSocketArray[WSA_MAXIMUM_WAIT_EVENTS] = {};

	// 이벤트 상태를 감지할 수 있는(대기 시킬) 이벤트 배열을 만든다.
	WSAEVENT hSocketEventArray[WSA_MAXIMUM_WAIT_EVENTS] = {};
	int iSocketCount = 0;

	SOCKET hListen = socket(PF_INET, SOCK_STREAM, 0);
	
	SOCKADDR_IN tListenAddr = {};
	tListenAddr.sin_family = AF_INET;
	tListenAddr.sin_port = htons(PORT);
	tListenAddr.sin_addr.s_addr = htonl(INADDR_ANY);

	bind(hListen, (SOCKADDR*)&tListenAddr, sizeof(tListenAddr));

	listen(hListen, SOMAXCONN);

	// 네트워크의 이벤트 상태를 감지할 이벤트 객체를 생성한다.
	WSAEVENT hEvent = WSACreateEvent();

	// 생성한 리슨 소켓과 이벤트 객체를 묶어준다. 
	// 세 번째 인자는 이벤트가 감지할 이벤트의 종류를 지정해주는 것이다.
	// 이를 통해 리슨 소켓은 접속 요청과 접속 종료 이벤트를 감지할 수 있게 된다.
	WSAEventSelect(hListen, hEvent, FD_ACCEPT | FD_CLOSE);

	// 바인딩된 소켓과 이벤트의 종류가 지정된 이벤트 객체를 소켓 배열에 넣어준다.
	hSocketArray[iSocketCount] = hListen;
	hSocketEventArray[iSocketCount] = hEvent;
	++iSocketCount;

	// ioctlsocket : 소켓의 입출력 모드를 변경한다.
	// socket 함수는 기본적으로 블로킹 소켓을 생성한다.
	// 때문에 현재 hListen 소켓은 블로킹 소켓으로 설정되어 있으므로
	// 이를 논블로킹 소켓으로 바꿔준다. (리슨 소켓을 논블로킹 소켓으로 바꿔준다)
	// 하나의 프로세스에서만 잡히게 될 리슨 소켓의 입출력 상황을 감지해주는 방식을 바꿔준 것 뿐이다.
	// 두 번째 인자는 소켓(hListen)이 수행할 커맨드를 전달한다.
	// FIONBIO : 작업을 수행할 소켓을 논블로킹 모드로 활성화한다.
	// 반환값 : 성공 0, 실패 SOCKET_ERROR(-1) 반환
	// WSAGetLastError 함수를 이용해서 특정한 에러 코드도 얻을 수 있다.
	u_long on = TRUE;
	ioctlsocket(hListen, FIONBIO, &on);

	// WSANETWORKEVENTS : 발생한 이벤트 종류를 감지해주는 구조체이다.
	// 이를위해 네트워크 이벤트 종류를 저장한다.

	/*
	typedef struct _WSANETWORKEVENTS {
       long lNetworkEvents;				// 발생한 네트워크 이벤트를 저장한다.
       int iErrorCode[FD_MAX_EVENTS];	// 오류가 발생했을 때 오류 정보를 저장한다. 
	} WSANETWORKEVENTS, FAR * LPWSANETWORKEVENTS;
	
	*/

	WSANETWORKEVENTS wsaNetEvents;
	
	while (true)
	{
		// WSAWaitForMultipleEvents : 발생한 이벤트를 감시한다.
		// 이벤트 상태가 시그널 상태가 될 때 까지 대기한다.
		// 이벤트 객체의 신호 상태를 감지하는 것이다.
		// arg 1) DWORD cEvents : 대기할 이벤트 개수
		// arg 2) const WSAEVENT *lphEvents : 대기할 이벤트 배열
		// arg 3) BOOL fWaitAll : TRUE이면 모든 객체가 시그널 상태가 되기 전 까지 대기,
		// FALSE이면 한 이벤트 객체가 신호상태이면 리턴
		// arg 4) DWORD dwTimeout : 이벤트 객체 대기상태 대기 시간
		// arg 5) BOOL fAlertable : 스레드 실행에 대한 입출력 완료 루틴을 설정한다. 패스.
		// 반환값 : 성공(이벤트가 발생한 이벤트 객체 배열의 인덱스 값)
		// 타임 아웃(WSA_WAIT_TIMEOUT : 지금은 INIFITE로 했으므로 이벤트가 발생했을 때만 리턴), 
		// 실패(WSA_WAIT_FAILED)
		DWORD dwIndex = WSAWaitForMultipleEvents(iSocketCount, hSocketEventArray, FALSE,
			WSA_INFINITE, FALSE);

		if (dwIndex == WSA_WAIT_FAILED)
			continue;

		// 리턴 받은 이벤트 객체의 인덱스에서 WSA_WAIT_EVENT_0를 빼주면
		// 몇 번째 인덱스에서 이벤트가 발생했는지 알 수 있게 된다.
		dwIndex -= WSA_WAIT_EVENT_0;

		// WSAEnumNetworkEvents : 최종적으로 구한 인덱스를 통해 발생한 이벤트의 종류를 알아낸다.
		// arg 1) SOCKET s : 발생한 이벤트를 알고자하는 소켓
		// arg 2) WSAEVENT hEventObject : 발생한 이벤트를 알고자하는 이벤트 핸들
		// arg 3) LPWSANETWORKEVENTS lpNetworkEvents : 발생한 이벤트 종류 기록을 채워넣을 구조체
		// 발생한 에러 또한 담길수도 있다.
		// MSDN 참조
		// https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsaenumnetworkevents
		
		if (WSAEnumNetworkEvents(hSocketArray[dwIndex], hSocketEventArray[dwIndex],
			&wsaNetEvents) == SOCKET_ERROR)
			continue;

		/* WSAEnumNetworkEvents 함수의 인자로 wsaNetEvents 구조체 포인터를 전달해서
		   wsaNetEvents.lNetworkEvents에 발생한 이벤트의 종류가 담겨있다.
		   이 이벤트 종류를 바탕으로 처리한다. */

		// 클라이언트 접속 이벤트가 발생했을 때
		// FD_ACCEPT 이벤트를 처리한다.
		if (wsaNetEvents.lNetworkEvents & FD_ACCEPT)
		{
			// wsaNetEvents.iErrorCode : 여기에는 이벤트 에러가 발생했을 때 기록된다.
			// 에러가 발생했을 때 lNetworkEvents의 FD_ACCEPT는 iErrorCode의 FD_ACCEPT_BIT와 대응된다.
			
			// 그 밖에 대응되는 매크로 함수
			/*
				    [ lNetworkEvents ]           [ lNetworkEvents ]
					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
			*/

			if (wsaNetEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
			{
				// 에러 로그를 출력하고 다음 순서를 진행한다.
				cout << "Accept Error !!" << endl;
				continue;
			}

			// 접속을 성공했으면 accept 소켓을 생성한다.
			SOCKADDR_IN tAddr = {};
			int iSize = sizeof(tAddr);
			SOCKET hSocket = accept(hSocketArray[dwIndex], (SOCKADDR*)&tAddr, &iSize);

			// 접속 실패시 모든 소켓을 종료한다.
			if (hSocket == SOCKET_ERROR)
			{
				for (int i = 0; i < iSocketCount; ++i)
				{
					closesocket(hSocketArray[i]);
				}

				WSACleanup();

				return 0;
			}

			cout << hSocket << " Client Connect" << endl;

			// 이벤트 객체를 생성해서 접속된 클라이언트 소켓과 이벤트 객체를 묶어준다.
			hEvent = WSACreateEvent();

			// FD_READ : 데이터 수신이 가능하면 윈도우 메시지를 발생시키는 이벤트 종류를 지정한다.
			// FD_CLOSE : 상대가 접속을 종료할 수 있는 이벤트 종류를 지정한다.
			WSAEventSelect(hSocket, hEvent, FD_READ | FD_CLOSE);

			hSocketArray[iSocketCount] = hSocket;
			hSocketEventArray[iSocketCount] = hEvent;
			++iSocketCount;

			// 접속을 성공한 클라에게 send
			send(hSocket, "접속 성공", 9, 0);
		}

		// Recv 이벤트를 검사한다.
		if (wsaNetEvents.lNetworkEvents & FD_READ)
		{
			if (wsaNetEvents.iErrorCode[FD_READ_BIT] != 0)
			{
				cout << "Recv Error !!" << endl;
				continue;
			}

			char strPacket[PACKET_LENGTH] = {};
			int iLength = recv(hSocketArray[dwIndex], strPacket, PACKET_LENGTH, 0);
			
			// 클라이언트에서 send한 Packet 메시지를 출력한다.
			cout << hSocketArray[dwIndex] << " Client Message : "
				<< strPacket << endl;

			// 접속하고 있는 다른 유저에게도 클라에서 전송한 메시지를 보여준다.
			for (int j = 0; j < iSocketCount; ++j)
			{
				// 리슨 소켓 무시
				if (hListen == hSocketArray[j])
					continue;

				// 패킷 메시지를 송신한 자기 자신 무시
				else if (hSocketArray[dwIndex] == hSocketArray[j])
					continue;

				send(hSocketArray[j], strPacket, iLength, 0);
			}
		}

		// close 이벤트가 발생했을 경우
		if (wsaNetEvents.lNetworkEvents & FD_CLOSE)
		{
			if (wsaNetEvents.iErrorCode[FD_CLOSE_BIT] != 0)
			{
				cout << "Close Error !!" << endl;
				continue;
			}

			// 소켓을 닫는다.
			closesocket(hSocketArray[dwIndex]);

			// 개방되어있는 이벤트 객체 핸들을 닫는다.
			WSACloseEvent(hSocketEventArray[dwIndex]);

			// 배열의 뒤에 있는 소켓을 앞으로 한 칸씩 땡겨준다.
			for (DWORD j = dwIndex; j < iSocketCount - 1; ++j)
			{
				hSocketArray[j] = hSocketArray[j + 1];
				hSocketEventArray[j] = hSocketEventArray[j + 1];
			}

			--iSocketCount;
		}
	}

	// 모든 소켓을 종료한다.
	for (int i = 0; i < iSocketCount; ++i)
	{
		closesocket(hSocketArray[i]);
		WSACloseEvent(hSocketEventArray[i]);
	}

	WSACleanup();

	return 0;
}

 

실행 결과

클라이언트 C++ 소스 코드는 멀티 스레드, 크리티컬 섹션 포스팅에 있는

소스 코드와 동일합니다. (https://pony11.tistory.com/14?category=868582)

'윈도우즈 소켓 프로그래밍 기초' 카테고리의 다른 글

IOCP (I/O Completion Port)  (0) 2020.01.03
Overlapped I/O  (0) 2019.12.29
I/O 멀티플렉싱 (fd_set, select)  (0) 2019.12.29
멀티 스레드, 크리티컬 섹션 동기화  (0) 2019.12.29
TCP / UDP 통신  (0) 2019.12.27