KoreanFoodie's Study
[C++ 게임 서버] 5-1. BufferHelper 본문
[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 의 크기를 일일히 빼 주고 있는데, 이런 부분도 추후 깔끔하게 다듬을 것이다. 😉
'Game Dev > Game Server' 카테고리의 다른 글
[C++ 게임 서버] 5-3. Unicode (0) | 2023.12.15 |
---|---|
[C++ 게임 서버] 5-2. PacketHandler (0) | 2023.12.15 |
[C++ 게임 서버] 4-10. PacketSession (0) | 2023.12.14 |
[C++ 게임 서버] 4-9. SendBuffer Pooling (0) | 2023.12.14 |
[C++ 게임 서버] 4-8. SendBuffer (0) | 2023.12.13 |