네트워크 라이브러리가 얼추 완성 되었다. 네트워크 라이브러리는 사실 한 번 만들어 놓으면 수정할 일이 드물다.
실제로도 대부분의 코드 작업들은 패킷에 관련된 부분에서 다 일어날 것이다.
버퍼를 통해 데이터를 보낼 때 헤더에 정보를 기입하고 헤더 크기를 제외한 만큼의 데이터를 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 |