6. 패킷 직렬화 #3
이번에는 보내는 쪽에서 바로 버퍼에 입력하도록 수정할 것이다.
ServerPacketHandler.h
#pragma once
#include "BufferReader.h"
#include "BufferWriter.h"
enum
{
S_TEST = 1
};
template<typename T, typename C>
class PacketIterator
{
public:
PacketIterator(C& container, uint16 index) : _container(container), _index(index) {}
bool operator!=(const PacketIterator& other) const { return _index != other._index; }
const T& operator*() const { return _container[_index]; }
T& operator*() { return _container[_index]; }
T* operator->() { return &_container[_index]; }
PacketIterator& operator++() { _index++; return *this; }
PacketIterator operator++(int32) { PacketIterator ret = *this; ++_index; return ret; }
private:
C& _container;
uint16 _index;
};
template<typename T>
class PacketList
{
public:
PacketList() : _data(nullptr), _count(0) {}
PacketList(T* data, uint16 count) : _data(data), _count(count) {}
T& operator[](uint16 index)
{
ASSERT_CRASH(index < _count);
return _data[index];
}
uint16 Count() { return _count; }
// range-based for 지원
PacketIterator<T, PacketList<T>> begin() { return PacketIterator<T, PacketList <T>>(*this, 0); }
PacketIterator<T, PacketList<T>> end() { return PacketIterator<T, PacketList <T>>(*this, _count); }
private:
T* _data;
uint16 _count;
};
class ServerPacketHandler
{
public:
static void HandlePacket(BYTE* buffer, int32 len);
};
#pragma pack(1)
// [ PKT_S_TEST ][ BuffListItem BuffListItem BuffListItem ]
struct PKT_S_TEST
{
struct BuffsListItem
{
uint32 buffId;
float remainTime;
};
uint16 packetSize; // 공용 헤더
uint16 packetId; // 공용 헤더
uint64 id; // 8
uint32 hp; // 4
uint16 attack; // 2
uint16 buffsOffset;
uint16 buffsCount;
};
// [ PKT_S_TEST ][ BuffListItem BuffListItem BuffListItem ]
class PKT_S_TEST_WRITE
{
public:
using BuffsListItem = PKT_S_TEST::BuffsListItem;
using BuffsList = PacketList<PKT_S_TEST::BuffsListItem>;
PKT_S_TEST_WRITE(uint64 id, uint32 hp, uint16 attack)
{
_sendBuffer = GSendBufferManager->Open(4096);
_bw = BufferWriter(_sendBuffer->Buffer(), _sendBuffer->AllocSize());
_pkt = _bw.Reserve<PKT_S_TEST>();
_pkt->packetSize = 0; // To Fill
_pkt->packetId = S_TEST;
_pkt->id = id;
_pkt->hp = hp;
_pkt->attack = attack;
_pkt->buffsOffset = 0; // To Fill
_pkt->buffsCount = 0; // To Fill
}
// 버프를 하나씩 추가하다가 다른 패킷이 추가되거나 하면 문제가 생기므로
// 버프의 개수를 먼저 받은 다음 메모리를 할당하고 그 다음 채움
BuffsList ReserveBuffsList(uint16 buffCount)
{
BuffsListItem* firstBuffsListItem = _bw.Reserve<BuffsListItem>(buffCount);
_pkt->buffsOffset = (uint64)firstBuffsListItem - (uint64)_pkt;
_pkt->buffsCount = buffCount;
return BuffsList(firstBuffsListItem, buffCount);
}
private:
PKT_S_TEST* _pkt = nullptr;
SendBufferRef _sendBuffer;
BufferWriter _bw;
};
#pragma pack()
PKT_S_TEST_WRITE라는 클래스를 하나 더 만들어 필요한 PKT_S_TEST라는 패킷을 보낼 때 필요한 변수, 함수들을 들고 있도록 하였다. BufferWriter의 Reserve도 정수를 받아 그만큼 할당해주도록 바꾸었다.
그렇게 되면
vector<BuffData> buffs{ BuffData{100, 1.3f}, BuffData{130, 1.8f}, BuffData{200, 2.3f} };
이렇게 임시 객체를 만들어서 데이터를 넣었는데 이제는
PKT_S_TEST_WRITE pktWriter(1001, 100, 10);
PKT_S_TEST_WRITE::BuffsList buffList = pktWriter.ReserveBuffsList(3);
buffList[0] = { 100, 1.3f };
buffList[1] = { 130, 1.8f };
buffList[2] = { 200, 2.3f };
이렇게 바로 넣을 수 있게 되었다.
위 코드에서 아직 채우지 않은 데이터가 있는데 바로 packetSize이다. 이제 진짜 데이터 입력이 끝났으니 sendBuffer를 닫으면서 채워넣으면 된다. PKT_S_TEST_WRITE 클래스에 마지막으로 함수 하나를 추가해주자.
SendBufferRef CloseAndReturn()
{
// 패킷 사이즈 계산
_pkt->packetSize = _bw.WriteSize();
_sendBuffer->Close(_bw.WriteSize());
return _sendBuffer;
}
Main.cpp
while (true)
{
PKT_S_TEST_WRITE pktWriter(1001, 100, 10);
PKT_S_TEST_WRITE::BuffsList buffList = pktWriter.ReserveBuffsList(3);
buffList[0] = { 100, 1.3f };
buffList[1] = { 130, 1.8f };
buffList[2] = { 200, 2.3f };
SendBufferRef sendBuffer = pktWriter.CloseAndReturn();
GSessionManager.Broadcast(sendBuffer);
std::this_thread::sleep_for(250ms);
}
메인 서버에서는 이런식으로 사용된다. 한 가지 아쉬운점이 있다면 가변길이 데이터의 크기를 처음에 정해줘야 한다는 것이다. 만약 리스트 안에 리스트가 또 있다면 어떻게 될까?
그냥 비슷한 작업을 또 해주면 된다.
#pragma pack(1)
// [ PKT_S_TEST ][ BuffListItem BuffListItem BuffListItem ][ victim victim ][ victim victim ]
struct PKT_S_TEST
{
struct BuffsListItem
{
uint32 buffId;
float remainTime;
// Victims List
uint16 victimsOffset;
uint16 victimsCount;
};
uint16 packetSize; // 공용 헤더
uint16 packetId; // 공용 헤더
uint64 id; // 8
uint32 hp; // 4
uint16 attack; // 2
uint16 buffsOffset;
uint16 buffsCount;
};
// [ PKT_S_TEST ][ BuffListItem BuffListItem BuffListItem ]
class PKT_S_TEST_WRITE
{
public:
using BuffsListItem = PKT_S_TEST::BuffsListItem;
using BuffsList = PacketList<PKT_S_TEST::BuffsListItem>;
using BuffsVictimsList = PacketList<uint64>;
PKT_S_TEST_WRITE(uint64 id, uint32 hp, uint16 attack)
{
_sendBuffer = GSendBufferManager->Open(4096);
_bw = BufferWriter(_sendBuffer->Buffer(), _sendBuffer->AllocSize());
_pkt = _bw.Reserve<PKT_S_TEST>();
_pkt->packetSize = 0; // To Fill
_pkt->packetId = S_TEST;
_pkt->id = id;
_pkt->hp = hp;
_pkt->attack = attack;
_pkt->buffsOffset = 0; // To Fill
_pkt->buffsCount = 0; // To Fill
}
// 버프를 하나씩 추가하다가 다른 패킷이 추가되거나 하면 문제가 생기므로
// 버프의 개수를 먼저 받은 다음 메모리를 할당하고 그 다음 채움
BuffsList ReserveBuffsList(uint16 buffCount)
{
BuffsListItem* firstBuffsListItem = _bw.Reserve<BuffsListItem>(buffCount);
_pkt->buffsOffset = (uint64)firstBuffsListItem - (uint64)_pkt;
_pkt->buffsCount = buffCount;
return BuffsList(firstBuffsListItem, buffCount);
}
BuffsVictimsList ReserveBuffsVictimsList(BuffsListItem* buffsItem, uint16 victimsCount)
{
uint64* firstVictimsListItem = _bw.Reserve<uint64>(victimsCount);
buffsItem->victimsOffset = (uint64)firstVictimsListItem - (uint64)_pkt;
buffsItem->victimsCount = victimsCount;
return BuffsVictimsList(firstVictimsListItem, victimsCount);
}
SendBufferRef CloseAndReturn()
{
// 패킷 사이즈 계산
_pkt->packetSize = _bw.WriteSize();
_sendBuffer->Close(_bw.WriteSize());
return _sendBuffer;
}
private:
PKT_S_TEST* _pkt = nullptr;
SendBufferRef _sendBuffer;
BufferWriter _bw;
};
#pragma pack()
리스트 안에 오프셋과 카운트를 넣고 Reserve함수를 하나 더 만들었다. 메인 서버에서는 이렇게 사용한다.
while (true)
{
PKT_S_TEST_WRITE pktWriter(1001, 100, 10);
PKT_S_TEST_WRITE::BuffsList buffList = pktWriter.ReserveBuffsList(3);
buffList[0] = { 100, 1.3f };
buffList[1] = { 130, 1.8f };
buffList[2] = { 200, 2.3f };
PKT_S_TEST_WRITE::BuffsVictimsList vic0 = pktWriter.ReserveBuffsVictimsList(&buffList[0], 2);
{
vic0[0] = 1000;
vic0[1] = 2000;
}
PKT_S_TEST_WRITE::BuffsVictimsList vic1 = pktWriter.ReserveBuffsVictimsList(&buffList[1], 1);
{
vic1[0] = 1000;
}
PKT_S_TEST_WRITE::BuffsVictimsList vic2 = pktWriter.ReserveBuffsVictimsList(&buffList[2], 3);
{
vic2[0] = 1000;
vic2[1] = 2000;
vic2[2] = 3000;
}
SendBufferRef sendBuffer = pktWriter.CloseAndReturn();
GSessionManager.Broadcast(sendBuffer);
std::this_thread::sleep_for(250ms);
}
문제는 없긴 하지만 딱 봐도 지저분하고 효율적이지 않아 보인다. 지금은 리스트가 2개만 중첩되어 있지만 만약에 더 많은 수의 리스트가 중첩되어 있다면 코드를 작성할 때 실수도 많아질 것이고 굉장히 힘들 것이다.
서버쪽 코드를 위와 같이 수정했으니 이에 맞춰 클라이언트 측 코드도 수정해보자.
#pragma pack(1)
// [ PKT_S_TEST ][ BuffListItem BuffListItem BuffListItem ]
struct PKT_S_TEST
{
struct BuffsListItem
{
uint32 buffId;
float remainTime;
// Victims List
uint16 victimsOffset;
uint16 victimsCount;
bool Validate(BYTE* packetStart, uint16 packetSize, OUT uint32& size)
{
if (victimsOffset + victimsCount * sizeof(uint64) > packetSize)
return false;
size += victimsCount * sizeof(uint64);
return true;
}
};
uint16 packetSize; // 공용 헤더
uint16 packetId; // 공용 헤더
uint64 id; // 8
uint32 hp; // 4
uint16 attack; // 2
uint16 buffsOffset;
uint16 buffsCount;
bool Validate()
{
uint32 size = 0;
size += sizeof(PKT_S_TEST);
if (packetSize < size)
return false;
// 버프 오프셋(시작위치)부터 버프의 개수만큼 셌는데 패킷 크기를 넘어가면 문제 있음
if (buffsOffset + buffsCount * sizeof(BuffsListItem) > packetSize)
return false;
size += buffsCount * sizeof(BuffsListItem);
BuffsList buffsList = GetBuffsList();
for (int32 i = 0; i < buffsList.Count(); i++)
{
if (buffsList[i].Validate((BYTE*)this, packetSize, OUT size) == false)
return false;
}
// 계산한 크기가 저장된 패킷 크기랑 다르면 문제가 있음
if (size != packetSize)
return false;
return true;
}
using BuffsList = PacketList<PKT_S_TEST::BuffsListItem>;
using BuffsVictimsList = PacketList<uint64>;
BuffsList GetBuffsList()
{
// 바이트로 선언하여 덧셈 원활하게 함
BYTE* data = reinterpret_cast<BYTE*>(this);
data += buffsOffset;
return BuffsList(reinterpret_cast<PKT_S_TEST::BuffsListItem*>(data), buffsCount);
}
BuffsVictimsList GetBuffsVictimsList(BuffsListItem* buffsItem)
{
BYTE* data = reinterpret_cast<BYTE*>(this);
data += buffsItem->victimsOffset;
return BuffsVictimsList(reinterpret_cast<uint64*>(data), buffsItem->victimsCount);
}
};
#pragma pack()
리스트 안에 오프셋과 카운트를 하나 더 두고 victim에 대한 Validate함수를 하나 더 만들어서 확인하도록 했다. 만약 리스트가 여러개가 중첩되어 나올 때에도 이렇게 Validate안에서 Validate를 호출하고, 그 안에서 또 Validate를 호출하는 식으로 만들 수 있다.
사용할 때에도 이전에 리스트를 사용할 때와 비슷하게
cout << "BuffCount : " << buffs.Count() << endl;
for (int32 i = 0; i < pkt->buffsCount; i++)
{
cout << "BuffInfo : " << buffs[i].buffId << ' ' << buffs[i].remainTime << endl;
PKT_S_TEST::BuffsVictimsList victims = pkt->GetBuffsVictimsList(&buffs[i]);
cout << "Victims Count : " << victims.Count() << endl;
for (int32 j = 0; j < victims.Count(); j++)
{
cout << "Victim : " << victims[i] << endl;
}
}
이렇게 사용한다.
이렇게 임시객체를 만들지 않고 바로 버퍼에 입력하는 방법을 알아보았다. 언뜻 보면 이 방법이 비용도 적게 들고 효율적인 것 같지만 프로그래머 입장에서 보면 코드가 지저분하니 유지보수가 더 힘들 수 밖에 없다. 그렇기 때문에 여러 방법들을 알고 장단점을 파악하여 적절하게 사용하는 것이 중요하다.
임시객체를 사용하면 코드를 작성하기 편하지만 복사비용이 발생하고 지금처럼 버퍼에 바로 밀어넣는 방식을 사용한다면 코드는 어쩔 수 없이 지저분해진다.