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

1. Buffer Helpers

광란의슈가슈가룬 2025. 5. 24. 19:03

네트워크 라이브러리가 얼추 완성 되었다. 네트워크 라이브러리는 사실 한 번 만들어 놓으면 수정할 일이 드물다.

실제로도 대부분의 코드 작업들은 패킷에 관련된 부분에서 다 일어날 것이다.

 

버퍼를 통해 데이터를 보낼 때 헤더에 정보를 기입하고 헤더 크기를 제외한 만큼의 데이터를 memcpy 해서 보내는 과정은 굉장히 복잡하고 실수가 생길 수 있다. 그래서 이를 도와줄 버퍼 헬퍼를 만들어 보자.

BufferReader.h

#pragma once

//----------------
//	BufferReader
//----------------

class BufferReader
{
public:
	BufferReader();
	BufferReader(BYTE* buffer, uint32 size, uint32 pos = 0);
	~BufferReader();

	BYTE*		Buffer() { return _buffer; };
	uint32		Size() { return _size; };
	uint32		ReadSize() { return _pos; };
	uint32		FreeSize() { return _size - _pos; };

	template<typename T>
	bool		peek(T* dest) { return Peek(dest, sizeof(T)); };
	bool		Peek(void* dest, uint32 len);

	template<typename T>
	bool		Read(T* dest) { return Read(dest, sizeof(T)); };
	bool		Read(void* dest, uint32 len);

	template<typename T>
	BufferReader& operator>>(OUT T& dest);

private:
	BYTE*		_buffer = nullptr;
	uint32		_size = 0;
	uint32		_pos = 0;
};

template<typename T>
inline BufferReader& BufferReader::operator>>(OUT T& dest)
{
	dest = *reinterpret_cast<T*>(&_buffer[_pos]);
	_pos += sizeof(T);
	return *this;
}

BufferReader.cpp

#include "pch.h"
#include "BufferReader.h"

//----------------
//	BufferReader
//----------------

BufferReader::BufferReader()
{
}

BufferReader::BufferReader(BYTE* buffer, uint32 size, uint32 pos)
	: _buffer(buffer), _size(size), _pos(pos)
{
}

BufferReader::~BufferReader()
{
}

bool BufferReader::Peek(void* dest, uint32 len)
{
	if (FreeSize() < len)
		return false;

	::memcpy(dest, &_buffer[_pos], len);
	return true;
}

bool BufferReader::Read(void* dest, uint32 len)
{
	if (Peek(dest, len) == false)
		return false;

	_pos += len;
	return true;
}

BufferReader는 말 그대로 버퍼에 저장된 데이터를 읽는 것이다. peek로 값을 확인할 수 있고 read로 값을 읽어올 수 있다. 두 개의 차이는 pos를 옮기냐 아니냐의 차이다.

 

BufferWriter.h

#pragma once

//----------------
//	BufferWriter
//----------------

class BufferWriter
{
public:
	BufferWriter();
	BufferWriter(BYTE* buffer, uint32 size, uint32 pos = 0);
	~BufferWriter();

	BYTE*		Buffer() { return _buffer; };
	uint32		Size() { return _size; };
	uint32		ReadSize() { return _pos; };
	uint32		FreeSize() { return _size - _pos; };

	template<typename T>
	bool		Write(T* src) { return Write(src, sizeof(T)); };
	bool		Write(void* src, uint32 len);

	template<typename T>
	T*			Reserve();

	template<typename T>
	BufferWriter& operator<<(const T& src);
	
	template<typename T>
	BufferWriter& operator<<(T&& src);

private:
	BYTE*		_buffer = nullptr;
	uint32		_size = 0;
	uint32		_pos = 0;
};

template<typename T>
T* BufferWriter::Reserve()
{
	if (FreeSize() < sizeof(T))
		return nullptr;

	T* ret = reinterpret_cast<T*>(&_buffer[_pos]);
	_pos += sizeof(T);

	return ret;
}

template<typename T>
BufferWriter& BufferWriter::operator<<(const T& src)
{
	*reinterpret_cast<T*>(&_buffer[_pos]) = src;
	_pos += sizeof(T);
	return *this;
}

template<typename T>
inline BufferWriter& BufferWriter::operator<<(T&& src)
{
	*reinterpret_cast<T*>(&_buffer[_pos]) = std::move(src);
	_pos += sizeof(T);
	return *this;
}

BufferWriter.cpp

#include "pch.h"
#include "BufferWriter.h"

//----------------
//	BufferWriter
//----------------

