KoreanFoodie's Study

[C++ 게임 서버] 4-7. RecvBuffer 본문

Game Dev/Game Server

[C++ 게임 서버] 4-7. RecvBuffer

GoldGiver 2023. 12. 12. 21:07

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

[C++ 게임 서버] 4-7. RecvBuffer

핵심 :

1. RecvBuffer 를 만들 때는, ReadPos / WritePos 와 관련한 처리를 어떻게 할 것인지가 중요하다.

2. 보통 BufferSize 보다 크게 메모리를 할당해, Clean 을 효율적으로 구현하는 방법을 많이 사용한다. 

이전에 Recv 는 Send 에 비해 비교적 구조가 간단하다고 말한 적이 있다. 왜냐면... Send 는 여러 군데에서 동시에 호출하고 난리를 치지만, Recv 는 결국 멀티 쓰레드 환경일지라도 동시에 받는 것이 아니라 순차적으로 받게 되어 있으므로(논리상), 상대적으로 얌전(?) 할 수 밖에 없는 것이다 😅

물론, Recv 의 경우에도 RecvBuffer 를 어떻게 만들어야 좋을지에 대해서는 고민이 약간 필요한데, 이 글에서는 RecvBuffer 를 아래와 같이 구현할 것이다 :

  1. 버퍼의 크기보다 훨씬 큰(e.g. 10 배) 영역을 잡고, Read/Write Pos 를 움직이도록 한다.
  2. 읽으려는 DataSize 가 0 이 되면(즉, Read/Write Pos 가 일치하면) 두 Pos 를 전부 시작점으로 옮기고, 만약 자투리(Buffer 사이즈보다 작은)가 남으면, 해당 DataSize 만큼을 앞 부분으로 옮기고, Write Pos 도 그에 맞게 옮긴다.

말로 하니까 더 두루뭉술해지는 것 같으니, 코드를 한 번 보자. 😂

// Session.h
				/* 수신 관련 */
RecvBuffer			_recvBuffer;

먼저, Session.h 에 RecvBuffer 클래스 타입의 _recvBuffer 를 선언할 것이다. 이 클래스는 다음과 같이 생겼는데...

 

RecvBuffer.h

class RecvBuffer
{
	enum { BUFFER_COUNT = 10 };

public:
	RecvBuffer(int32 bufferSize);
	~RecvBuffer();

	void			Clean();
	bool			OnRead(int32 numOfBytes);
	bool			OnWrite(int32 numOfBytes);

	BYTE*			ReadPos() { return &_buffer[_readPos]; }
	BYTE*			WritePos() { return &_buffer[_writePos]; }
	int32			DataSize() { return _writePos - _readPos; }
	int32			FreeSize() { return _capacity - _writePos; }

private:
	int32			_capacity = 0;
	int32			_bufferSize = 0;
	int32			_readPos = 0;
	int32			_writePos = 0;
	Vector<BYTE>	_buffer;
};

위에서 BUFFER_COUNT 는, 버퍼의 원래 크기에 곱하여 실제 버퍼의 크기를 할당하는 데 사용될 것이다.

OnRead, OnWrite, ReadPos, WritePos 등의 함수는 말 그대로 읽고 쓰는데 사용된다.

그렇다면 위에서 _capacity 와 _bufferSize 는 어떤 관계일까?

 

RecvBuffer.cpp

#include "pch.h"
#include "RecvBuffer.h"

RecvBuffer::RecvBuffer(int32 bufferSize) : _bufferSize(bufferSize)
{
	_capacity = bufferSize * BUFFER_COUNT;
	_buffer.resize(_capacity);
}

RecvBuffer::~RecvBuffer()
{
}

void RecvBuffer::Clean()
{
	int32 dataSize = DataSize();
	if (dataSize == 0)
	{
		// 딱 마침 읽기+쓰기 커서가 동일한 위치라면, 둘 다 리셋.
		_readPos = _writePos = 0;
	}
	else
	{
		// 여유 공간이 버퍼 1개 크기 미만이면, 데이터를 앞으로 땅긴다.
		if (FreeSize() < _bufferSize)
		{
			::memcpy(&_buffer[0], &_buffer[_readPos], dataSize);
			_readPos = 0;
			_writePos = dataSize;
		}
	}
}

bool RecvBuffer::OnRead(int32 numOfBytes)
{
	if (numOfBytes > DataSize())
		return false;

	_readPos += numOfBytes;
	return true;
}

bool RecvBuffer::OnWrite(int32 numOfBytes)
{
	if (numOfBytes > FreeSize())
		return false;

	_writePos += numOfBytes;
	return true;
}

_capacity 는 _bufferSize 에 BUFFER_COUNT 를 곱한 값으로, 실제 사용할 버퍼 자료구조의 최대 크기로 정한다. 이 안에서, _readPos 와 _writePos 가 움직이며 데이터를 읽을 것이다.

사실 제일 중요한 부분이 바로 Clean 함수인데... 이는 위에서 1, 2 로 나누어 설명한 부분에 대한 구현이다! 😉

 

이제 위의 OnRead 와 OnWrite 함수는 Session 함수에서 다음과 같이 쓰이게 된다 : 

void Session::ProcessRecv(int32 numOfBytes)
{
	_recvEvent.owner = nullptr; // RELEASE_REF

	if (numOfBytes == 0)
	{
		Disconnect(L"Recv 0");
		return;
	}

	if (_recvBuffer.OnWrite(numOfBytes) == false)
	{
		Disconnect(L"OnWrite Overflow");
		return;
	}

	int32 dataSize = _recvBuffer.DataSize();
	int32 processLen = OnRecv(_recvBuffer.ReadPos(), dataSize); // 컨텐츠 코드에서 재정의
	if (processLen < 0 || dataSize < processLen || _recvBuffer.OnRead(processLen) == false)
	{
		Disconnect(L"OnRead Overflow");
		return;
	}
	
	// 커서 정리
	_recvBuffer.Clean();

	// 수신 등록
	RegisterRecv();
}

위에서, _recvBuffer 를 통해 numOfBytes 만큼을 읽은 다음, recvBuffer 의 _readPos, _writePos 를 Clean 을 통해 재조정해 주는 것을 확인할 수 있다! 😮

Comments