저번엔 PacketHeader 구조체와 PacketSession, CAuthSession 클래스를 만들어봤다.
이번엔 CAuthSession의 OnReadPacket 안에서 readbuffer -> 패킷 클래스 객체 를 해줄 함수들을 만들어보자.
ClientPacketHandler
클라이언트로부터 오는 패킷이나 클라이언트에게 보낼 패킷을 처리하는 함수들을 만들어보자.
원래는 게임서버 강의에서 배운 패키핸들러를 거의 그대로 갖다 쓸려 했는데 멀티스레드 활용 방법의 차이때문에 그대로 쓸 수 없게됐다.
자세히 말하자면 게임서버의 패킷핸들러는 패킷핸들러가 클라이언트가 보내온 패킷에 따라 적절한 실행함수를 호출하여 DB작업이라던가 하는 작업을 바로 해버리고, 성공하면 true 실패하면 false를 리턴한다. 그리고나서 CP에 다시 read 이벤트를 등록하는 방식으로 작동했다. 이렇게 작동할 수 있는 이유는 워커스레드와 메인스레드의 구분이 없기 때문이다. 워커스레드가 이벤트 감지, 등록을 모두 알아서 효율적으로 하기 때문.
하지만 내가 만든 인증서버는 메인스레드만 epoll_wait, read, write를 수행하기때문에 여기에 DB작업까지 해버리면 그냥 싱글스레드 프로그램이 되어버린다. 그래서 나는 패킷에 따라 함수를 호출하는게 아니라 워커스레드에서 호출할 함수를 받아와서 JobQueue에 집어넣어서 워커스레드가 그것을 꺼내서 실핼할 수 있게 만들어줘야 한다.
아무튼 그래서 두가지 역할에 대한 함수를 만들었다.
- GetCallback : 버퍼와 패킷길이를 넣으면 워커스레드에서 실행할 함수를 받는다.(리턴이 아니라 OUT 인자로 받음)
리턴값은 bool로, 버퍼로 패킷을 만드는데 실패하면 false를 리턴한다. 성공하면 true.
성공 시 인자로 받는 함수를 Job에 넣어 JobQueue 에 Push 하고, 실패 시 세션을 Disconnect 시킨다던지 하면 적절할 것 같다. - MakeWriteBuffer : 패킷을 넣으면 WriteBufferRef를 리턴한다.
이 함수는 워커스레드가 작업을 다 마치고 세션의 WriteBufferQueue에 직렬화한 패킷을 넣기위해 사용할 예정이다.
ClientPacketHandler.h
#pragma once
#include "Types.h"
#include "Session.h"
#include "Protocol.pb.h"
#include "WriteBuffer.h"
#include <memory>
#include <functional>
extern std::function<bool(std::function<void()>&, PacketSessionRef, BYTE*, int32)> GPacketHandler[UINT16_MAX];
enum : uint16
{
PKT_C_SIGNUP = 1000,
PKT_S_SIGNUP = 1001,
PKT_C_VERIFY_EMAIL = 1002,
PKT_S_VERIFY_EMAIL = 1003,
PKT_C_LOGIN = 1004,
PKT_S_LOGIN = 1005,
};
bool Handle_INVALID(std::function<void()>& outFunc, PacketSessionRef session, BYTE* buffer, int32 len);
void Handle_C_SIGNUP(PacketSessionRef session, Protocol::C_SIGNUP pkt);
void Handle_C_VERIFY_EMAIL(PacketSessionRef session, Protocol::C_VERIFY_EMAIL pkt);
void Handle_C_LOGIN(PacketSessionRef session, Protocol::C_LOGIN pkt);
class ClientPacketHandler
{
public:
static void Init()
{
for (int i = 0; i < UINT16_MAX; ++i)
GPacketHandler[i] = Handle_INVALID;
GPacketHandler[PKT_C_SIGNUP] = [](std::function<void()>& outFunc, PacketSessionRef session, BYTE* buffer, int32 len) {
return GetCallback<Protocol::C_SIGNUP>(outFunc, Handle_C_SIGNUP, session, buffer, len);
};
GPacketHandler[PKT_C_VERIFY_EMAIL] = [](std::function<void()>& outFunc, PacketSessionRef session, BYTE* buffer, int32 len) {
return GetCallback<Protocol::C_VERIFY_EMAIL>(outFunc, Handle_C_VERIFY_EMAIL, session, buffer, len);
};
GPacketHandler[PKT_C_LOGIN] = [](std::function<void()>& outFunc, PacketSessionRef session, BYTE* buffer, int32 len) {
return GetCallback<Protocol::C_LOGIN>(outFunc, Handle_C_LOGIN, session, buffer, len);
};
}
static bool PacketHandler(std::function<void()>& outFunc, PacketSessionRef session, BYTE* buffer, int32 len)
{
PacketHeader* header = reinterpret_cast<PacketHeader*>(buffer);
return GPacketHandler[header->id](outFunc, session, buffer, len);
}
private:
template<typename PacketType, typename ProcessFunc>
static bool GetCallback(std::function<void()>& outFunc, ProcessFunc func, PacketSessionRef session, BYTE* buffer, int32 len)
{
PacketType pkt;
if (false == pkt.ParseFromArray(buffer + sizeof(PacketHeader), len - sizeof(PacketHeader)))
return false;
outFunc = [func, session, pkt](){ func(session, pkt); };
return true;
}
template<typename T>
static WriteBufferRef MakeWriteBuffer(T& pkt, uint16 pktId)
{
int headerSize = sizeof(PacketHeader);
int pktSize = pkt.ByteSizeLong();
WriteBufferRef writeBuffer = std::make_shared<WriteBuffer>(headerSize + pktSize);
PacketHeader header;
header.id = pktId;
header.size = headerSize + pktSize;
writeBuffer->AppendBuffer(reinterpret_cast<BYTE*>(&header), headerSize);
pkt.SerializeToArray(writeBuffer->GetCopyBuffer(), pktSize);
return writeBuffer;
}
};
사용
void CAuthSession::OnReadPacket(BYTE* buffer, int len)
{
/*...*/
std::function<void()> callback; // OUT받을 콜백함수 준비
// 나중에 write해줄 session(자기자신) 준비
PacketSessionRef session = std::static_pointer_cast<PacketSession>(shared_from_this());
// 콜백 받음
bool result = ClientPacketHandler::PacketHandler(callback, session, buffer, len);
if (result == false) // 실패시 실패처리 TODO
std::cout << "error" << std::endl;
else // 성공시 잡에 넣고 JobQueue에 넣기
{
std::cout << "success" << std::endl;
JobRef job = std::make_shared<Job>(callback);
GThreadManager->InsertJob(job);
}
}
사용은 이렇게 하고 PacketHandler함수가 좀 복잡해서 내부 작동 순서에 대해 좀 기록해놔야곘다.
- PacketHandler(OUT callback, session, buffer, len) 호출.
- 버퍼에 적힌 헤더로 어떤 패킷 처리해야하는지 GPacketHandler 함수배열 사용해서 가져옴.
- GPacketHandler[n] 에 저장된 함수는 GetCallback탬플릿함수에 나중에 워커스레드가 호출할 함수를 인자로 넣어 호출한다.
- GetCallback탬플릿함수는 OUT callback에 나중에 워커스레드가 호출할 함수 를 람다함수에서 호출되도록해서 넣어준다.
좀 복잡한데 결국은 id에 따라 바뀌는 type을 처리하기 어려워서 똥꼬쇼를 한 느낌이다. 프로토버프 클래스가 아니라 내가 관리하는 클래스였다면 상속으로 조금은 쉽게 해결했을지도? 해보진 않아서 또 다른 어려움이 있을지 모르지만...
완성하고 나니 게임서버 강의의 패킷핸들러에서 진짜 조금만 수정한 느낌인데, 고민은 되게 오래했다.
일단 Job에서 void() 함수만 수용하니 리턴을 void()로 해야하는데 그러면 어떻게 워커스레드가 원하는 작업을 하게 만들지? 에 대한 해결법이 쉽게 떠오르지 않았다.
'프로젝트 > Project_Island' 카테고리의 다른 글
| 10. 프로젝트 리펙토링 + 모듈화 (0) | 2026.03.06 |
|---|---|
| 9. DBConnectionPool 기초다지기 (0) | 2026.03.05 |
| 7. Protobuf, PacketHeader, PacketSession (0) | 2026.03.04 |
| 6. 멀티스레드 작업 (0) | 2026.03.03 |
| 5. WriteBuffer와 Send작업 (0) | 2026.03.02 |