WSAEventSelect 모델은 WSAEventSelect()
함수가 핵심 역할을 한다는 뜻에서 붙인 이름이다.
WSAEventSelect 모델을 사용하면 소켓과 관련된 네트워크 이벤트를 이벤트 객체를 통해 감지한다. 즉, 아래 그림과 같이 이벤트 객체를 소켓당 하나씩 생성하고 이벤트 객체들을 관찰하면 멀티스레드를 사용하지 않고도 여러 소켓을 처리할 수 있다.
WSAEventSelect 모델을 사용하면 소켓 함수 호출이 성공할 수 있는 시점을 이벤트 객체를 통해 알 수 있다. 아래 그림과 같이 소켓에 대해 이벤트 객체를 짝지어두면, 네트워크 이벤트가 발생할 때마다 이벤트 객체가 신호 상태가 된다. 따라서 이벤트 객체의 상태 변화를 관찰함으로써 네트워크 이벤트 발생을 감지할 수 있다. 그러나 이것만으로는 구체적으로 어떤 종류의 이벤트가 발생했는지 또는 어떤 오류가 발생했는지 알 수 없다는 문제가 있다.
WSAEventSelect 모델이 동작하려면 아래 표의 왼쪽과 같은 기능이 필요하다. 표의 오른쪽은 관련 함수이다.
필요 기능 | 관련 함수 |
---|---|
이벤트 객체 생성과 제거 | WSACreateEvent() , WSACloseEvent() |
소켓과 이벤트 객체 짝짓기 | WSAEventSelect() |
이벤트 객체의 신호 상태 감지하기 | WSAWaitForMultipleEvents() |
구체적인 네트워크 이벤트 알아내기 | WSAEnumNetworkEvents() |
WSAEventSelect 모델을 이용한 소켓 입출력 절차는 다음과 같다.
WSACreateEvent()
함수를 이용해 이벤트 객체를 생성한다.WSAEventSelect()
함수를 이용해 소켓과 이벤트 객체를 짝지음과 동시에, 처리할 네트워크 이벤트를 등록한다.WSAWaitForMultipleEvents()
함수를 호출해 이벤트 객체가 신호 상태가 되기를 기다린다. 등록한 네트워크 이벤트가 발생하면 소켓과 연관된 이벤트 객체가 신호 상태가 된다.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
);
네트워크 이벤트 | 의미 |
---|---|
FD_ACCEPT | 접속한 클라이언트가 있다. |
FD_READ | 데이터 수신이 가능하다. |
FD_WRITE | 데이터 송신이 가능하다. |
FD_CLOSE | 상대가 접속을 종료했다. |
FD_CONNECT | 통신을 위한 연결 절차가 끝났다. |
FD_OOB | OOB(Out-Of-Band) 데이터가 도착했다. |
아래 코드는 소켓 s와 이벤트 객체를 짝짓고 FD_READ
와 FD_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_ACCEPT | accept() |
FD_READ | recv() , recvfrom() |
FD_WRITE | send() , sendto() |
FD_CLOSE | 없음 |
FD_CONNECT | 없음 |
FD_OOB | recv() , 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
);
WSAWaitForMultipleEvents()
함수를 사용할 때는 이벤트 객체 핸들을 모두 배열에 넣어 전달해야 한다. cEvents는 배열 원소 개수, lphEvents는 배열의 시작 주소를 나타낸다. cEvents의 최댓값은 WSA_MAXIMUM_WAIT_EVENTS
(64)이다.TRUE
이면 모든 이벤트 객체가 신호 상태가 될 때까지 기다린다. FALSE
면 이벤트 객체 중 하나가 신호 상태가 되는 즉시 리턴한다.WSAWaitForMultipleEvents()
함수가 리턴한다. 대기 시간으로 WSA_INFINIT
값을 사용하면 조건이 만족될 때까지 무한히 기다린다.FALSE
를 전달한다.WSAEnumNetworkEvents()
함수는 소켓과 관련하여 발생한 구체적인 네트워크 이벤트를 알려주는 역할을 한다.
// 성공: 0, 실패: SOCKET_ERROR
int WSAEnumNetworkEvents(
SOCKET s,
WSAEVENT hEventObject,
LPWSANETWORKEVENTS lpNetworkEvents
);
NULL
값을 넘겨주면 된다.WSANETWORKEVENTS 구조체는 아래와 같다.
typedef struct _WSANETWORKEVENTS {
long lNetwrokEvents;
int iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, *LPWSANETWORKEVENTS;
lNetwrokEvents 인자에는 아래 표의 상수값이 조합된 형태로 저장되어, 발생한 네트워크 이벤트를 알려준다. 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;
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 모델을 이용해 TCP 서버를 작성한 코드이다.
https://github.com/LEEBONGHAK/TCP-IP_window_socket/tree/main/Chapter10/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];
\0
)를 추가하기 위해 BUFSIZE + 1 길이인 바이트 배열을 선언했다. recvbytes, sendbytes는 각각 받은 바이트 수와 보낸 바이트 수를 유지하기 위한 변수다.WSA_MAXIMUM_WAIT_EVENTS
)로 정의하고 있다.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");
AddSocketInfo()
함수를 호출해 연결 대기 소켓 정보를 추가한다.WSAEventSelect()
함수를 호출해 연결 대기 소켓과 이벤트 객체를 짝짓는다. 연결 대기 소켓은 FD_ACCEPT
와 FD_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;
WSAWaitForMultipleEvents()
함수를 호출해 이벤트 객체가 신호 상태가 될 때까지 대기한다.WSAWaitForMultipleEvents()
함수의 리턴 값은 신호 상태가 된 이벤트 객체의 배열 인덱스 + WSA_WAIT_EVENT_0
값이다. 따라서 실제 인덱스 값을 얻으려면 WSA_WAIT_EVENT_0
값을 빼야한다.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 }
FD_ACCEPT
이벤트 발생을 체크하고 오류를 처리한다.accept()
함수를 호출해 클라이언트 접속을 처리하고, 클라이언트 정보를 화면에 출력한다.WSA_MAXIMUM_WAIT_EVENTS
(64)개 이상의 소켓은 처리할 수 없다.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 }
FD_READ
또는 FD_WRITE
이벤트 발생을 체크하고 오류를 처리한다.recv()
함수를 호출해 데이터를 읽는다. 오류가 발생하면 RemoveSocketInfo()
함수를 호출해 소켓 정보를 삭제한다. send()
함수를 호출해 데이터를 보낸다. 받은 만큼 모두 보냈다면 받은 바이트 수와 보낸 바이트 수를 다시 0으로 초기화한다.FD_CLOSE
이벤트 발생을 체크하고 오류를 처리한다.RemoveSocketInfo()
함수를 호출해 소켓 정보를 삭제한다.참고 자료
김성우 저, "TCP/IP 윈도우 소켓 프로그래밍", 한빛아카데미, 2018