KoreanFoodie's Study

[C++ 게임 서버] 5-5. 패킷 직렬화 #2 본문

Game Dev/Game Server

[C++ 게임 서버] 5-5. 패킷 직렬화 #2

GoldGiver 2023. 12. 16. 01:34

Rookiss 님의 '[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버' 를 들으며 배운 내용을 정리하고 있습니다. 관심이 있으신 분은 꼭 한 번 들어보시기를 추천합니다!

[C++ 게임 서버] 5-5. 패킷 직렬화 #2

핵심 :

1. 가변 데이터를 처리할 수 있는 PacketList 를 만들면, 버퍼를 읽을 때 임시 객체를 생성하지 않고 캐스팅을 통해 데이터를 효과적으로 읽어들일 수 있다. 

자, 일단 저번 시간에는 가변 데이터 형식인 buffs 에 대해 패킷 직렬화를 수행했었다. 그런데 코드를 잘 보면, 조금 마음에 들지 않는 부분이 있다.

vector<PKT_S_TEST::BuffsListItem> buffs;

buffs.resize(pkt.buffsCount);
for (int32 i = 0; i < pkt.buffsCount; i++)
    br >> buffs[i];

바로 buffs 데이터를 읽을 때 임시 객체가 생성되는 것인데... 생각해보면 굳이 그럴 필요가 있나 싶다. 🤔

이 부분을 최적화하기 위해, 이번 글에서는 일단 ClientPacketHandler 쪽에서 buffs 를 읽는 부분을 수정할 것이다.

 

먼저, ClientPacketHandler 의 헤더 쪽에 리스트를 다룰 수 있는 템플릿 클래스를 하나 만들어 준다.

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

	// ranged-base 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;
};

내용을 보면, 템플릿을 사용하고 있긴 하지만 실상 data 와 count 만 주의 깊게 보면 된다. 즉 _data 에는 읽으려는 버퍼의 시작점이 들어가고, _count 에는 버퍼에 들어간 가변 길이 데이터에서의 요소 갯수가 들어갈 것이다.

참, ranged-base for 를 지원하기 위해 PacketList 를 위한 PacketIterator 도 아래와 같이 만들어 주자 :

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

 

그럼 PKT_S_TEST 에서, buffs 를 처리했던 부분을 다음과 같이 조금 수정해 준다 :

#pragma pack(1)

// [ PKT_S_TEST ][BuffsListItem BuffsListItem BuffsListItem]
struct PKT_S_TEST
{
	struct BuffsListItem
	{
		uint64 buffId;
		float remainTime;
	};

	/** ... */

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

	//vector<BuffData> buffs;
	//wstring name;
};
#pragma pack()

이제 BuffList 타입은, PacketList 의 PKT_S_TEST::BuffsListItem 타입에 대한 특수화 버전으로 나가게 된다.

 

그럼 이제 Handle_S_TEST 는 다음과 같이 변하게 되는데 :

// [ PKT_S_TEST ][BuffsListItem BuffsListItem BuffsListItem]
void ClientPacketHandler::Handle_S_TEST(BYTE* buffer, int32 len)
{
	BufferReader br(buffer, len);

	PKT_S_TEST* pkt = reinterpret_cast<PKT_S_TEST*>(buffer);

	if (pkt->Validate() == false)
		return;

	//cout << "ID: " << id << " HP : " << hp << " ATT : " << attack << endl;

	// 예전 방식. 임시 벡터가 필요했었다.
	vector<PKT_S_TEST::BuffsListItem> buffs;
    
	// 지금 방식. 이제 pkt 의 주소를 바로 읽어 캐스팅해서 사용하므로, 임시 객체가 필요 없다.
	PKT_S_TEST::BuffsList buffs = pkt->GetBuffsList();
	
	cout << "BufCount : " << buffs.Count() << endl;
	for (int32 i = 0; i < buffs.Count(); i++)
		cout << "BufInfo : " << buffs[i].buffId << " " << buffs[i].remainTime << endl;

	for (auto it = buffs.begin(); it != buffs.end(); ++it)
		cout << "BufInfo : " << it->buffId << " " << it->remainTime << endl;

	for (auto& buff : buffs)
		cout << "BufInfo : " << buff.buffId << " " << buff.remainTime << endl;
}

무엇이 달라진 것일까? 바로 buffs 를 읽고 저장하는 방식이 바뀌었다! 이제는 buffs 를 위한 벡터를 따로 임시로 만들어 줄 필요 없이, 패킷의 버퍼를 그대로 읽어서 캐스팅만 적절히 취해줌으로써 Count 를 이용한 for loop 과 range-based for loop 까지 사용할 수 있게 된 것을 확인할 수 있다 😄

Comments