프로토버퍼 연동이 끝났으니 이제 사용법을 알아보자.
Protocol.proto
syntax = "proto3";
package Protocol;
message BuffData
{
uint64 buffId = 1;
float remainTime = 2;
repeated uint64 victims = 3;
}
message S_TEST
{
uint64 id = 1;
uint32 hp = 2;
uint32 attack = 3;
repeated BuffData buffs = 4;
}
이 프로토 파일을 보면 패키지를 Protocol로 하고 각 패킷을 작성했는데 Protocol이 네임스페이스, 각 패킷의 이름들이 클래스 명이라고 생각하면 된다. 실제로 사용한 코드를 보자.
Protocol::S_TEST pkt;
pkt.set_id(1000);
pkt.set_hp(200);
pkt.set_attack(20);
{
Protocol::BuffData* buf = pkt.add_buffs();
buf->set_buffid(100);
buf->set_remaintime(3.5f);
buf->add_victims(5);
}
{
Protocol::BuffData* buf = pkt.add_buffs();
buf->set_buffid(200);
buf->set_remaintime(2.3f);
buf->add_victims(4);
buf->add_victims(1);
}
Protocol의 S_TEST 패킷을 pkt라고 만들었다. 점을 찍어서 확인해보면 그냥 클래스를 사용하는 것 처럼 다양한 함수들이 있는 것을 볼 수 있다. set 함수를 사용하여 각 맴버의 값을 넣을 수 있고 직접 정의한 자료형(위에서는 BuffData)의 가변 데이터(buffs)는 add라는 함수를 사용하고 특이하게 포인터를 반환한다. 이때 정의한 자료형의 포인터를 반환하는데 전에 실습했던 패킷 관리 방법과 유사한 것을 볼 수 있다. victims는 직접 정의한 자료형이 아니라 uint64이기 때문에 따로 반환값 없이 파라미터로 값을 넣어주면 된다.
pkt을 그저 데이터를 보내기 위한 임시 객체로도 사용할 수 있지만 함수내에서 계속 들고 있으면서 맴버 변수로도 사용해도 된다.
지금은 버프를 하나하나 추가했는데 벡터나 리스트 형태로 저장된 데이터를 한 번에 저장하는 방법은 없는지 궁금해진다.
auto m = pkt.mutable_buffs();
m->Add();
pkt의 함수를 보면 add_buffs뿐만 아니라 mutable_buffs가 있는데 이 함수를 통해 받은 객체의 Add를 보면 아래 사진과 같이 여러개로 오버로딩 되어있는 것을 볼 수 있다.
보면 begin과 end를 받아 리스트 형태로 저장된 데이터도 받을 수 있도록 구현되어 있다.
이렇게 패킷을 채웠으면 이제 버퍼에 집어넣어야 된다.
ServerPacketHandler.h
template<typename T>
SendBufferRef MakeSendBuffer(T& pkt, uint16 pktId)
{
const uint16 dataSize = static_cast<uint16>(pkt.ByteSizeLong());
const uint16 packetSize = dataSize + sizeof(PacketHeader);
SendBufferRef sendBuffer = GSendBufferManager->Open(packetSize);
PacketHeader* header = reinterpret_cast<PacketHeader*>(sendBuffer->Buffer());
header->size = packetSize;
header->id = pktId;
ASSERT_CRASH(pkt.SerializeToArray(&header[1], dataSize));
sendBuffer->Close(packetSize);
return sendBuffer;
}
패킷핸들러에 SendBuffer를 만드는 함수를 탬플릿으로 만들었다. pkt을 받아 dataSize를 가져오고 패킷 크기는 dataSize에 헤더를 더한 값이므로 새로 변수를 만들어 계산해준다. 그 이후는 지금까지 했던 방식과 같다. 버퍼를 열고 헤더를 채워넣고 데이터를 채워넣는다. SerializeToArray라는 함수를 사용했는데 헤더의 위치에서 헤더의 크기를 더한 만큼의 위치부터 데이터를 넣어야하니 header[1]의 위치부터 데이터를 넣어준다.
//ServerPacketHandler.h
class ServerPacketHandler
{
public:
static void HandlePacket(BYTE* buffer, int32 len);
static SendBufferRef MakeSendBuffer(Protocol::S_TEST& pkt);
};
//ServerPacketHandler.cpp
SendBufferRef ServerPacketHandler::MakeSendBuffer(Protocol::S_TEST& pkt)
{
return _MakeSendBuffer(pkt, S_TEST);
}
이제 패킷을 보내는 함수를 만들었는데 나중에 패킷이 여러개로 늘어날 경우 이런 함수를 계속 만들어서 사용하면 될 것이다. 이런 반복작업은 툴을 만들어서 자동화시키는 편이 좋다.
클라이언트도 작업을 해보자.
ClientPacketHandler.cpp
void ClientPacketHandler::Handle_S_TEST(BYTE* buffer, int32 len)
{
Protocol::S_TEST pkt;
ASSERT_CRASH(pkt.ParseFromArray(buffer + sizeof(PacketHeader), len - sizeof(PacketHeader)));
cout << pkt.id() << ' ' << pkt.hp() << ' ' << pkt.attack() << endl;
cout << "Buff Size : " << pkt.buffs_size() << endl;
for (auto& buf : pkt.buffs())
{
cout << buf.buffid() << ' ' << buf.remaintime() << endl;
cout << "Victims : " << buf.victims_size() << endl;
for (auto& victim : buf.victims())
{
cout << victim << ' ';
}
cout << endl;
}
}
pkt으로 파싱만 하면 되는데 buffer에는 헤더까지 같이 들어있으니 그 부분을 제외한 나머지만 파싱해주면 된다.
오늘 한 부분에서 아쉬운 점이 있다면 패킷이 추가될 때마다 함수를 일일히 추가해 줘야 한다는 것인데 다음에는 이것을 자동화 하는 방법을 알아보도록 할 것이다.
'게임서버 강의 > 패킷 직렬화' 카테고리의 다른 글
10. 패킷 자동화 #2 (1) | 2025.07.19 |
---|---|
9. 패킷 자동화 #1 (0) | 2025.07.18 |
7. Protobuf#1 (2) | 2025.07.15 |
6. 패킷 직렬화 #3 (2) | 2025.07.05 |
5. 패킷 직렬화 #2 (0) | 2025.07.04 |