KoreanFoodie's Study
[C++ 게임 서버] 4-4. Session #1 본문
[C++ 게임 서버] 4-4. Session #1
핵심 :
1. 게임 세션에 대한 기본 동작과 구현을 숙지하자. 제일 중요한 것은, 흐름이다.
2. 세션에 대한 Reference Count 는 상황에 맞게 잘 처리해 주어야, IocpObject 나 Session 에서 Memory Leak 이 발생하지 않는다.
이번 글에서는, Session 클래스를 만들고 기본적인 Accept 동작을 할 수 있도록 구현을 진행해 보겠다.
먼저 Session 클래스의 구현을 한 번 보자.
Session.h
#pragma once
#include "IocpCore.h"
#include "IocpEvent.h"
#include "NetAddress.h"
class Service;
/*--------------
Session
---------------*/
class Session : public IocpObject
{
friend class Listener;
friend class IocpCore;
friend class Service;
public:
Session();
virtual ~Session();
public:
void Disconnect(const WCHAR* cause);
shared_ptr<Service> GetService() { return _service.lock(); }
void SetService(shared_ptr<Service> service) { _service = service; }
public:
/* 정보 관련 */
void SetNetAddress(NetAddress address) { _netAddress = address; }
NetAddress GetAddress() { return _netAddress; }
SOCKET GetSocket() { return _socket; }
bool IsConnected() { return _connected; }
SessionRef GetSessionRef() { return static_pointer_cast<Session>(shared_from_this()); }
private:
/* 인터페이스 구현 */
virtual HANDLE GetHandle() override;
virtual void Dispatch(class IocpEvent* iocpEvent, int32 numOfBytes = 0) override;
private:
/* 전송 관련 */
void RegisterConnect();
void RegisterRecv();
void RegisterSend();
void ProcessConnect();
void ProcessRecv(int32 numOfBytes);
void ProcessSend(int32 numOfBytes);
void HandleError(int32 errorCode);
protected:
/* 컨텐츠 코드에서 오버로딩 */
virtual void OnConnected() { }
virtual int32 OnRecv(BYTE* buffer, int32 len) { return len; }
virtual void OnSend(int32 len) { }
virtual void OnDisconnected() { }
public:
// TEMP
char _recvBuffer[1000];
private:
weak_ptr<Service> _service;
SOCKET _socket = INVALID_SOCKET;
NetAddress _netAddress = {};
Atomic<bool> _connected = false;
private:
USE_LOCK;
/* 수신 관련 */
/* 송신 관련 */
private:
/* IocpEvent 재사용 */
RecvEvent _recvEvent;
};
기본적으로 정보를 Get/Set 하는 부분을 정의하고, Disconnect 하는 것도 만들어 놓는다.
GetHandle 이나 Dispatch 같은 함수는 파생 클래스에서 오버라이드하도록 만들어야 한다. 추후 이 부분에 대해서는 흐름을 좀 더 자세히 파악해 볼 것이다.
Register.../Process... 가 Prefix 로 붙은 함수는 전송 및 수신과 관련된 함수이다.
마지막으로, OnConnected 같은 이벤트 함수를 만들어서, 파생 클래스에서 알아서 커스터마이징 할 수 있도록 하자.
public:
// TEMP
char _recvBuffer[1000];
private:
weak_ptr<Service> _service;
SOCKET _socket = INVALID_SOCKET;
NetAddress _netAddress = {};
Atomic<bool> _connected = false;
private:
USE_LOCK;
/* 수신 관련 */
/* 송신 관련 */
private:
/* IocpEvent 재사용 */
RecvEvent _recvEvent;
아래 부분에는 _recvBuffer 가 임시로 선언되어 있다. 일단 버퍼를 세션에 저장해서 사용할 것이다.
나머지 해당 세션이 속한 서비스나 소켓, NetAddress 등에 대한 정보도 Session 이 갖고 있다.
이제 실질적인 구현을 한 번 보자.
Session.cpp
#include "pch.h"
#include "Session.h"
#include "SocketUtils.h"
#include "Service.h"
/*--------------
Session
---------------*/
Session::Session()
{
_socket = SocketUtils::CreateSocket();
}
Session::~Session()
{
SocketUtils::Close(_socket);
}
void Session::Disconnect(const WCHAR* cause)
{
if (_connected.exchange(false) == false)
return;
// TEMP
wcout << "Disconnect : " << cause << endl;
OnDisconnected(); // 컨텐츠 코드에서 오버로딩
SocketUtils::Close(_socket);
GetService()->ReleaseSession(GetSessionRef());
}
HANDLE Session::GetHandle()
{
return reinterpret_cast<HANDLE>(_socket);
}
void Session::Dispatch(IocpEvent* iocpEvent, int32 numOfBytes)
{
switch (iocpEvent->eventType)
{
case EventType::Connect:
ProcessConnect();
break;
case EventType::Recv:
ProcessRecv(numOfBytes);
break;
case EventType::Send:
ProcessSend(numOfBytes);
break;
default:
break;
}
}
void Session::RegisterConnect()
{
}
void Session::RegisterRecv()
{
if (IsConnected() == false)
return;
_recvEvent.Init();
_recvEvent.owner = shared_from_this(); // ADD_REF
WSABUF wsaBuf;
wsaBuf.buf = reinterpret_cast<char*>(_recvBuffer);
wsaBuf.len = len32(_recvBuffer);
DWORD numOfBytes = 0;
DWORD flags = 0;
if (SOCKET_ERROR == ::WSARecv(_socket, &wsaBuf, 1, OUT &numOfBytes, OUT &flags, &_recvEvent, nullptr))
{
int32 errorCode = ::WSAGetLastError();
if (errorCode != WSA_IO_PENDING)
{
HandleError(errorCode);
_recvEvent.owner = nullptr; // RELEASE_REF
}
}
}
void Session::RegisterSend()
{
}
void Session::ProcessConnect()
{
_connected.store(true);
// 세션 등록
GetService()->AddSession(GetSessionRef());
// 컨텐츠 코드에서 오버로딩
OnConnected();
// 수신 등록
RegisterRecv();
}
void Session::ProcessRecv(int32 numOfBytes)
{
_recvEvent.owner = nullptr; // RELEASE_REF
if (numOfBytes == 0)
{
Disconnect(L"Recv 0");
return;
}
// TODO
cout << "Recv Data Len = " << numOfBytes << endl;
// 수신 등록
RegisterRecv();
}
void Session::ProcessSend(int32 numOfBytes)
{
}
void Session::HandleError(int32 errorCode)
{
switch (errorCode)
{
case WSAECONNRESET:
case WSAECONNABORTED:
Disconnect(L"HandleError");
break;
default:
// TODO : Log
cout << "Handle Error : " << errorCode << endl;
break;
}
}
위에서부터 하나하나 살펴보자.
Session 의 경우, 소멸자가 불릴 때는 연결된 소켓도 닫아 주어야 할 것이다. 또한 클라이언트를 갑자기 종료한다던지 하는 식으로 세션 연결이 끊길 때도, 비슷한 처리를 해 주어야 한다.
Disconnect 함수에서 exchange 는, 원래 값이 인자로 준 값과 다를 경우에만 true 를 리턴하고, 바꾸려는 값과 원본 값이 같으면 false 를 리턴한다. 즉, 값을 딱 1번만 바꾸고 싶을 때 exchange 를 활용하면 좋다.
연결이 끝기면, OnDisconnedted 이벤트 함수를 호출하고, 현재 속한 서비스에서 현재 세션에 대한 Reference Count 를 감소 시켜야 한다. 그래야 세션 메모리가 제대로 해제될 수 있을 것이다.
사실 Dispatch 가 어떻게 보면 현재 구조에서의 핵심이라고 볼 수 있는데, 전달된 IocpEvent 에 맞게 Connect, Recv, Send 등의 다양한 동작을 지원하게 된다.
Recv 는 이전에 했던 것처럼 WSARecv 를 호출하고, Connect 되었을 경우 서비스에 세션을 등록하고 수신 등록을 실시한다. 나머지는 한번 쓱 보면 이해가 갈 것이다 😂
아, 참고로 RegisterRecv 함수에서 소켓 에러가 발생했는데 이것이 'Pending' 상태로 인한 것이 아니라면, 뭔가 심각하게 잘못되었다는 뜻이므로 _recvEvent.owner 를 nullptr 로 만들어서 Reference Count 를 줄어줘야 한다. 그래야 _recvEvent 가 사용이 끝났을 때, 메모리가 제대로 해제될 것이다! 😄
위에서는 Session 클래스의 기본적인 구현에 대한 이야기를 했지만, 사실 흐름을 파악하는 것이 제일 중요하다.
따라서 GameServer 의 main 에서부터 우리가 만든 Session 이 어떤 식으로 사용되는지 흐름을 파악해 보자.
GameServer.cpp
int main()
{
ServerServiceRef service = MakeShared<ServerService>(
NetAddress(L"127.0.0.1", 7777),
MakeShared<IocpCore>(),
MakeShared<GameSession>, // TODO : SessionManager 등
100);
ASSERT_CRASH(service->Start());
for (int32 i = 0; i < 5; i++)
{
GThreadManager->Launch([=]()
{
while (true)
{
service->GetIocpCore()->Dispatch();
}
});
}
GThreadManager->Join();
}
일단 GameServer 로부터 시작할 것이다. 위를 보면, 서버가 서비스를 만들고, 세션을 넣어주는 것을 알 수 있다.
그 후, 루프를 돌면서 서비스에 대해 Dispatch 를 해 주게 되는데...
그럼 IocpCore::Dispatch 가 불리게 된다.
bool IocpCore::Dispatch(uint32 timeoutMs)
{
DWORD numOfBytes = 0;
ULONG_PTR key = 0;
IocpEvent* iocpEvent = nullptr;
if (::GetQueuedCompletionStatus(_iocpHandle, OUT &numOfBytes, OUT &key, OUT reinterpret_cast<LPOVERLAPPED*>(&iocpEvent), timeoutMs))
{
IocpObjectRef iocpObject = iocpEvent->owner;
iocpObject->Dispatch(iocpEvent, numOfBytes);
}
else
{
int32 errCode = ::WSAGetLastError();
switch (errCode)
{
case WAIT_TIMEOUT:
return false;
default:
// TODO : 로그 찍기
IocpObjectRef iocpObject = iocpEvent->owner;
iocpObject->Dispatch(iocpEvent, numOfBytes);
break;
}
}
return true;
}
그리고 나서 iocpEvent 의 owner 를 찾아 해당 오브젝트에 대해 Dispatch 를 불러주는데... 사실 이 iocpObject 가 바로 Listener 가 된다.
Listener.cpp
void Listener::Dispatch(IocpEvent* iocpEvent, int32 numOfBytes)
{
ASSERT_CRASH(iocpEvent->eventType == EventType::Accept);
AcceptEvent* acceptEvent = static_cast<AcceptEvent*>(iocpEvent);
ProcessAccept(acceptEvent);
}
Listener 는 다시 ProcessAccept 를 호출한다.
void Listener::ProcessAccept(AcceptEvent* acceptEvent)
{
SessionRef session = acceptEvent->session;
if (false == SocketUtils::SetUpdateAcceptSocket(session->GetSocket(), _socket))
{
RegisterAccept(acceptEvent);
return;
}
SOCKADDR_IN sockAddress;
int32 sizeOfSockAddr = sizeof(sockAddress);
if (SOCKET_ERROR == ::getpeername(session->GetSocket(), OUT reinterpret_cast<SOCKADDR*>(&sockAddress), &sizeOfSockAddr))
{
RegisterAccept(acceptEvent);
return;
}
session->SetNetAddress(NetAddress(sockAddress));
session->ProcessConnect();
RegisterAccept(acceptEvent);
}
그럼 여기서 AcceptEvent 의 세션을 조회하고, Session 에 대해 ProcessConnect 를 불러준다.
Session.cpp
void Session::ProcessConnect()
{
_connected.store(true);
// 세션 등록
GetService()->AddSession(GetSessionRef());
// 컨텐츠 코드에서 오버로딩
OnConnected();
// 수신 등록
RegisterRecv();
}
그럼 세션을 등록하고, RegisterRecv 함수를 호출한다.
void Session::RegisterRecv()
{
if (IsConnected() == false)
return;
_recvEvent.Init();
_recvEvent.owner = shared_from_this(); // ADD_REF
WSABUF wsaBuf;
wsaBuf.buf = reinterpret_cast<char*>(_recvBuffer);
wsaBuf.len = len32(_recvBuffer);
DWORD numOfBytes = 0;
DWORD flags = 0;
if (SOCKET_ERROR == ::WSARecv(_socket, &wsaBuf, 1, OUT &numOfBytes, OUT &flags, &_recvEvent, nullptr))
{
int32 errorCode = ::WSAGetLastError();
if (errorCode != WSA_IO_PENDING)
{
HandleError(errorCode);
_recvEvent.owner = nullptr; // RELEASE_REF
}
}
}
여기서 이제 _recvEvent 의 owner 를 Session 으로 바꾸어 주게 되고, 앞으로는 세션이 살아있는 한, Dispatch 의 로직은 Session 의 로직이 불릴 것이다! 😉
RegisterRecv 이후에는, 세션은 클라가 보낸 정보를 받는 역할을 하므로, Dispatch 에서 ProcessRecv 가 불릴 것이다.
그럼 이제 ProcessRecv 에서 데이터 전송 관련한 처리가 진행되게 된다! 😁
void Session::ProcessRecv(int32 numOfBytes)
{
_recvEvent.owner = nullptr; // RELEASE_REF
if (numOfBytes == 0)
{
Disconnect(L"Recv 0");
return;
}
// TODO
cout << "Recv Data Len = " << numOfBytes << endl;
// 수신 등록
RegisterRecv();
}
'Game Dev > Game Server' 카테고리의 다른 글
[C++ 게임 서버] 4-6. Session #3 (0) | 2023.12.12 |
---|---|
[C++ 게임 서버] 4-5. Session #2 (0) | 2023.12.07 |
[C++ 게임 서버] 4-3. Server Service (0) | 2023.12.05 |
[C++ 게임 서버] 4-2. IocpCore (0) | 2023.10.26 |
[C++ 게임 서버] 4-1. Socket Utils (0) | 2023.10.25 |