WSAEventSelect 모델

bolee·2022년 5월 10일
0

WSAEventSelect 모델은 WSAEventSelect() 함수가 핵심 역할을 한다는 뜻에서 붙인 이름이다.
WSAEventSelect 모델을 사용하면 소켓과 관련된 네트워크 이벤트를 이벤트 객체를 통해 감지한다. 즉, 아래 그림과 같이 이벤트 객체를 소켓당 하나씩 생성하고 이벤트 객체들을 관찰하면 멀티스레드를 사용하지 않고도 여러 소켓을 처리할 수 있다.

WSAEventSelect 모델의 동작 원리

WSAEventSelect 모델을 사용하면 소켓 함수 호출이 성공할 수 있는 시점을 이벤트 객체를 통해 알 수 있다. 아래 그림과 같이 소켓에 대해 이벤트 객체를 짝지어두면, 네트워크 이벤트가 발생할 때마다 이벤트 객체가 신호 상태가 된다. 따라서 이벤트 객체의 상태 변화를 관찰함으로써 네트워크 이벤트 발생을 감지할 수 있다. 그러나 이것만으로는 구체적으로 어떤 종류의 이벤트가 발생했는지 또는 어떤 오류가 발생했는지 알 수 없다는 문제가 있다.

WSAEventSelect 모델이 동작하려면 아래 표의 왼쪽과 같은 기능이 필요하다. 표의 오른쪽은 관련 함수이다.

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

WSAEventSelect 모델을 이용한 소켓 입출력 절차는 다음과 같다.

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

WSAEventSelect 모델에서 사용할 함수를 기능별로 알아보면 아래와 같다.

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

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

// 성공: 이벤트 객체 핸들, 실패: WSA_INVALID_EVENT
WSAEVENT WSACreateEvent();
// 성공: TRUE, 실패: FALSE
BOOL WSACloseEvent(WSAEVENT hEvent);

이 밖에 이벤트 객체의 상태를 변화시키는 함수로 WSASetEvent(), WSAResetEvent()가 있는데 각각 SetEvent(), ResetEvent() 함수와 같다.

소켓과 이벤트 객체 짝짓기

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

// 성공: 0, 실패: SOCKET_ERROR
int WSAEventSelect(
	SOCKET		s,
    WSAEVENT	hEventObject,
    long		lNetworkEvents
);
  • s: 네트워크 이벤트를 처리하고자 하는 소켓
  • hEventObject: 소켓과 연관시킬 이벤트 객체의 핸들
  • lNetworkEvents: 관심 있는 네트워크 이벤트를 비트마크(아래 표) 조합으로 나타낸다. WSAAsyncSelect 모델에서 사용한 것과 동일한 것을 알 수 있다.
네트워크 이벤트의미
FD_ACCEPT접속한 클라이언트가 있다.
FD_READ데이터 수신이 가능하다.
FD_WRITE데이터 송신이 가능하다.
FD_CLOSE상대가 접속을 종료했다.
FD_CONNECT통신을 위한 연결 절차가 끝났다.
FD_OOBOOB(Out-Of-Band) 데이터가 도착했다.

아래 코드는 소켓 s와 이벤트 객체를 짝짓고 FD_READFD_WRITE 이벤트를 등록하는 예를 보여준다.

WSAEVENT hEvent = WSACreateEvent();
WSAEventSelcet(s, hEvent, FD_READ|FD_WRITE);

