※ Rookiss님의 [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 강의를 보고 정리한 글입니다.
select모델은 초기화와 등록을 계속 반복해야한다는 아쉬움이 있다.
이번에 알아볼 WSAEventSelect 모델은 이벤트를 기반으로한 모델이다.
⇒ 소켓과 관련된 네트워크 이벤트를 이벤트 객체를 통해 감지한다.
소켓과 이벤트 객체를 연동시켜야한다. ⇒ 소켓 개수만큼 이벤트 함수를 만들어주어야함.
연동 함수: WSAEventSelect(socket,event,networkEvents)
현재 사용할 네트워크 이벤트는 5가지 정도 있다.
소켓 listen까지의 작업을 실행해준다.
WSAData wsaData;
if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
return 0;
SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (listenSocket == INVALID_SOCKET)
return 0;
u_long on = 1;
if (::ioctlsocket(listenSocket, FIONBIO, &on) == INVALID_SOCKET)
return 0;
SOCKADDR_IN serverAddr;
::memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY);
serverAddr.sin_port = ::htons(7777);
if (::bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
return 0;
if (::listen(listenSocket, SOMAXCONN) == SOCKET_ERROR)
return 0;
cout << "Accept" << endl;
vector<WSAEVENT> wsaEvents;
vector<Session> sessions;
sessions.reserve(100);
WSAEVENT listenEvent = ::WSACreateEvent(); // 이벤트 생성
wsaEvents.push_back(listenEvent);
sessions.push_back(Session{ listenSocket });
// listenSocket은 연결되었을 때, 연결을 끊었을 때 이벤트가 활성화 된다.
if (::WSAEventSelect(listenSocket, listenEvent, FD_ACCEPT | FD_CLOSE) == SOCKET_ERROR)
return 0;
listenSocket은 클라이언트와 연결된 세션은 아니지만 WSAEvnetSelect모델 특성상 동일한 인덱스를 활용되게끔 해야 관리가 쉬우니 일단 session을 만들어 sessions에 등록해준다.
이제 WSAWaitForMultipleEvents함수를 통해 이벤트 신호를 감지해보자.
파라미터: WSAWaitForMultipleEvents(이벤트 수, 이벤트 주소, waitAll 유무, 지금은 false)
리턴값: 완료된 첫번째 인덱스 + WSA_WAIT_EVENT_0
while (true) {
int32 index = ::WSAWaitForMultipleEvents(wsaEvents.size(), &wsaEvents[0], FALSE, WSA_INFINITE,FALSE);
// 공식 문서를 보면 WSA_WAIT_EVENT_0라는 걸 더해줌. 빼줘야 정상적으로 사용가능
if (index == WSA_WAIT_FAILED)
continue;
index -= WSA_WAIT_EVENT_0;
// WSAEnumNetworkEvents에 포함되어있음.
//::WSAResetEvent(wsaEvents[index]);
모두 기다리지 않고, 무한히 기다리는 wsaEvents의 이벤트 신호를 감지하는 함수로 사용하고 있다.
리턴값으론 완료된 이벤트의 인덱스가 나오는데 이상하게 WSA_WAIT_EVENT_0라는것이 더해져있다. 그러니 우리가 활용해 주기위해서 저 값을 빼주자.
그 후 이 이벤트는 Manual-Reset(수동 리셋) 이기 때문에 WSAReserEvent() 함수를 통해 리셋해 주어야하지만, 다음 과정에 활용하는WSAEnumNetworkEvents()에서 이것을 내부적으로 작동해주니 일단 해주지않는다!
WSAEnumNetworkEvents함수를 사용해 어떤 네트워크 이벤트인지 판별해준다.
WSANETWORKEVENTS networkEvents;
if (::WSAEnumNetworkEvents(sessions[index].socket, wsaEvents[index], &networkEvents) == SOCKET_ERROR)
return 0;
그럼 네트워크 이벤트와 에러코드를 담고있는 WSANETWORKEVENTS형의 데이터를 통해 우리가 어떤 이벤트인지 확인 할 수 있다.
lNetworkEvents: 이벤트 종류
iErrorCode: 에러 코드
이제 어떤 이벤트인지 판별까지했으니 그에 따라 후처리를 해주자. 먼저 FD_ACCEPT 이벤트 부터 처리 해주자.
현재 우리는 ACCEPT를 하는 소켓은 listening 소켓밖에 없으므로 그에 맞게 처리해주면 된다.
if (networkEvents.lNetworkEvents & FD_ACCEPT) {
if (networkEvents.iErrorCode[FD_ACCEPT_BIT] != 0) // FD_ACCEPT에 맞는 에러
continue;
//ACCEPT
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 });
//클라이언트 소켓은 READ와 WRITE에 활용할 것!
if (::WSAEventSelect(clientSocket, clientEvent, FD_READ | FD_WRITE | FD_CLOSE) == SOCKET_ERROR)
return 0;
}
}
현재상황에선 FD_READ와 FD_WRITE는 ClientSocket에서만 처리한다. 그를 통해 처리해주자.
if (networkEvents.lNetworkEvents & FD_READ || networkEvents.lNetworkEvents & FD_WRITE) {
// Error-Check: READ인 상황인데 READ 오류라면
if ((networkEvents.lNetworkEvents & FD_READ) && networkEvents.iErrorCode[FD_READ_BIT] != 0)
continue;
// Error-Check: WRITE인 상황인데 WRITE오류라면
if ((networkEvents.lNetworkEvents & FD_WRITE) && networkEvents.iErrorCode[FD_WRITE_BIT] != 0)
continue;
// 세션 받아오기
Session& s = sessions[index];
//recvByte가 0? -> 모두 send함 -> recv받아야함.
if (s.recvBytes == 0) {
int32 recvLen = ::recv(s.socket, s.recvBuffer, BUFSIZE, 0);
if (recvLen == SOCKET_ERROR && ::WSAGetLastError() != WSAEWOULDBLOCK) {
continue;
}
s.recvBytes = recvLen;
cout << "Recv Data= " << recvLen << endl;
}
// Send
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) {
continue;
}
s.sendBytes += sendLen;
if (s.recvBytes == s.sendBytes) {
s.recvBytes = 0;
s.sendBytes = 0;
}
cout << "Send Data= " << sendLen << endl;
}
}
중간에 SOCKET_ERROR가 난 부분에서 주의사항에서 말했던 가끔 뜨는 WSAEWOULDBLOCK인지 체크해주는 것을 볼 수 있다.
WSAEventSelect는 전체 리셋을 계속 해주지 않아도 된다는 장점을 가지고있다.
event 객체를 만들고 연동만 잘 시켜준다면? 모두 잘 매핑되어 있기 때문에 잘 동작한다.
물런 WSAResetEvent를 통해 리셋을 해주긴 해야하지만 그건 EnumNetworkEvents에서 간접적으로 해준다.
이 모델도 최대 갯수가 64개라는 단점이 있다. 그래서 많은 양을 처리해야하는 서버에선 쓸만하진않다. 하지만 자신만 처리해주는 클라이언트라면 쓸만하다!
좋은 글 잘 읽고 갑니다~