게임서버 강의/패킷 직렬화

5. 패킷 직렬화 #2

광란의슈가슈가룬 2025. 7. 4. 20:14

지금까지는 버퍼에 데이터를 넣기 위해서 임시로 객체를 만든 다음에 데이터를 받은 후 버퍼에 데이터를 넣고 있다.

이 방법에 문제가 있는 것은 아니다. 코드를 작성하거나 가독성 측면에서는 오히려 이러한 방법이 효율적이라고 볼 수 있다. 하지만 당연하게도 임시 객체를 만들게 되면 불필요한 복사 비용이 들게 된다.

 

이번에는 그러한 과정을 건너뛰고 버퍼에 바로 데이터를 넣고 바로 읽어서 쓰는 방법에 대해 알아볼 것이다.

 

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