WSAEventSelect() 함수 사용 시 유의할 점은 다음과 같다.

  • WSAEventSelect() 함수를 호출하면 해당 소켓은 자동으로 넌블로킹 모드로 전환된다.
  • accept() 함수가 리턴하는 소켓은 연결 대기 소켓과 동일한 속성을 갖게 된다. 연결 대기 소켓은 직접 데이터 송수신을 하지 않으므로 FD_READ, FD_WRITE 이벤트를 처리하지 않는다. 반면 accept() 함수가 리턴하는 소켓은 FD_READ, FD_WRITE 이벤트를 처리해댜 하므로, 다시 WSAEventSelect() 함수를 호출해 관심 있는 이벤트를 등록해야 한다.
  • 네트워크 이벤트에 대응하여 소켓 함수를 호출하면 대부분 성공하지만, WSAWOULDBLOCK 오류 코드가 발생하는 경우가 드물게 있다.
  • 네트워크 이벤트 발생 시 적절한 소켓 함수를 호출하지 않으면, 다음 번에는 같은 네트워크 이벤트가 발생하지 않는다. 예를 들어 FD_READ 이벤트에 대응해 recv() 함수를 호출하지 않으면, 동일 소켓에 대한 FD_READ 이벤트는 다시 발생하지 않는다. 따라서 네트워크 이벤트가 발생하면 아래 표의 대응 함수를 호출해야 하며, 그렇지 않을 경우 응용 프로그램이 네트워크 이벤트 발생 사실을 기록해두고 나중에 대응 함수를 호출해야한다.
네트워크 이벤트대응 함수
FD_ACCEPTaccept()
FD_READrecv(), recvfrom()
FD_WRITEsend(), sendto()
FD_CLOSE없음
FD_CONNECT없음
FD_OOBrecv(), recvfrom()

이벤트 객체의 신호 상태 감지하기

WSAWaitForMultipleEvents()함수는 여러 이벤트 객체를 동시에 관찰할 수 있는 기능을 제공한다. WSAForMultipleObjects() 함수와 비슷하다.

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

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

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

// 성공: 0, 실패: SOCKET_ERROR
int WSAEnumNetworkEvents(
	SOCKET s,
    WSAEVENT hEventObject,
    LPWSANETWORKEVENTS lpNetworkEvents
);
  • s: 대상 소켓
  • hEventObject: 대상 소켓 s와 짝지어둔 이벤트 객체 핸들을 넘겨주면 이벤트 객체가 자동으로 비신호 상태로 된다. 이 인자는 선택 사항으로 사용하지 않으려면 NULL 값을 넘겨주면 된다.
  • lpNetworkEvents: WSANETWORKEVENTS 구조체 변수의 주소 값을 전달하면, 발생한 네트워크 이벤트와 오류 정보가 이 변수가 저장된다.

WSANETWORKEVENTS 구조체는 아래와 같다.

