이전에 select 모델을 알아봤는데,
select 모델 같은 경우에는 매번 사용할 때 마다 소켓 셋을 초기화 하고 다시 등록해줘야 했다.
이런 작업도 결국 성능에 영향을 끼치기 때문에 많은 양의 접속과 송수신 요청을 처리하기에는 조금 아쉽다는 단점이 있었다.
그런 단점을 개선한 WSAEventSelect Model에 대해서 알아보자.
WSAEventSelect Model
WSAEventSelect 모델은 WSAEventSelect() 함수가 핵심적인 역할을 한다는 뜻에서 붙인 이름이다.
각 소켓마다 이벤트 객체를 하나씩 생성하여 짝지어두면, 네트워크 이벤트가 발생할 때마다
이벤트 객체는 신호 상태가 된다.
따라서 이벤트 객체의 신호 상태를 통해 네트워크 이벤트 발생을 감지할 수 있다.
WSAEventSelect 모델이 동작하기 위한 이벤트 객체 관련 함수들
- 생성 : WSACreateEvent (수동 리셋 Manual-Reset + Non-Signaled 상태 시작)
- 삭제 : WSACloseEvent
- 신호 상태 감지 : WSAWaitForMultipleEvents
- 구체적인 네트워크 이벤트 알아내기 : WSAEnumNetworkEvents
- 소켓과 이벤트 객체 짝짓기(연동) : WSAEventSelect(socket, event, networkEvents);
관찰할 네트워크 이벤트
- FD_ACCEPT : 접속한 클라가 있음 accept
- FD_READ : 데이터 수신 가능 recv, recvfrom
- FD_WRITE : 데이터 송신 가능 send, sendto
- FD_CLOSE : 상대가 접속 종료
- FD_CONNECT : 통신을 위한 연결 절차 완료
- FD_OOB
주의 사항
- WSAEvenSelect 함수를 호출하면, 해당 소켓은 자동으로 넌블로킹 모드 전환.
- accept() 함수가 리턴하는 소켓은 listenSocket과 동일한 속성을 갖는다.
- 따라서 clinetSocket은 FD_READ, FD_WRITE 등을 다시 등록 필요.
- 드물게 WSAEWOULDBLOCK 오류가 뜰 수 있으니 예외 처리가 필요하다.
중요
- 이벤트 발생 시, 적절한 소켓 함수를 호출해야 한다.
- 그렇지 않으면 다음 번에는 동일 네트워크 이벤트가 발생하지 않는다.
- 예를 들어 우리가 FD_READ 이벤트를 관찰하고 있던 중에 FD_READ가 완료되었다고 통지가 왔을 때,
반드시 recv를 해줘야지만 다음 번에 FD_READ를 관찰할 수 있게 된다. - 즉, 통지를 해줬을 때 적절한 함수를 짝지어 호출하지 않으면 호출 할 때까지 또다시 통지를 해주지 않는다.
WSAEventSelect를 호출해서 등록만하고 통지를 받는건 별도의 함수로 한다.
바로 WSAWaitForMultipleEvents 이다.
- count, event 이벤트 개수와, 이벤트 포인터
- waitAll : 모두 기다릴지 하나만 완료되어도 빠져 나올지 정함.
- timeOut : 타임아웃
- 지금은 false (WSAEventSelect와는 무관하다.)
- return : 처음으로 완료된 인덱스
이제 누가 완료되었는지 알았지만(이벤트 통지 받음) 해당 이벤트가 무엇이었는지를 모르기 때문에 이를
확인하기위한 함수가 있다.
바로 WSAEnumNetworkEvents 이다.
- socket
- eventObject : socket과 연동된 이벤트 객체 핸들을 넘겨주면, 이벤트 객체를 non-signaled 상태로 바꿈.
- networkEvent : 네트워크 이벤트 / 오류 정보가 저장
<예제 코드>
.
.
.
vector<WSAEVENT> wsaEvents;
vector<Session> sessions;
sessions.reserve(100);
WSAEVENT listenEvent = ::WSACreateEvent();
wsaEvents.push_back(listenEvent);
sessions.push_back(Session{ listenSocket });
if (::WSAEventSelect(listenSocket, listenEvent, FD_ACCEPT | FD_CLOSE) == SOCKET_ERROR)
return 0;
while (true)
{
int32 index = ::WSAWaitForMultipleEvents(wsaEvents.size(), &wsaEvents[0], FALSE, WSA_INFINITE, FALSE);
if (index == WSA_WAIT_FAILED)
continue;
index -= WSA_WAIT_EVENT_0;
//::WSAResetEvent(wsaEvents[index]); -> WSAEnumNetworkEvents 포함 되어있음.
WSANETWORKEVENTS networkEvents;
if (::WSAEnumNetworkEvents(sessions[index].socket, wsaEvents[index], &networkEvents) == SOCKET_ERROR)
continue;
//Listener 소켓 체크
if (networkEvents.lNetworkEvents & FD_ACCEPT)
{
//Error-Check
if (networkEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
continue;
SOCKADDR_IN clientAddr;
int32 addrLen = sizeof(clientAddr);
SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
if (clientSocket != INVALID_SOCKET)
{
cout << "Client Connected" << endl;
WSAEVENT clientEvent = ::WSACreateEvent();
wsaEvents.push_back(clientEvent);
sessions.push_back(Session{ clientSocket });
if (::WSAEventSelect(clientSocket, clientEvent, FD_READ | FD_WRITE | FD_CLOSE) == SOCKET_ERROR)
return 0;
}
}
//Client Session 소켓 체크
if (networkEvents.lNetworkEvents & FD_READ || networkEvents.lNetworkEvents & FD_READ)
{
//Error-Check
if (networkEvents.lNetworkEvents & FD_READ && (networkEvents.iErrorCode[FD_READ_BIT]!=0))
continue;
//Error-Check
if (networkEvents.lNetworkEvents & FD_WRITE && networkEvents.iErrorCode[FD_WRITE_BIT] != 0)
continue;
Session& s = sessions[index];
//Read
if (s.recvBytes == 0)
{
int32 recvLen = ::recv(s.socket, s.recvBuffer, BUFSIZE, 0);
if (recvLen == SOCKET_ERROR && ::WSAGetLastError() != WSAEWOULDBLOCK)
{
//여기까지 진입 했다면 진짜 문제가 있는 상황
//TODO : Remove Session
continue;
}
s.recvBytes = recvLen;
cout << "Recv Data = " << recvLen << endl;
}
//Write
if (s.recvBytes > s.sendBytes)
{
int32 sendLen = ::send(s.socket, &s.recvBuffer[s.sendBytes], s.recvBytes - s.sendBytes, 0);
if (sendLen == SOCKET_ERROR && ::WSAGetLastError() != WSAEWOULDBLOCK)
{
//여기까지 진입 했다면 진짜 문제가 있는 상황
//TODO : Remove Session
continue;
}
s.sendBytes += sendLen;
if (s.recvBytes == s.sendBytes)
{
s.sendBytes = 0;
s.recvBytes = 0;
}
cout << "Send Data = " << sendLen << endl;
}
}
//FD_CLOSE 처리
if (networkEvents.lNetworkEvents & FD_CLOSE)
{
//TODO : Remove Socket
}
}
.
.
.
sessions.push_back(Session{ listenSocket });
지금 만든 Session은 엄밀히 말해 클라이언트에서 연결한 Session은 아니다.
WSAEvent와 소켓을 1:1 대응을 시켜주기 위하여 listenSocket을 등록시킨 것이다.
1:1 대응을 시키는 이유는 동일한 인덱스를 활용하게 해줘야 관리하기 쉽기 때문이다.
if (::WSAEventSelect(listenSocket, listenEvent, FD_ACCEPT | FD_CLOSE) == SOCKET_ERROR)
llistenSocket과 listenEvent를 짝지어서 listenSocket과 FD_ACCEPT 또는 FD_CLOSE 네트워크 이벤트가 발생하면 listenEvent를 통해 통보 받을 수 있다.
int32 index = ::WSAWaitForMultipleEvents(wsaEvents.size(), &wsaEvents[0], FALSE, WSA_INFINITE, FALSE);
아래 사진 처럼 반환할 대 WSA_WAIT_EVENT_0 에다가 어떤 값이 더해져 있는 것을 알 수 있다.
그러니 우리가 index를 사용할 때는 WSA_WAIT_EVENT_0 빼서 사용해야 한다.
index -= WSA_WAIT_EVENT_0;
WSAWaitForMultipleEvents function (winsock2.h) - Win32 apps
Returns when one or all of the specified event objects are in the signaled state, when the time-out interval expires, or when an I/O completion routine has executed.
learn.microsoft.com
WSANETWORKEVENTS networkEvents;
if (::WSAEnumNetworkEvents(sessions[index].socket, wsaEvents[index], &networkEvents) == SOCKET_ERROR)
FD_ACCEPT | FD_CLOSE 처럼 관찰할 이벤트가 여러개일 수도 있기 때문에 정확히 어떤 이벤트가 완료되었는지
알 수 없다. 그래서 WASEnumNetworkEvents를 사용한다.
그리고 Listen 소켓을 체크하여 networkEvents.lNetworkEvents와 FD_ACCEPT를 비트 연산해서 동일할 경우
클라이언트가 연결하였다는 뜻이기 때문에 클라이언트 소켓을 만들어서 클라이언트 정보를 저장한다.
그리고 클라이언트 소켓의 이벤트를 체크해서 데이터를 송 수신 한다.
select 모델과 비슷하지만 매번 소켓셋을 리셋하고 초기화 해줄 필요가 없다.
select 모델과 마찬가지로 너무 많이 만들 수는 없다. (최대 64개)
클라이언트를 서버와 연동 시키고 클라쪽에서 select 또는 WSAEventSelect 사용해서 만드는건 나쁘지 않다.
하지만 굳이 서버를 많드는데 iocp를 두고 select 또는 WSAEventSelect를 사용할 필요는 없다.
'네트워크 프로그래밍' 카테고리의 다른 글
Overlpapped 모델(이벤트 기반) (0) | 2023.02.23 |
---|---|
비동기 vs 동기, Blocking vs Non-Blocking (0) | 2023.02.23 |
Select Model (0) | 2023.02.20 |
TCP vs UDP (0) | 2023.02.20 |
UDP 통신에서 보낸 데이터 횟수와 받은 데이터 횟수가 다른 경우 (0) | 2022.12.18 |