BufferWriter::BufferWriter()
{
}

BufferWriter::BufferWriter(BYTE* buffer, uint32 size, uint32 pos)
	:_buffer(buffer), _size(size), _pos(pos)
{
}

BufferWriter::~BufferWriter()
{
}

bool BufferWriter::Write(void* src, uint32 len)
{
	if (FreeSize() < len)
		return false;

	::memcpy(&_buffer[_pos], src, len);
	_pos += len;
	return true;
}

BufferWriter는 버퍼에 데이터를 쓰는 것으로 오른값 왼값 모두 쓸 수 있도록 연산자 오버로딩을 해주었다. Reverse는 버퍼에 데이터를 넣다가 앞으로 돌아와야 될 일이 생길 수 있는데 그때 사용한다. 예를 들어 버퍼를 채울 때 헤더 부분은 잠시 비워두고 데이터 부분을 먼저 기입한다고 하면 헤더 부분으로 돌아올 수 있도록 그 위치의 주소를 리턴하고 pos는 그만큼 뒤로 민다.

 

그럼 어떻게 사용하는지 한 번 보자.

 

먼저 서버의 코드이다.

	char sendData[] = "Hello World";
	while (true)
	{
		SendBufferRef sendBuffer = GSendBufferManager->Open(4096);

		BYTE* buffer = sendBuffer->Buffer();
		((PacketHeader*)buffer)->size = (sizeof(sendData) + sizeof(PacketHeader));
		((PacketHeader*)buffer)->id = 1;
		::memcpy(&buffer[4], sendData, sizeof(sendData));
		sendBuffer->Close((sizeof(sendData) + sizeof(PacketHeader)));
	
		GSessionManager.Broadcast(sendBuffer);

		std::this_thread::sleep_for(250ms);
	}

이런 식으로 버퍼에 데이터를 밀어넣었는데

char sendData[] = "Hello World";
while (true)
{
	SendBufferRef sendBuffer = GSendBufferManager->Open(4096);

	BufferWriter bw(sendBuffer->Buffer(), 4096);

	PacketHeader* header = bw.Reserve<PacketHeader>();

	// id(uint64), 체력(uint32), 공격력(uint16)
	bw << (uint64)1001 << (uint32)150 << (uint16)10;
	bw.Write(sendData, sizeof(sendData));

	header->size = bw.WriteSize();
	header->id = 1;

	sendBuffer->Close(bw.WriteSize());

	GSessionManager.Broadcast(sendBuffer);

	std::this_thread::sleep_for(250ms);
}

이렇게 바뀌었다. Reserve를 통해서 Header를 넣을 위치를 따로 빼서 관리하고 << 연산자를 통해서 데이터를 밀어넣을 수도 있다. 가변길이 데이터의 경우 Write를 사용해서 데이터를 넣으면 된다.

 

그럼 클라이언트의 코드를 한 번 보자.

virtual int32 OnRecvPacket(BYTE* buffer, int32 len) override
{
	PacketHeader header = *((PacketHeader*)buffer);
	cout << " Size : " << header.size << endl;

	char recvBuffer[4096];
	memcpy(recvBuffer, &buffer[4], header.size - sizeof(PacketHeader));
	cout << recvBuffer << endl;

	return len;
}

기존의 코드이다.

virtual int32 OnRecvPacket(BYTE* buffer, int32 len) override
{
	BufferReader br(buffer, len);

	PacketHeader header;
	br >> header;

	uint64 id;
	uint32 hp;
	uint16 attack;
	br >> id >> hp >> attack;

	cout << "ID: " << id
		<< " HP: " << hp
		<< " Attack: " << attack << endl;

	char recvBuffer[4096];
	br.Read(recvBuffer, header.size - sizeof(PacketHeader) - 8 - 4 - 2);
	cout << recvBuffer << endl;

	return len;
}

코드가 더욱 깔끔하게 변한 것을 볼 수 있다. Read로 가변길이 데이터를 읽을 때 현재는 어쩔 수 없이 무식하게 계산을 했지만 물론 이 방법은 좋은 방법이 아니다. 나중에는 가변길이의 길이도 함께 보내주어 코드를 수정할 것이다.

'게임서버 강의 > 패킷 직렬화' 카테고리의 다른 글

6. 패킷 직렬화 #3  (2) 2025.07.05
5. 패킷 직렬화 #2  (0) 2025.07.04
4. 패킷 직렬화 #1  (0) 2025.05.29
3. Unicode  (0) 2025.05.28
2. Packet Handler  (0) 2025.05.26