KoreanFoodie's Study
[C++ 게임 서버] 4-10. PacketSession 본문
[C++ 게임 서버] 4-10. PacketSession
핵심 :
1. 간단하게 패킷을 만들어 보자. 사실 데이터에 size 와 id(프로토콜) 을 헤더로 붙이면, 그게 기본적인 패킷의 형태다.
2. Session 을 제거하고 넣을 때는 기존 컨테이너의 정합성이 깨지지 않도록 주의하자. 이를 회피하기 위해 클라가 종료될 때, 바로 세션을 제거하지 않고, 대신 Disconnect 이벤트를 등록만 한 다음 다른 쓰레드가 Dispatch 될 때 세션을 제거하게 만들 수도 있다.
저번까지는 버퍼에 우리가 보내려고 하는 문자열을 그냥 담아서 보냈다. 그런데 실제 게임에서는 이 데이터가 무슨 데이터인지에 대한 정보가 필요한데, 흔히 우리는 이것을 패킷이라고 한다.
즉, 해당 정보에 대한 메타 정보(흔히 헤더 부분에 붙인다)와 실제 정보를 합치면, 간단한 패킷이 탄생하게 된다.
어떤 게임이냐에 따라, 또 플젝에 따라 패킷의 구조와 사이즈는 천차만별이겠지만... 일단 여기서는 기본적으로 size 와 id 에 대한 값을 패킷의 헤더 부에 붙여서 주고 받는 식으로 만들어 보자.
Session 을 상속한 PacketSession 을 다음과 같이 만들 것이다 :
PacketSession.h
struct PacketHeader
{
uint16 size;
uint16 id; // 프로토콜ID (ex. 1=로그인, 2=이동요청)
};
class PacketSession : public Session
{
public:
PacketSession();
virtual ~PacketSession();
PacketSessionRef GetPacketSessionRef() { return static_pointer_cast<PacketSession>(shared_from_this()); }
protected:
virtual int32 OnRecv(BYTE* buffer, int32 len) sealed;
virtual int32 OnRecvPacket(BYTE* buffer, int32 len) abstract;
};
앞서 설명한 size 와 id 가 PakcetHeader 로 들어가는 것을 볼 수 있다. 이때 id 는 일반적으로 프로토콜의 ID 를 상징할 것이다! 😁
PacketSession.cpp
PacketSession::PacketSession()
{
}
PacketSession::~PacketSession()
{
}
// [size(2)][id(2)][data....][size(2)][id(2)][data....]
int32 PacketSession::OnRecv(BYTE* buffer, int32 len)
{
int32 processLen = 0;
while (true)
{
int32 dataSize = len - processLen;
// 최소한 헤더는 파싱할 수 있어야 한다
if (dataSize < sizeof(PacketHeader))
break;
PacketHeader header = *(reinterpret_cast<PacketHeader*>(&buffer[processLen]));
// 헤더에 기록된 패킷 크기를 파싱할 수 있어야 한다
if (dataSize < header.size)
break;
// 패킷 조립 성공
OnRecvPacket(&buffer[processLen], header.size);
processLen += header.size;
}
return processLen;
}
OnRecv 함수를 보자. 일단 위의 주석에 패킷이 어떤 식으로 생겨 먹었는지가 쓰여 있다. 🤣
본문에서는, dataSize 를 받아서 PacketHeader 의 사이즈보다 큰지, 패킷의 크기가 PakcetHeader 명세에 쓰여 있는 사이즈보다 작지 않은지를 검사하고, OnRecvPacket 을 호출해 패킷을 조립한다.
결국 Buffer 에서 processLen 의 위치를 읽어나가면서 패킷을 처리하는 것이다.
일단 OnRecvPacket 은, 다음과 같이 로그 정보만 찍어주도록 간단히 만들어 볼 것이다. 아래는 GameServer 에서 사용하는 GameSession 이고,
int32 GameSession::OnRecvPacket(BYTE* buffer, int32 len)
{
PacketHeader header = *((PacketHeader*)buffer);
cout << "Packet ID : " << header.id << "Size : " << header.size << endl;
return len;
}
DummyClient 에서 사용하는 ServerSession 의 경우, 다음과 같다 :
virtual int32 OnRecvPacket(BYTE* buffer, int32 len) override
{
PacketHeader header = *((PacketHeader*)buffer);
//cout << "Packet ID : " << header.id << "Size : " << header.size << endl;
char recvBuffer[4096];
::memcpy(recvBuffer, &buffer[4], header.size - sizeof(PacketHeader));
cout << recvBuffer << endl;
return len;
}
그럼 행복하게 패킷을 주고 받으면서 패킷을 조립하고 읽으면 될 것 같은데... GameServer 의 main 함수에서 생 Buffer 가 아니라 Packet 을 보내주도록 약간의 수정만 가해주면 된다!
while (true)
{
SendBufferRef sendBuffer = GSendBufferManager->Open(4096);
BYTE* buffer = sendBuffer->Buffer();
((PacketHeader*)buffer)->size = (sizeof(sendData) + sizeof(PacketHeader));
((PacketHeader*)buffer)->id = 1; // 1 : Hello Msg
::memcpy(&buffer[4], sendData, sizeof(sendData));
sendBuffer->Close((sizeof(sendData) + sizeof(PacketHeader)));
GSessionManager.Broadcast(sendBuffer);
this_thread::sleep_for(250ms);
}
원래는 sendBuffer 만 보내줬는데, 여기에 PacketHeader 를 끼워서 보내주고 있다. 😂
PacketHeader 에서 size 와 id 는 각각 uint16 으로 2Byte 니까, 2 + 2 = 4 이므로, &buffer[4] 위치에 우리가 실제로 보내려고 하는 sendData 를 ::memcpy 해주고 있다. 😉
그럼 이제 아무런 문제가 없을 것처럼 보인다.
실제로 프로젝트를 실행한 후 Client 를 닫아 버리면, GameSession 의 BroadCast 에서 크래시가 난다. 😅
void GameSessionManager::Broadcast(SendBufferRef sendBuffer)
{
WRITE_LOCK;
for (GameSessionRef session : _sessions)
{
session->Send(sendBuffer);
}
}
코드를 보면, 왠지 Loop 를 도는 와중 아이템을 하나 삭제해서 Container 가 깨진 것 같다는 생각이 드는데... 실제로 코드를 계속 파고 들어가다 보면,
GameSession::Send -> GameSession::RegisterSend(Send 한 번 보냈으니까 다시 낚싯대를 걸어 준다) -> Session::HandleError(SendEvent 를 다시 등록해야 하는데, 클라이언트를 강제 종료했으니 뭔가 에러가 발생할 것이다) -> Session::DisConnect 를 호출하게 된다.
문제는, Session::DisConnect 함수가 다음과 같다는 것이다 :
void Session::Disconnect(const WCHAR* cause)
{
if (_connected.exchange(false) == false)
return;
// TEMP
wcout << "Disconnect : " << cause << endl;
RegisterDisconnect();
OnDisconnected(); // 컨텐츠 코드에서 재정의
GetService()->ReleaseSession(GetSessionRef());
}
RegisterDisconnect 를 해서 Disconnect 처리를 해 주는 부분은 좋은데, 문제는 OnDisconnected 부분 부터이다.
void GameSession::OnDisconnected()
{
GSessionManager.Remove(static_pointer_cast<GameSession>(shared_from_this()));
}
OnDisConnected 함수는 자신이 속한 SessionManager 를 이용해 자신의 세션을 세션의 Set (Set<GameSessionRef> _sessions 로 정의되어 있음) 으로부터 제거하게 된다.
그러면, 실질적으로 연결이 끊겼을때, _sessions 컨테이너에서 자기 자신을 제거하게 되므로, 결과적으로 다음과 같은 동작을 하게 된다 :
void GameSessionManager::Broadcast(SendBufferRef sendBuffer)
{
WRITE_LOCK;
for (GameSessionRef session : _sessions)
{
session->Send(sendBuffer);
_sessions.erase(session);
}
}
그럼 이걸 해결하기 위해서, OnDisconnected 함수가 등장하는 곳을 옮겨주도록 하자. 이사할 위치는... 바로 ProcessDisconnect 되시겠다.
void Session::ProcessDisconnect()
{
_disconnectEvent.owner = nullptr; // RELEASE_REF
OnDisconnected(); // 컨텐츠 코드에서 재정의
GetService()->ReleaseSession(GetSessionRef());
}
이렇게 옮겨주면, 이제 클라가 강제 종료 되더라도 Session 이 바로 제거되지 않고, Disconnect 이벤트만 Register 되어 있다가, 나중에 다른 쓰레드가 Dispatch 될 때 실질적인 Disconnect 처리를 진행하므로써 안정적으로 Session 이 _sessions 로부터 제거될 것이다 😄
'Game Dev > Game Server' 카테고리의 다른 글
[C++ 게임 서버] 5-2. PacketHandler (0) | 2023.12.15 |
---|---|
[C++ 게임 서버] 5-1. BufferHelper (0) | 2023.12.14 |
[C++ 게임 서버] 4-9. SendBuffer Pooling (0) | 2023.12.14 |
[C++ 게임 서버] 4-8. SendBuffer (0) | 2023.12.13 |
[C++ 게임 서버] 4-7. RecvBuffer (0) | 2023.12.12 |