지금까지는 버퍼에 데이터를 넣기 위해서 임시로 객체를 만든 다음에 데이터를 받은 후 버퍼에 데이터를 넣고 있다.
이 방법에 문제가 있는 것은 아니다. 코드를 작성하거나 가독성 측면에서는 오히려 이러한 방법이 효율적이라고 볼 수 있다. 하지만 당연하게도 임시 객체를 만들게 되면 불필요한 복사 비용이 들게 된다.
이번에는 그러한 과정을 건너뛰고 버퍼에 바로 데이터를 넣고 바로 읽어서 쓰는 방법에 대해 알아볼 것이다.
ClientPacketHandler.cpp
PKT_S_TEST* pkt = reinterpret_cast<PKT_S_TEST*>(buffer);
//PKT_S_TEST pkt;
//br >> pkt;
기존에 새로운 패킷을 만들어 버퍼에서 복사하던 것을 이렇게 버퍼를 포인터 형식으로 캐스팅해서 복사하지 않고 그냥 사용할 수 있도록 하였다.
포인터로 받았으니 전과 같이 사용하되 연산자만 ->로 바꿔주면 된다. 그럼 이제 가변길이 데이터를 처리해보자.
현재 패킷의 구조가
[ PKT_S_TEST ][ BuffListItem BuffListItem BuffListItem ]
이렇게 되어 있는데 뒤에 따라오는 가변데이터의 시작위치와 개수를 알기 때문에 굳이 하나하나 꺼내서 쓸 필요 없이 바로 접근해서 쓸 수 있다. 아래는 이를 도와줄 탬플릿 클래스이다.
ClientPacketHandler.h
template<typename T>
class PacketList
{
public:
PacketList() : _data(nullptr), _count(0) { }
PacketList(T* data, uint15 count) : _data(data), _count(count) { }
T& operator[](uint16 index)
{
ASSERT_CRASH(index < _count);
return _data[index];
}
uint16 Count() { return _count; }
private:
T* _data;
uint16 _count;
};
이렇게 데이터와 개수만 알면 배열처럼 접근해서 쓸 수 있게 만들었다.
아래쪽의 BuffsList가 사용 예시이다.
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;
bool Validate()
{
uint32 size = 0;
size += sizeof(PKT_S_TEST);
if (packetSize < size)
return false;
size += buffsCount * sizeof(BuffsListItem);
// 계산한 크기가 저장된 패킷 크기랑 다르면 문제가 있음
if (size != packetSize)
return false;
// 버프 오프셋(시작위치)부터 버프의 개수만큼 셌는데 패킷 크기를 넘어가면 문제 있음
if (buffsOffset + buffsCount * sizeof(BuffsListItem) > packetSize)
return false;
return true;
}
using BuffsList = PacketList<PKT_S_TEST::BuffsListItem>;
BuffsList GetBuffsList()
{
// 바이트로 선언하여 덧셈 원활하게 함
BYTE* data = reinterpret_cast<BYTE*>(this);
data += buffsOffset;
return BuffsList(reinterpret_cast<PKT_S_TEST::BuffsListItem*>(data), buffsCount);
}
};
이렇게 되면 가변데이터가 수십 개, 수백 개라도 만들어지는 변수는 T타입의 포인터(위에서는 BuffsListItem)하나와 uint16 변수밖에 없다.
vector<PKT_S_TEST::BuffsListItem> buffs;
buffs.resize(pkt->buffsCount);
for (int i = 0; i < pkt->buffsCount; i++)
{
br >> buffs[i]; // pragma pack(1)했으므로 문제 없음
}
cout << "BuffCount : " << pkt->buffsCount << endl;
for (int32 i = 0; i < pkt->buffsCount; i++)
{
cout << "BuffInfo : " << buffs[i].buffId << ' ' << buffs[i].remainTime << endl;
}
기존에는 이렇게 데이터를 하나씩 꺼내서 사용했지만 바꾼 코드에서는
PKT_S_TEST::BuffsList buffs = pkt->GetBuffsList();
cout << "BuffCount : " << buffs.Count() << endl;
for (int32 i = 0; i < pkt->buffsCount; i++)
{
cout << "BuffInfo : " << buffs[i].buffId << ' ' << buffs[i].remainTime << endl;
}
이렇게 바로 사용할 수 있다.
이 방법은 데이터를 사용하기 위해 불필요한 임시 객체를 만들어 복사를 하지 않고 바로 사용할 수 있다는 점에서 굉장히 중요하다고 볼 수 있다.
위 코드를 보면 뭔가 부족한 부분이 있다. 이전과 같이 벡터를 사용하게 되면
for (auto it = buffs.begin(); it != buffs.end(); it++)
{
cout << "BuffInfo : " << it->buffId << ' ' << it->remainTime << endl;
}
for (auto& buff : buffs)
{
cout << "BuffInfo : " << buff.buffId << ' ' << buff.remainTime << endl;
}
이렇게 다양하게 사용할 수 있는데 위 코드에서는 아직 불가능하다. 해결법은 위 연산자들에 대해서 탬플릿으로 정의하는 것이다.
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;
};
이렇게 정의해주면 아래의 코드 전부 작동한다.
PKT_S_TEST::BuffsList buffs = pkt->GetBuffsList();
cout << "BuffCount : " << buffs.Count() << endl;
for (int32 i = 0; i < pkt->buffsCount; i++)
{
cout << "BuffInfo : " << buffs[i].buffId << ' ' << buffs[i].remainTime << endl;
}
for (auto it = buffs.begin(); it != buffs.end(); it++)
{
cout << "BuffInfo : " << it->buffId << ' ' << it->remainTime << endl;
}
for (auto& buff : buffs)
{
cout << "BuffInfo : " << buff.buffId << ' ' << buff.remainTime << endl;
}
이번에는 Read하는 부분만 살펴봤는데 다음에는 Write 할 때에도 임시객체를 만들지 않고 버퍼에 바로 기입하는 방법을 알아볼 것이다.
'게임서버 강의 > 패킷 직렬화' 카테고리의 다른 글
7. Protobuf#1 (2) | 2025.07.15 |
---|---|
6. 패킷 직렬화 #3 (2) | 2025.07.05 |
4. 패킷 직렬화 #1 (0) | 2025.05.29 |
3. Unicode (0) | 2025.05.28 |
2. Packet Handler (0) | 2025.05.26 |