typedef struct _WSANETWORKEVENTS {
	long lNetwrokEvents;
    int iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, *LPWSANETWORKEVENTS;

lNetwrokEvents 인자에는 아래 표의 상수값이 조합된 형태로 저장되어, 발생한 네트워크 이벤트를 알려준다. iErrorCode[] 배열에는 네트워크 이벤트와 연관된 오류 정보가 저장된다. 오류 정보를 참조하려면 배열 인덱스 값을 사용해야 한다. 아래 표의 왼쪽은 네트워크 이벤트를, 오른쪽는 배열 인텍스를 나타낸다.

네트워크 이벤트배열 인덱스
FD_ACCEPTFD_ACCEPT_BIT
FD_READFD_READ_BIT
FD_WRITEFD_WRITE_BIT
FD_CLOSEFD_CLOSE_BIT
FD_CONNECTFD_CONNECT_BIT
FD_OOBFD_OOB_BIT

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

SOCKET s;
WSAEVENT hEvent;
WSANETWORKEVENTS NetworkEvents;
...
WSAEnumNetworkEvents(s, hEvent, &NetworkEvents);
// FD_ACCEPT 이벤트 처리
if (NetworkEvents.lNetworkEvents & FD_ACCEPT)
{
	if (NetworkEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
    {
    	printf("오류 코드 = %d\n", NetworkEvents.iErrorCode[FD_ACCEPT_BIT]);
    }
    else
    {
    	// accept() 함수 호출
    }
}
// FD_READ 이벤트 처리
if (NetworkEvents.lNetworkEvents & FD_READ)
{
	if (NetworkEvents.iErrorCode[FD_READ_BIT] != 0)
    {
    	printf("오류 코드 = %d\n", NetworkEvents.iErrorCode[FD_READ_BIT]);
    }
    else
    {
    	// recv() 함수 호출
    }
}

WSAEventSelect 모텔 서버 작성

아래 링크는 WSAEventSelect 모델을 이용해 TCP 서버를 작성한 코드이다.

https://github.com/LEEBONGHAK/TCP-IP_window_socket/tree/main/Chapter10/WSAEventSelect

WSAEventSelect 모델 코드 분석

WSAEventSelect 모델에서도 다른 모델(Select, WSAAsyncSelect)과 마찬가지로 소켓 정보를 응용 프로그램이 관리해야 한다. WSAEventSelect 모델은 동시에 처리할 수 있는 소켓의 개수가 WSA_MAXIMUM_WAIT_EVENTS(64)로 제한되어 있다는 점에서 Select 모델과 같다. 따라서 예제에서 소켓 정보를 관리하기 위해 사용하는 구조 역시 Select 모델과 같다.

위 그림은 예제에서 소켓 정보를 관리하기 위한 구조이다. WSAEventSelect 모델에서 처리할 수 있는 소켓의 최대 개수는 WSA_MAXIMUM_WAIT_EVENTS로 정해져 있으므로, 이 개수만큼 SOCKETINFO 구조체의 주소를 저장할 수 있도록 배열을 선언한다.

새로운 소켓이 생성되면 위 그림처럼 SOCKETINFO 구조체를 동적으로 할당하고, 배열에 주소 값을 저장해둔다.

소켓 정보를 삭제할 때는 포인터 배열 중간에 빈 곳이 없도록 위 그림과 같이 배열의 맨 끝에 있는 유효 원소를 삭제한 위치로 옮긴다.

이제 핵심 코드를 행 단위로 분석해볼 것이다. 소켓 정보 저장을 위한 구조체와 변수는 아래와 같다.

009	// 소켓 정보 저장을 위한 구조체와 변수
010	struct SOCKETINFO
011	{
012		SOCKET sock;
013		char buf[BUFSIZE + 1];
014		int recvbytes;
015		int sendbytes;
016	};
017
018	int nTotalSockets = 0;
019	SOCKETINFO *SocketInfoArray[WSA_MAXIMUM_WAIT_EVETNS];
020	WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVETNS];
  • 10-16: 소켓 정보 저장을 위한 SOCKETINFO 구조체다. 데이터를 받은 후 끝에 널 문자(\0)를 추가하기 위해 BUFSIZE + 1 길이인 바이트 배열을 선언했다. recvbytes, sendbytes는 각각 받은 바이트 수와 보낸 바이트 수를 유지하기 위한 변수다.
  • 18: SOCKETFINFO 구조체의 개수다. 소켓을 생성할 때마다 1씩 증가하고 소켓을 닫을 때마다 1씩 감소한다.
  • 19: SOCKETINFO형 포인터를 저장할 배열이다. 원소 개수는 WSAEventSelect 모델에서 처리할 수 있는 소켓의 최대 개수(WSA_MAXIMUM_WAIT_EVENTS)로 정의하고 있다.
  • 20: 소켓과 짝지을 이벤트 객체 핸들을 저장할 배열이다. 원소 개수는 WSAEventSelect 모델에서 처리할 수 있는 소켓의 최대 개수(WSA_MAXIMUM_WAIT_EVENTS)로 정의하고 있다.
031	int main(int argc, char **argv)
032	{
...
035		// 윈속 초기화
...
040		// socket()
...	
045		// bind()
...
055		// listen()
...	
060		// 소켓 정보 추가 & WSAEventSelect()
061		AddSocketInfo(listen_sock);
062		retval = WSAEventSelect(listen_sock, EventArray[nTotalSockets - 1], FD_ACCEPT|FD_CLOSE);
063		if (retval == SOCKET_ERROR)
064			err_quit("WSAEventSelect");
  • 61: AddSocketInfo() 함수를 호출해 연결 대기 소켓 정보를 추가한다.
  • 52-64: WSAEventSelect() 함수를 호출해 연결 대기 소켓과 이벤트 객체를 짝짓는다. 연결 대기 소켓은 FD_ACCEPTFD_CLOSE 두 개의 네트워크 이벤트만 처리하면 된다.
066		// 데이터 통신에 사용할 변수
067		WSANETWORDEVENTS NetworkEvents;
068		SOCKET client_sock;
069		SOCKADDR_IN clientaddr;
070		int i, addrlen;
071
072		while (1)
073		{
074			// 이벤트 객체 관찰하기
075			i = WSAWaitForMultipleEvents(nTotalSockets, EventArray, FALSE, WSA_INFINITE, FALSE);
076			if (i == WSA_WAIT_FAILED)
077				continue;
078			i -= WSA_WAIT_EVENT_0;
079
080			// 구체적인 네트워크 이벤트 알아내기
081			retval = WSAEnumNetworkEvents(SocketInfoArray[i]->sock, EventArray[i], &NetworkEvents);
082			if (retval == SOCKET_ERROR)
083				continue;
  • 75-77: WSAWaitForMultipleEvents() 함수를 호출해 이벤트 객체가 신호 상태가 될 때까지 대기한다.
  • 78: WSAWaitForMultipleEvents() 함수의 리턴 값은 신호 상태가 된 이벤트 객체의 배열 인덱스 + WSA_WAIT_EVENT_0 값이다. 따라서 실제 인덱스 값을 얻으려면 WSA_WAIT_EVENT_0 값을 빼야한다.
  • 81-83: WSAEnumNetworkEvents() 함수를 호출해 구체적인 네트워크 이벤트를 알아낸다.
085			// FD_ACCEPT 이벤트 처리
086			if (NetworkEvents.lNetworkEvents & FD_ACCEPT)
087			{
088				if (NetworkEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
089				{
090					err_display(NetworkEvents.iErrorCode[FD_ACCEPT_BIT]);
091					continue;
092				}
093	
094				addrlen = sizeof(clientaddr);
095				client_sock = accept(listen_sock, (SOCKADDR *) &clientaddr, &addrlen);
096				if (client_sock == INVALID_SOCKET)
097				{
098					err_display("listen_sock");
099					continue;
100				}
101				printf("[TCP 서버] 클라이언트 접속: IP 주소=%s, 포트 번호=%d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
102
103				if (nTotalSockets >= WSA_MAXIMUM_WAIT_EVENTS)
104				{
105					printf("[오류] 더 이상 접속을 받아들일 수 없습니다!\n");
106					closesocket(client_sock);
107					continue;
108				}
109
110				// 소켓 정보 추가 & WSAEventSelct()
111				AddSocketInfo(client_sock);
112				retval = WSAEventSelect(client_sock, EventArray[nTotalSockets - 1], FD_READ|FD_WRITE|FD_CLOSE);
113				if (retval == SOCKET_ERROR)
114					err_quit("WSAEventSelect()");
115			}
  • 86-92: FD_ACCEPT 이벤트 발생을 체크하고 오류를 처리한다.
  • 94-101: accept() 함수를 호출해 클라이언트 접속을 처리하고, 클라이언트 정보를 화면에 출력한다.
  • 103-108: WSA_MAXIMUM_WAIT_EVENTS(64)개 이상의 소켓은 처리할 수 없다.
  • 111-115: 소켓 정보를 추가한 후 WSAEventSelect() 함수를 호출해 FD_READ, FD_WRITE, FD_CLOSE 세 개의 네트워크 이벤트를 등록한다.
117			// FD_READ & FD_WRITE 이벤트 처리
118			if (NetworkEvents.lNetworkEvents & FD_READ || NetworkEvents.lNetworkEvents & FD_WRITE)
119			{
120				if (NetworkEvents.lNetworkEvents & FD_READ && NetworkEvents.iErrorCode[FD_READ_BIT] != 0)
121				{
122					err_display(NetworkEvents.iErrorCode[FD_READ_BIT]);
123					continue;
124				}
125				if (NetworkEvents.lNetworkEvents & FD_WRITE && NetworkEvents.iErrorCode[FD_WRITE_BIT] != 0)
126				{
127					err_display(NetworkEvents.iErrorCode[FD_WRITE_BIT])
128					continue;
129				}
130
131				SOCKETINFO *ptr = SocketInfoArray[i];
132
133				if (ptr->recvbytes == 0)
134				{
135					// 데이터 받기
136					retval = recv(ptr->sock, ptr->buf, BUFSIZE, 0);
137					if (retval == SOCKET_ERROR)
138					{
139						if (WSAGetLastError() != WSAEWOULDBLOCK)
140						{
141							err_display("recv()");
142							RemoveSocketInfo(i);
143						}
144						continue;
145					}
146				}
147				ptr->recvbytes = retval;
148				// 받은 데이터 출력
149				ptr->buf[retval] = '\0';
150				addrlen = sizeof(clientaddr);
151				getpeername(ptr->sock, (SOCKADDR *) &clientaddr, &addrlen);
152				printf("[TCP/%s: %d] %s\n", inet_ntoa(clientaddt.sin_addr), ntohs(clientaddr.sin_port), ptr->buf);
153			}
154	
155			if (ptr->recvbytes > ptr->sendbytes)
156			{
157				// 데이터 보내기
158				retval = send(ptr->sock, ptr->buf + ptr->sendbytes, ptr->recvbytes - ptr->sendbytes, 0);
159				if (retval == SOCKET_ERROR)
160				{
161					if (WSAGetLastError() != WSAEWOULDBLOCK)
162					{
163						err_display("send()");
164						RemoveSocketInfo(i);
165					}
167					continue;
168				}
169				ptr->sendbytes += retval;
170				// 받은 데이터를 모두 보냈는지 체크
171				if (ptr->recvbytes == ptr->sendbytes)
172					ptr->recvbytes = ptr->sendbytes = 0;
173			}
174		}
175	
176		// FD_CLOSE 이벤트 처리
177		if (NetworkEvents.lNetworkEvents & FD_CLOSE)
178		{
179			if (NetworkEvents.iErrorCode[FD_CLOSE_BIT] != 0)
180				err_display(NetworkEvents.iErrorCode[FD_CLOSE_BIT]);
181			RemoveSocketInfo(i);
182		}
183
184		// 윈속 종료
185		WSACleanup();
186		return 0;
187	}
  • 118-129: FD_READ 또는 FD_WRITE 이벤트 발생을 체크하고 오류를 처리한다.
  • 131: 발생한 네트워크 이벤트에 대응하는 소켓 정보에 쉽게 접근할 수 있도록 ptr 변수에 구조체를 저장해둔다.
  • 133-153: 받은 바이트 수가 0일 경우에만 recv() 함수를 호출해 데이터를 읽는다. 오류가 발생하면 RemoveSocketInfo() 함수를 호출해 소켓 정보를 삭제한다.
  • 155-173: 받은 바이트 수가 보낸 바이트 수보다 크면 send() 함수를 호출해 데이터를 보낸다. 받은 만큼 모두 보냈다면 받은 바이트 수와 보낸 바이트 수를 다시 0으로 초기화한다.
  • 177-180: FD_CLOSE 이벤트 발생을 체크하고 오류를 처리한다.
  • 181: RemoveSocketInfo() 함수를 호출해 소켓 정보를 삭제한다.

참고 자료
김성우 저, "TCP/IP 윈도우 소켓 프로그래밍", 한빛아카데미, 2018

0개의 댓글