select 모델은 ::select(...)에서 block되는 구조였다.
::WSAEventSelect(...)를 통해 해당 함수 호출은 non-blocking으로 하고 이벤트가 발생하면 통보를 받는 방식으로 구현할 수 있다. 이를 WSAEventSelect 모델이라고 한다.
GameServer.cpp
const int32 BUFSIZE = 1000;
struct Session
{
SOCKET socket;
char recvBuffer[BUFSIZE] = {};
int32 recvBytes = 0;
int32 sendBytes = 0;
};
int main()
{
WSAData wsaData;
if (::WSAStartup(MAKEWORD(2, 2), &wsaData))
return 0;
// 블록킹(Blocking)소켓
// accept -> 접속한 클라가 있을 때
// connect -> 서버 접속 성공했을 때
// send, sendto -> 요청한 데이터를 송신 버퍼에 복사했을 때
// recv, recvfrom -> 수신 버퍼에 도착한 데이터가 있고, 이를 유저레벨 버퍼에 복사했을 때
// 논블록킹(Non-Blocking)
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;
// 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
// 주의 사항
// WSAEventSelect 함수를 호출하면, 해당 소켓은 자동으로 Non-blocking 모드로 전환됨.
// accept() 함수가 리턴하는 소켓은 listenSocket과 동일한 속성을 갖는다
// - 따라서 clientSocket은 FD_READ, FD_WRITE 등을 다시 등록 필요
// 드물게 WSAEWOULDBLOCK 오류가 뜰 수 있으니 예외 처리 필요
// 중요)
// - 이벤트 발생 시, 적절한 소켓 함수 호출해야 함
// - 아니면 다음 번에는 동일 네트워크 이벤트가 발생 X
// ex) FD_READ 이벤트 떳으면 recv() 호출해야 하고, 안하면 FD_READ 두 번 다시 X
// 1) cout, event
// 2) waitAll : 모두 기다림? 하나만 완료 되어도 OK?
// 3) timeoout
// 4) 지금은 false
// return : 완료된 첫 번째 인덱스
// WSAWaitForMultipleEvents
// 1) socket
// 2) eventObject : socket과 연동된 이벤트 객체 핸들을 넘겨주면, 이벤트 객체를 non-signaled
// 3) networkEvent : 네트워크 이벤트 / 오류 정보가 저장
// WSAEnumNetworkEvents
vector<WSAEVENT> wsaEvents; // session 갯수만큼 WSAEVENT도 만들어준다
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]);
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_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
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;
}
}
// FD_CLOSE 처리
if (networkEvents.lNetworkEvents & FD_CLOSE)
{
// TODO : Remove Socket
}
}
// 윈속 종료
::WSACleanup();
}
WSAEventSelect 모델은 한계가 있다. ::WSAWaitForMultipleEvents 가 하나의 함수로 통지 받을 수 있는 최대 이벤트 개수는 64이다.
1. select 모델과 다른 점은?
- select 모델과 다르게 select에서 block 되지 않는다. 이벤트로 소켓상태를 통지 받는다.
- 그러나 WSAWaitForMultipleEvents에서 block 된다.
2. accept 함수가 리턴하는 socket은 어떤 성질을 갖는지? 이 성질이 read, write에는 어떤 영향을 미치는지?
- listen socket과 비슷한 성질을 갖는데, read, write 이벤트를 받으려면 이를 다시 등록해야 한다.
3. event 발생시, 적절한 함수 호출하지 않으면 발생하는 문제는?
- 동일 네트워크에서 동일한 이벤트가 더 이상 발생하지 않는다.
4. vector WSAEVENT 와 vector Session 을 1:1 매칭 하는 이유는?
- WSAWaitForMultipleEvents 으로 얻을 수 있는 index를 통해 해당 이벤트가 어떤 세션의 이벤트 였는지 알기 위함.
참조 : [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 - 인프런 | 강의 (inflearn.com)
Overlapped모델 (콜백기반) (0) | 2021.11.18 |
---|---|
Overlapped 모델(이벤트기반) (0) | 2021.11.17 |
Select 모델 (0) | 2021.11.11 |
논블록킹 소켓 (0) | 2021.11.09 |
소켓 옵션 (0) | 2021.11.08 |
댓글 영역