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

6. 패킷 직렬화 #3

광란의슈가슈가룬 2025. 7. 5. 19:01

이번에는 보내는 쪽에서 바로 버퍼에 입력하도록 수정할 것이다.

 

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;
	}
}

이렇게 사용한다.

실행화면

이렇게 임시객체를 만들지 않고 바로 버퍼에 입력하는 방법을 알아보았다. 언뜻 보면 이 방법이 비용도 적게 들고 효율적인 것 같지만 프로그래머 입장에서 보면 코드가 지저분하니 유지보수가 더 힘들 수 밖에 없다. 그렇기 때문에 여러 방법들을 알고 장단점을 파악하여 적절하게 사용하는 것이 중요하다.


임시객체를 사용하면 코드를 작성하기 편하지만 복사비용이 발생하고 지금처럼 버퍼에 바로 밀어넣는 방식을 사용한다면 코드는 어쩔 수 없이 지저분해진다.