※ Rookiss님의 [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 강의를 보고 정리한 글입니다.
지난번에 했던 콜백 기반 overlapped 모델을 정리하고 Completion Port(IOCP) 모델을 알아보자
이번 서버는 Main Thread가 Accept를 담당하고 다른 쓰레드가 recv를 맡아보는 식으로 제작할 것이다.
⇒ 논블로킹 소켓이 필요없음!
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;
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;
중앙 APC 큐 역할인 Completion Port를 생성해준다.
HANDLE iocpHandle = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
여기서 사용하는 함수인 CreateIoCompletionPort()
함수는 두 가지 쓰임새를 가지고 있다.
⇒ 생성할 땐 파라미터를 위와 같이 넣어주면된다.
이번에 구현하는 서버의 메인스레드는 Accept만 담당한다.
⇒ 블로킹 소켓으로 accept
while (true) {
SOCKADDR_IN clientAddr;
int32 addrLen = sizeof(clientAddr);
SOCKET clientSocket;
clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
if (clientSocket == INVALID_SOCKET)
return 0;
이번 서버는 1대1 연결을 목표로하는 게아닌 여러 클라이언트와 연결을 목표로하기 때문에 Session을 동적할당해 줄 것이다.
또한 관리할 vector를 만들어주자.
vector<Session*> sessionManager;
...
Session* session = new Session();
session->socket = clientSocket;
sessionManager.push_back(session);
그리고 소켓을 Completion Port에 등록해주자.
::CreateIoCompletionPort((HANDLE)clientSocket, iocpHandle, (ULONG_PTR)session, 0);
이 때의 CreateIoCoompletionPort()
함수는 소켓 등록으로 사용된다.
파라미터는 각각 이렇게 넣어준다.
현재는 입출력이 완료 됐을 때를 관찰하기 위해 Completion Port에 등록한 것 뿐이다. 그러니 한 번의 입출력은 해주어야한다!!
WSARecv
를 해주기 앞서 저번에 WSARecv
를 할 땐 overlapped를 session 맨 앞에 두어 session의 정보를 활용하는 곳에 썼다. 하지만 지금은 소켓을 등록해줄 때 key값으로 session을 넣어준 상태!!
이를 잘 이용하면 다른 정보도 줄 수 있을 것이다.
OverlappedEx 구조체 만들기
enum IO_TYPE {
READ,
WRITE,
ACCEPT,
CONNECT,
};
struct OverlappedEx {
WSAOVERLAPPED overlapped = {};
int32 type = 0;// read, write, accept, connect
};
우리는 어떤 타입의 입출력이었는 지를 포함하여 보내줄 것이다.
WSARecv
WSABUF wsaBuf;
wsaBuf.buf = session->recvBuffer;
wsaBuf.len = BUFSIZE;
OverlappedEx* overlappedEx = new OverlappedEx();
overlappedEx->type = IO_TYPE::READ;
DWORD recvLen = 0;
DWORD flags = 0;
::WSARecv(clientSocket, &wsaBuf, 1, &recvLen, &flags, &overlappedEx->overlapped, NULL);
이제 Completion Port에서 완료된 결과를 가져와 후처리를 해볼 것이다.
결과 받아오기
void WorkerThreadMain(HANDLE iocpHandle) {
while (true) {
DWORD bytesTransferred = 0;
Session* session = nullptr;
OverlappedEx* overlappedEx = nullptr;
BOOL ret = ::GetQueuedCompletionStatus(iocpHandle,&bytesTransferred,
(ULONG_PTR*)&session, (LPOVERLAPPED*)&overlappedEx, INFINITE);
if (ret == FALSE || bytesTransferred == 0) {
// TODO: 연결 끊기
continue;
}
cout << "Recv Data IOCP = " << bytesTransferred << endl;
GetQueuedCompletionStatus()
함수를 이요해 입출력한 바이트 수, key값, overlapped를 가져왔다.
GetQueuedCompletionStatus의 파라미터
그 후 다시 한 번 WSARecv
를 해준다.
WSABUF wsaBuf;
wsaBuf.buf = session->recvBuffer;
wsaBuf.len = BUFSIZE;
DWORD recvLen = 0;
DWORD flags = 0;
::WSARecv(session->socket, &wsaBuf, 1, &recvLen, &flags,
&overlappedEx->overlapped, NULL);
왜 해줄까?
다시 Recv를 안해주면 이 소켓은 이대로 끝이 난다. 하지만 다시 Recv를 해줄 경우 이 소켓에서 오는 정보를 한 번 뿐만아니라 계속 받아줄 수 있다.
다른 입출력이하고싶다면 OverlappedEx를 새로 만들어서 넣어주면된다.