이전 Select Model에 이어 WSAEventSelect Model에 대해 공부해 보자.
1. WSAEventSelect 모델의 개념
WSAEventSelect함수가 핵심이 되는 모델이다.
소켓과 관련한 네트워크 이벤트를 이벤트 객체를 통해 감지한다.
1.1. 이벤트 객체 관련
객체 관련해서 사용하는 함수는 아래와 같다.
- WSACreateEvent
이벤트 객체를 생성한다. Manual-Reset, Non-Signaled 상태에서 시작한다. - WSACloseEvent
이벤트 객체를 삭제한다. - WSAWaitForMultipleEvents
이벤트 발생을 감지한다. - WSAEnumNetworkEvents
구체적으로 어떤 이벤트가 일어났는지를 확인한다.
1.2. 소켓 - 이벤트 객체 연동
- WSAEventSelect(socket, event, networkEvents);
관심 있는 네트워크 이벤트 - FD_ACCEPT
접속한 클라이언트가 있음. accep() - FD_READ
데이터 수신 가능. recv(), recvfrom() - FD_WRITE
데이터 송신 가능. send(), sendto() - FD_CLOSE
상대가 접속 종료 - FD_CONNECT
통신을 위한 연결 절차 완료 - FD_OOB
2. 천천히 구현
클라이언트나 소켓을 만드는 부분은 이전에 다 써놨으니 생략.
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;
// Select와 다르게 전체 리셋이 필요 없음
while (true)
{
//WSA_MAXIMUM_WAIT_EVENTS
//MAXIMUM_WAIT_OBJECTS
int32 index = ::WSAWaitForMultipleEvents(wsaEvents.size(), &wsaEvents[0], FALSE, WSA_INFINITE, FALSE);
if (index == WSA_WAIT_FAILED)
continue;
index -= WSA_WAIT_EVENT_0;
//::WSAResetEvent(wsaEvent[index]);
// WSAEnumNetworkEvents에서 간접적으로 리셋을 해 줌
WSANETWORKEVENTS networkEvents;
if (::WSAEnumNetworkEvents(sessions[index].socket, wsaEvents[index], &networkEvents) == SOCKET_ERROR)
continue;
// Listener 소켓 체크
// Client 소켓 체크...
}
WSAWaitForMultipleEvents()의 파라미터에 주목해 보자.
#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
DWORD
WSAAPI
WSAWaitForMultipleEvents(
_In_ DWORD cEvents,
_In_reads_(cEvents) const WSAEVENT FAR * lphEvents,
_In_ BOOL fWaitAll,
_In_ DWORD dwTimeout,
_In_ BOOL fAlertable
);
#endif /* INCL_WINSOCK_API_PROTOTYPES */
cEvents는 이벤트의 개수(count)를 받는다. wsaEvents의 size()를 넣어준다.
lphEvents엔 이벤트 객체의 시작 주소를 넣어준다. 벡터는 동적 배열이므로 &wsaEvents [0]를 넣어준다.
fWaitAll은 모든 이벤트가 올라올 때 까지 기다릴 지의 여부다. 모든 걸 기다리는 것은 무리이므로 FALSE.
dwTimeout도 걸어둘 필요가 없으니 WSA_INFINITE.
fAlertable도 쓸 일이 없다. FALSE.
리턴 시엔 완료된 첫번째 이벤트의 인덱스를 리턴한다.
원래 수동 리셋을 해 줘야 했기 때문에
::WSAResetEvent(wsaEvent[index]) 가 필요했으나,
::WSAEnumNetworkEvents가 리셋을 해 주기 때문에 굳이 리셋을 위한 코드가 필요가 없다.
그리고 ::WSAEnumNetworkEvents()에 대해서도 자세히 보자.
#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
int
WSAAPI
WSAEnumNetworkEvents(
_In_ SOCKET s,
_In_ WSAEVENT hEventObject,
_Out_ LPWSANETWORKEVENTS lpNetworkEvents
);
#endif /* INCL_WINSOCK_API_PROTOTYPES */
3가지의 파라미터가 보인다.
SOCKET s는 말 그대로 소켓.
WSAEVENT hEventObject는 이벤틔 객체의 핸들이다. 호출 시 이 인자로 이벤트 객체 핸들을 넘겨주면,
이벤트 객체를 자동으로 Non-Signaled 상태로 바꿔준다.
LPWSANETWORKEVENTS lpNetworkEvents에는 네트워크 이벤트 또는 오류 정보가 저장된다.
3. 소켓 체크
// Listener 소켓 체크
if(networkEvents.lNetworkEvents & FD_ACCEPT)
{
// 에러 체크
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;
}
}
accept()를 통해 클라이언트와의 접속을 활성화 한다.
이제 통신할 세션이 생겼으니 이벤트를 생성하고 ::WSAEventSelect() 함수로 READ, WRITE, CLOSE에 대해 감지하도록 한다.
accept() 함수가 리턴하는 소켓은 listenSocket과 동일한 속성을 같기 때문에,
clientSocket에 대해 다시 FD_READ 등을 등록하는 것이다.
::WSAEventSelect() 함수를 호출하면 해당 소켓은 자동으로 논 블로킹 모드로 전환된다는 점도 기억해야 한다.
// Client Session 소켓 체크
if (networkEvents.lNetworkEvents & FD_READ || networkEvents.lNetworkEvents & FD_WRITE)
{
// 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...
// Write...
}
이제 연결된 클라이언트의 세션의 소켓을 확인해야 한다.
먼저 READ든 WRITE든 준비가 됐는지 확인하고 에러 처리를 해 준다.
// 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.recvBytes = 0;
s.sendBytes = 0;
}
cout << "Send Data = " << sendLen << endl;
이제 READ와 WRITE에 대해 처리를 해 주어야 한다.
여기까지 넘어왔다는 것은 아무 문제 없이 준비가 잘 됐다는 뜻이므로 recv()와 send()의 실행 자체엔 문제가 없을 것이다.
하지만 보면 ::WSAGetLastError() != WSAEWOULDBLOCK의 논블로킹 예외 처리 구분이 보인다.
원랜 필요 없다고 보는 게 맞겠지만, 드물게 저 오류가 생긴다고 한다. 따라서 항상 예외처리 해 주었다.
그리고 이렇게 이벤트가 발생하면 그 이벤트에 맞는 소켓 함수를 호출해야만 한다.
만약 FD_READ 이벤트가 발생했는데, recv()를 호출하지 않으면, FD_READ 이벤트가 다시는 발생하지 않아,
나중의 데이터에 대한 처리가 불가능하게 된다.
4. 실행 결과
정상적으로 데이터를 주고받음을 확인할 수 있다.
여기서 아래와 같은 상황이 생길 수도 있다.
이는 서버가 올라오기 전에 클라이언트가 접속을 시도해서 나타날 수 있는 문제다.
this_thread::sleep_for(1s)를 클라이언트에 추가하여 서버가 올라올 시간 동안 대기하게끔 해 주면 된다.
아니면 직접 서버를 먼저 실행한 후에 클라이언트를 실행해도 된다.
5. 마무리
뭔가 네트워크 쪽은 이렇게 포스팅하면서 복습을 해야 할 것 같다.
한 번에 이해가 잘 안 됐기 때문에 더 확실한 복습이 필요하다고 생각했다.
'Study > C++ & C#' 카테고리의 다른 글
[C++] Completion Port Model (0) | 2023.06.17 |
---|---|
[C++] Overlapped Model (0) | 2023.06.16 |
[C++] Select Model (0) | 2023.06.12 |
[C++] STL : Vector (0) | 2023.04.18 |
[C++] 스마트 포인터 (0) | 2023.02.04 |