KoreanFoodie's Study

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

Game Dev/Game Server

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

GoldGiver 2023. 12. 15. 16:41

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

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

핵심 :

1. 패킷 직렬화란, 포인터 및 가변 데이터 정보를 패킷으로 주고 받을 때 이를 안전하게 저장하고 불러올 수 있도록 파싱하는 기법이다.

2. 가변 데이터의 경우, Offset 과 Count 를 이용해 직렬화를 쉽게 진행할 수 있다.

3. 직렬화된 데이터는 xml 또는 json 포맷으로 세이브 & 로드를 하며 관리하게 된다. xml 은 가독성이 좋을 수 있으나 복잡하며, json 은 형태가 간단하며 파싱 속도가 빠르다.

이번 글에서는 패킷 직렬화에 대해 알아보자. 영어로는 사실 Serialization 인데, 직렬화라고 하니 살짝 어색하게 느껴지기는 한다. 😅

언리얼에서도 Serialization 이라는 용어를 사용하는데, 그 때는 데이터를 안전하고 완전하게 보관하기 위해 Serialization 을 별도로 제공한다는 설명이 있다.

그리고 패킷 직렬화도, 개념적으로는 패킷의 정보를 안전하고 완전하게 저장하고 불러오기 위해 사용한다. 예를 들어, 아래 예시와 같은 정보를 주고 받아야 한다고 가정해 보자.

struct PKT_S_PLAYER
{
	uint32 id;
	uint16 level;
	Player* data;
	vector<BuffData> buffs;
};

이때, 만약 패킷의 정보를 json 이나 xml 의 형태로 저장한 후, 다음에 게임을 켰을 때 그대로 불러와 플레이어와 관련된 정보를 세팅한다면 무슨 일이 일어날까?

일단, id 와 level 은 그렇다 쳐도, data 에 해당하는 녀석은 메모리 주소이므로, 크래시가 발생하게 될 것이다. 😂 또한, 가변 데이터인 buffs 에 대해서도 어떻게 저장하면 좋을지에 대한 아이디어가 필요하다.

이런 고민을 해결하기 위해, 직렬화(Serialization) 과정을 거쳐, 데이터가 손실이 되지 않게 저장을 해 주어야 한다.

개념적으로는, 아래의 그림처럼 표현을 할 수 있을 것이다 :

즉, 포인터인 data 나, 가변 데이터인 buffs 같은 녀석은 별도의 영역으로 빼서 저장을 안전하게 해 줄 것이다. 바로 예시를 보자. 이번 글에서는 일단 스트링인 name 은 빼고, 가변 데이터를 어떻게 처리할지만 다루도록 한다 😀

struct PKT_S_TEST
{
	struct BuffsListItem
	{
		uint64 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);
		size += buffsCount * sizeof(BuffsListItem);
		if (size != packetSize)
			return false;

		if (buffsOffset + buffsCount * sizeof(BuffsListItem) > packetSize)
			return false;

		return true;
	}
	//vector<BuffData> buffs;
	//wstring name;
};

일단 가변 데이터인 vector<BuffData> 를 어떻게 처리해야 하는지부터 보자.

여기서는 buffsOffset 과 buffsCount 를 각각 두었다. 그리고 Validate 함수로 해당 패킷의 유효성을 검증하는데...

잘 보면, buffsCount 와 buffsOffset 을 이용해 패킷의 사이즈를 체크함으로써 패킷의 유효성을 간단히 검증하고 있음을 알 수 있다.

즉, PKT_S_TEST 패킷이 담긴 버퍼는 실제로 아래와 같이 생겨먹었을 것이다 :

[ PKT_S_TEST ][BuffsListItem BuffsListItem BuffsListItem]

 

아, 그리고 한 가지 더 추가할 부분이 있다. 예를 들어, 아래 패킷의 사이즈는 몇 바이트일까?

struct PKT_S_TEST
{
	uint64 id; // 8
	uint32 hp; // 4
	uint16 attack; // 2
};

산술적으로 합하면 8 + 4 + 2 = 14 바이트가 될 것 같지만... 실제로 어셈블리를 확인해 보면, 24 바이트가 되는 것을 알 수 있다! 😮 그 이유는, 현재 프로젝트가 64 비트 환경에서 돌아가다 보니, 기본 정렬단위가 8 바이트이기 때문에 padding 이 추가되기 때문이다.

이것을 막으려면, #pragrma pack(1) 이라는, 1 바이트씩 읽을 것이라는 매크로를 붙여 주어야 한다!

#pragma pack(1)

struct PKT_S_TEST
{
	/** 같은 내용 */
};

#pragma pack()

따라서 위처럼 매크로로 감싸주면 되겠다. 😉

 

그럼 이제 서버에서도 내용을 조금 바꿔주면 된다. ServerPacketHandler 에서 buffs 를 보내는 방식은 아래와 같이 수정될 것이다 :

struct ListHeader
{
	uint16 offset;
	uint16 count;
};

// 가변 데이터
ListHeader* buffsHeader = bw.Reserve<ListHeader>();

buffsHeader->offset = bw.WriteSize();
buffsHeader->count = buffs.size();

for (BuffData& buff : buffs)
	bw << buff.buffId << buff.remainTime;

 

그럼 ClientPacketHandler 쪽에서는, S_TEST 패킷을 아래와 같이 핸들링하면 된다! 😁

void ClientPacketHandler::Handle_S_TEST(BYTE* buffer, int32 len)
{
	BufferReader br(buffer, len);

	if (len < sizeof(PKT_S_TEST))
		return;

	PKT_S_TEST pkt;
	br >> pkt;

	if (pkt.Validate() == false)
		return;

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

	vector<PKT_S_TEST::BuffsListItem> buffs;

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

	cout << "BufCount : " << pkt.buffsCount << endl;
	for (int32 i = 0; i < pkt.buffsCount; i++)
	{
		cout << "BufInfo : " << buffs[i].buffId << " " << buffs[i].remainTime << endl;
	}
}

 

결국 우리의 목적은 패킷을 안전하게 저장하려는 것인데, xml 로 저장한다면 아래와 같을 것이다(json 으로 저장해도 무방하다. 상황에 따라 선택).

<?xml version="1.0" encoding="utf-8"?>
<PDL>
	<Packet name="S_TEST" desc="테스트 용도">
		<Field name="id" type="uint64" desc=""/>
		<Field name="hp" type="uint32" desc=""/>
		<Field name="attack" type="uint16" desc=""/>
		<List name="buffs" desc="">
			<Field name="buffId" type="uint64" desc=""/>
			<Field name="remainTime" type="float" desc=""/>
		</List>
	</Packet>
</PDL>
Comments