KoreanFoodie's Study

[C++ 게임 서버] 5-1. BufferHelper 본문

Game Dev/Game Server

[C++ 게임 서버] 5-1. BufferHelper

GoldGiver 2023. 12. 14. 22:42

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

[C++ 게임 서버] 5-1. BufferHelper

핵심 :

1. Buffer 를 쉽게 읽고 쓸 수 있게 해 주는 Buffer Helper 클래스를 만들어 보자.

2. '>>' , '<<' 를 잘 오버로딩하면, cout, cin 처럼 버퍼를 쉽게 읽고 쓸 수 있다! 

이전에 우리는 버퍼에 우리가 원하는 데이터를 패킷으로 만들어 주고 받았었다. 그런데 버퍼를 만드는 코드는 아무래도  API 처럼 쉽게 재활용이 가능하면 좋으므로, 이를 돕기 위한 Helper 클래스를 이번 글에서 만들어 볼 것이다.

먼저, 버퍼를 읽는 BufferReader 부터 살펴보자.

BufferReader.h

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

읽을 대상인 _buffer 와 _size, 읽는 위치인 _pos 를 선언해 놓고, Peek/Read 함수로 버퍼에 접근할 수 있을 것이다.

사실 유의해서 볼 부분은 '>>' 를 오버로딩한 부분이다. 현재 버퍼에서 읽을 부분을 적절한 타입으로 캐스팅해 읽어낸 후, _pos 를 이동시키면, 연속해서 '>>' 연산을 수행할 수 있다. 실제 예시는 아래 부분에 DummyClient 클래스를 통해 확인할 수 있을 것이다 😄

 

BufferReader.cpp

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

나머지 Peek 과 Read 에 대한 연산은 비교적 간단히 구현되어 있다.

 

이제 Buffer 를 만들어 주는, BufferWriter 클래스를 살펴보자. 전반적인 구성은 BufferReader 와 비슷할 것이다.

BufferWriter.h

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

	BYTE*			Buffer() { return _buffer; }
	uint32			Size() { return _size; }
	uint32			WriteSize() { 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>
BufferWriter& BufferWriter::operator<<(T&& src)
{
	*reinterpret_cast<T*>(&_buffer[_pos]) = std::move(src);
	_pos += sizeof(T);
	return *this;
}

일단 눈여겨볼 부분은 Reserve 와 '<<' 오버로딩이다.

먼저 Reserve 의 경우, 알다시피 T 타입의 크기만큼의 영역을 미리 확보해 주는 녀석이다. '<<' 오버로딩의 경우 상수 좌측값 레퍼런스와 우측값 형식이 둘 다 존재하는데, 우측값인 경우에는 std::move 를 사용해주고 있다.

나머지 구현은 아래와 같이 간단하게 표현된다 :

BufferWriter.cpp

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

 

그럼 위 Helper 클래스들을 실제로 어떻게 사용할까? 먼저, 패킷을 만드는 GameServer 에서의 코드를 보자.

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)100 << (uint16)10;
    bw.Write(sendData, sizeof(sendData));

    header->size = bw.WriteSize();
    header->id = 1; // 1 : Test Msg

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

    GSessionManager.Broadcast(sendBuffer);

    this_thread::sleep_for(250ms);
}

먼저, BufferWriter 를 만들고, '<<' 를 이용해 id, 체력, 공격력을 넣어준다. 그 후, sendData 에 넣었던 데이터를 Write 해 주고, 패킷 헤더까지 채워서 패킷을 보내게 된다.

 

그럼 이제 DummyClient 에서는 BufferReader 로 이것을 어떻게 읽는지 보자.

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 << " ATT : " << attack << endl;

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

    return len;
}

패킷을 받으면, 위의 OnRecvPacket 함수가 호출되는데, Writer 와 비슷하게 '>>' 를 사용하고 있음을 알 수 있다.

여기서 br.Read 를 할 때 PacketHeader 의 사이즈와 각 id, hp, attack 의 크기를 일일히 빼 주고 있는데, 이런 부분도 추후 깔끔하게 다듬을 것이다. 😉

Comments