bdfgdfg

[온라인 서버] 네트워크 라이브러리 - RingBuffer 클래스 ★ (메모리 풀!) 본문

게임프로그래밍/서버 책

[온라인 서버] 네트워크 라이브러리 - RingBuffer 클래스 ★ (메모리 풀!)

marmelo12 2022. 1. 5. 16:35
반응형

RingBuffer 클래스

 - 이 클래스는 메모리 풀(Memory Pool)개념이 적용된 버퍼 공간으로 주된 사용 목적은 연결에 대한 패킷을 보내거나 받을 때 사용한다.

 - 메모리 풀링(Memory Pooling)은 미리 사용할 메모리를 할당해 놓고 필요할때마다 꺼내서 쓰는 방식.

   -> 쓰레드 풀과 같은 개념. 미리 쓰레드를 할당해놓고 필요할때마다 꺼내 사용하는 방식 (매번 할당/소멸의 형태x)

   -> 위와 마찬가지로 메모리를 매번 할당하고 해제하는 작업이 생각보다 CPU소모가 있다.

   -> 더 큰 이유로는 반복적으로 메모리를 할당하고 해제하면 메모리 단편화가 생길 수 있기 때문.

   -> 메모리 단편화는 메모리가 조각나는 현상. 심해지면 메모리를 할당하는데 오랜 시간이 걸리며 (OS가 메모리를 할         당할 공간을 찾는 시간) 최악의 경우 메모리를 더이상 할당할 수 없게되는 현상.

   -> 계속해서 켜져야 하는 서버 프로세스에서 이러한 메모리 단편화는 치명적이다. (되도록 동적할당이 자주 일어나지         않게끔 유도하고 미리 큰 메모리를 할당해 필요할때마다 꺼내다가 사용하고 다 사용한 메모리는 집어넣는 방식이         용)

 - 특히 패킷과 같이 변적인 크기를 가진 데이터의 메모리를 효율적으로 활용.

 - 위와 같은 일련의 과정을 메모리 풀링이라 한다.

 

RingBuffer 클래스에서 미리 메모리를 할당받아 링 버퍼(RingBuffer)구조를 이용해 메모리 풀링을 구현한다.

 - 링 버퍼 구조를 사용하는 이유는 가변적인 메모리 할당이 가능하다는 장점이 존재한다.

 

즉 링버퍼라는 미리 할당한 메모리를 가져다가 그 메모리를 계속해서 가져다가 사용하는 것.

ex)

[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]

100번지에서~ 200번지까지 미리 할당된 메모리를 가져다 쓴다.

예로들어 30바이트의 메모리를 사용하고 싶다면 100번지 ~ 130번지까지의 메모리에 할당.

그 다음에 메모리를 가져다가 사용하기 위해서는 겹치면 안되므로 CurrentMark등을 이용하여 현재 사용 가능한 공간은

130번지부터라는 것을 따로 기억해야 함.

 -> 또한 위의 버퍼는 끝이 존재하므로 링 버퍼라는 이름그대로 원형으로 쓰기위해(원형큐랑 유사)다시 100번지의 첫번       째 offset으로 돌리는등 처리가 필요하다.

 

여기서 링버퍼 클래스는

미리 할당된 메모리에서 요청한 메모리 크기를 요청하면 내부적으로 사용한만큼 사용위치 포인터를 옮긴 후

버퍼의 시작주소를 넘겨준다. 거기에 우리가 요청한 메모리 크기만큼 사용한다.

그리고 이렇게 얻어온 버퍼에 보낼 데이터 값들을 설정한 후 데이터를 송신하거나 데이터를 수신한다.

#include "Monitor.h"
class RingBuffer : public Monitor
{
public:
	RingBuffer();
	~RingBuffer();

	// 링 버퍼 메모리 할당
	bool			Create(int bufferSIze = MAX_RINGBUFSIZE);
	// 초기화
	bool			Initialize();
	//할당된 버퍼 크기를 반환
	inline  int		GetBufferSize() { return m_bufferSize; }

	//해당하는 내부 버퍼의 포인터를 반환
	inline char*	GetBeginMark() { return m_pBeginMark; }
	inline char*	GetCurrentMark() { return m_pCurrentMark; }
	inline char*	GetEndMark() { return m_pEndMark; }
	//내부 버퍼의 현재 포인터를 전진
	char*			ForwardMark(int forwardLen);
	char*			ForwardMark(int forwardLen, int nextLen, DWORD remainLen);

	// 사용된 내부 버퍼 해제
	void			ReleaseBuffer(int releaseSize);
	// 사용된 내부 버퍼 크기 반환
	inline int		GetUsedBufferSize() { return m_usedBufferSize; }
	// 누적 버퍼 사용양 반환
	inline int		GetAllUsedBufferSize() { return m_allUsedBufSize; }
	// 내부 버퍼 데이터 읽어서 반환
	char*			GetBuffer(int readSize, int* pReadSize);
private:
	char*			m_pRingBuffer;			// 실제 데이터를 저장하는 버퍼 포인터

	// LastMoveMark까지 버퍼의 위치를 가리키는 포인터 변수들.
	char*			m_pBeginMark;				// 버퍼의 처음부분을 가리키고 있는 포인터
	char*			m_pEndMark;				// 버퍼의 마지막 부분을 가리키고 있는 포인터
	char*			m_pCurrentMark;			// 버퍼의 현재까지 사용된 부분을 가리키고 있는 포인터.
	char*			m_pGettedBufferMark;		// 현재까지 데이터를 읽은 버퍼 포인터
	char*			m_pLastMoveMark;			// recycle되기 전에 마지막 포인터

	int				m_bufferSize; // 내부 버퍼의 총 크기
	int				m_usedBufferSize; // 현재 사용된 내부 버퍼의 크기
	UINT			m_allUsedBufSize; // 총 처리된 데이터 양
	Monitor			m_csRingBuffer; // 동기화 객체

private:
	// 복사 금지.
	RingBuffer(const RingBuffer& rhs)			 = delete;
	RingBuffer& operator=(const RingBuffer& rhs) = delete;
};
#include "RingBuffer.h"

RingBuffer::RingBuffer() : m_pRingBuffer(nullptr),m_pBeginMark(nullptr),m_pEndMark(nullptr),m_pCurrentMark(nullptr),
m_pGettedBufferMark(nullptr),m_pLastMoveMark(nullptr),m_usedBufferSize(0),m_allUsedBufSize(0)
{
    
}

RingBuffer::~RingBuffer()
{
    if (m_pBeginMark != nullptr)
        delete[] m_pBeginMark;;
}


bool RingBuffer::Initialize()
{
    Monitor::Owner lock(m_csRingBuffer); // Lock
    {
        m_usedBufferSize = 0;
        m_pCurrentMark = m_pBeginMark;
        m_pGettedBufferMark = m_pBeginMark;
        m_pLastMoveMark = m_pEndMark;
        m_allUsedBufSize = 0;
    }
    return true;
}

bool RingBuffer::Create(int bufferSIze)
{
    if (m_pBeginMark != nullptr)
        delete[] m_pBeginMark;

    m_pBeginMark = new char[bufferSIze];

    if (m_pBeginMark == nullptr) // 메모리 할당이 안되었으면
    {
        std::cout << "링버퍼 메모리 할당 실패." << std::endl;
        return false;
    }

    m_pEndMark = m_pBeginMark + bufferSIze - 1;
    return true;
}


char* RingBuffer::ForwardMark(int forwardLen)
{
    char* pPreCurrentMark = nullptr;
    Monitor::Owner lock(m_csRingBuffer);
    {
        //링 버퍼 오버플로우 체크
        if (m_usedBufferSize + forwardLen > m_bufferSize)
            return nullptr;


        if ((m_pEndMark - m_pCurrentMark) >= forwardLen)
        {
            pPreCurrentMark = m_pCurrentMark;
            m_pCurrentMark += forwardLen;
        }
        else
        {
            //순환되기 전 마지막 좌표를 저장
            m_pLastMoveMark = m_pCurrentMark;
            m_pCurrentMark = m_pBeginMark + forwardLen;
            pPreCurrentMark = m_pBeginMark;
        }
        m_usedBufferSize += forwardLen;
        m_allUsedBufSize += forwardLen;
    }
    return pPreCurrentMark;
}

char* RingBuffer::ForwardMark(int forwardLen, int nextLen, DWORD remainLen)
{
    Monitor::Owner lock(m_csRingBuffer);
    {
        // 링 버퍼 오버플로 체크
        if (m_usedBufferSize + forwardLen + nextLen > m_bufferSize)
            return nullptr;

        // 그게 아니라면 전진.
        if ((m_pEndMark - m_pCurrentMark) > (nextLen + forwardLen))
            m_pCurrentMark += forwardLen;
        else // 다시 좌표를 처음으로.
        {
            // 순환되기전 마지막 좌표를 저장.
            m_pLastMoveMark = m_pCurrentMark;
            ::CopyMemory(m_pBeginMark, m_pCurrentMark - (remainLen - forwardLen), remainLen);
            m_pCurrentMark = m_pBeginMark + remainLen;
        }
        m_usedBufferSize += forwardLen;
        m_allUsedBufSize += forwardLen;
    }
    return m_pCurrentMark;
}

void RingBuffer::ReleaseBuffer(int releaseSize)
{
    Monitor::Owner lock(m_csRingBuffer);
    {
        m_usedBufferSize -= releaseSize;
    }
}

char* RingBuffer::GetBuffer(int readSize, int* pReadSize)
{
    char* pRet = nullptr;
    Monitor::Owner lock(m_csRingBuffer);
    {
        // 마지막까지 다 읽었다면 그 읽어드릴 버퍼의 포인터는 맨 앞으로 옮긴다.
        if (m_pLastMoveMark == m_pGettedBufferMark)
        {
            m_pGettedBufferMark = m_pBeginMark;
            m_pLastMoveMark = m_pEndMark;
        }

        // 현재 버퍼에 있는 size가 읽어드릴 size보다 크다면
        if (m_usedBufferSize > readSize)
        {
            // 링 버퍼의 끝인지 판단한다.
            if ((m_pLastMoveMark - m_pGettedBufferMark) >= readSize)
            {
                *pReadSize = readSize;
                pRet = m_pGettedBufferMark;
                m_pGettedBufferMark += readSize;
            }
            else
            {
                *pReadSize = (int)(m_pLastMoveMark - m_pGettedBufferMark);
                pRet = m_pGettedBufferMark;
                m_pGettedBufferMark += *pReadSize;
            }
        }
        else if (m_usedBufferSize > 0)
        {
            // 링 버퍼의 끝인지 판단.
            if ((m_pLastMoveMark - m_pGettedBufferMark) >= m_usedBufferSize)
            {
                *pReadSize = m_usedBufferSize;
                pRet = m_pGettedBufferMark;
                m_pGettedBufferMark += m_usedBufferSize;
            }
            else
            {
                *pReadSize = (int)(m_pLastMoveMark - m_pGettedBufferMark);
                pRet = m_pGettedBufferMark;
                m_pGettedBufferMark += *pReadSize;
            }
        }
    }
    return pRet;
}

락이 걸려있는 모습이 있는데 둘 이상의 쓰레드가 동시에 공유자원에 값을 수정할 수 있으므로 lock을 걸었다.

 

하나씩 본다.

bool RingBuffer::Create(int bufferSIze)
{
    if (m_pBeginMark != nullptr)
        delete[] m_pBeginMark;

    m_pBeginMark = new char[bufferSIze];

    if (m_pBeginMark == nullptr) // 메모리 할당이 안되었으면
    {
        std::cout << "링버퍼 메모리 할당 실패." << std::endl;
        return false;
    }

    m_pEndMark = m_pBeginMark + bufferSIze - 1;
    return true;
}

먼저 Ring Buffer 생성.

Ring Buffer가 이미 생성되었따면 부순다음 다시 생성한다.

그리고 EndMark를 다시 재 설정한다. (인자로 넘어온 bufferSize의 크기에 맞추기 위해)

Mark가 붙은 멤버변수들이 나오는데 각각

처음 위치를 가리키는 포인터, 현재 위치를 가르키는 포인터, 마지막 위치를 가르키는 포인터로 사용된다.

ㅠㅠ

위 그림 처럼 링버퍼의 메모리에서 위치를 가르키는 포인터 역할을 한다.

currentMark의 경우는 누가 30바이트 메모리 할당을 요청한다면 30바이트만큼 전진하여 currentMark가 그 위치를 가리키게끔 한다.

 

여기서 currentMark를 움직여줄 함수가 필요한데 위 클래스에선 2가지 함수로 정의하고 있다.

먼저 인자가 하나인 ForwardMark함수.

이 함수는 데이터를 보낼 때 필요한 버퍼 공간을 할당받기 위해 쓰인다.

즉 앞으로 사용할 수 있는 링버퍼의 현재 포인터를 넘겨준다.(오버플로x)

char* RingBuffer::ForwardMark(int forwardLen)
{
    char* pPreCurrentMark = nullptr;
    Monitor::Owner lock(m_csRingBuffer);
    {
        //링 버퍼 오버플로우 체크
        if (m_usedBufferSize + forwardLen > m_bufferSize)
            return nullptr;

        // 버퍼의 용량이 충분하면
        if ((m_pEndMark - m_pCurrentMark) >= forwardLen)
        {
            // 바로 처리
            pPreCurrentMark = m_pCurrentMark;
            m_pCurrentMark += forwardLen;
        }
        else // 그게 아니라면 순환
        {
            //순환되기 전 마지막 좌표를 저장
            m_pLastMoveMark = m_pCurrentMark;
            m_pCurrentMark = m_pBeginMark + forwardLen;
            pPreCurrentMark = m_pBeginMark;
        }
        m_usedBufferSize += forwardLen;
        m_allUsedBufSize += forwardLen;
    }
    return pPreCurrentMark;
}

먼저 pPreCurrentMark를 선언하고 있다.

즉 현재 currentMark가 사용가능한 공간의 위치이고. 메모리를 먼저 할당할 공간만큼을 currentMark를 먼저 전진시킨 다음에 pPreCurrentMark를 이용하여 반환해준다.

 -> 즉 먼저 currentMark를 이동하기 위해서 pPreCurrentMark를 이용한것(현재 currentMark를 저장해서 반환)

 

if문을 보면 먼저 링버퍼의 메모리 공간이(앞으로 저장할 공간이)충분하다면 이어서 저장하면 된다.

그게 아니라면 링버퍼의 특징상 순환하여 다시 저장하는데.

먼저 순환되기 전 마지막 좌표를 저장하고, 현재 currentMark를 시작 위치에서 할당한 위치만큼 더해준다.

처음위치부터 다시 메모리를 할당한만큼 위치 시켰으므로 pPreCurrentMark는 시작위치 포인터를 대입한다.

 

이렇게 현재 위치 저장이 다 완료되었으면 현재 버퍼의 사용한 크기와 이때까지 사용되었던 메모리 크기를 더해준다.

 

char* RingBuffer::ForwardMark(int forwardLen, int nextLen, DWORD remainLen)
{
    Monitor::Owner lock(m_csRingBuffer);
    {
        // 링 버퍼 오버플로 체크
        if (m_usedBufferSize + forwardLen + nextLen > m_bufferSize)
            return nullptr;

        // 그게 아니라면 전진.
        if ((m_pEndMark - m_pCurrentMark) > (nextLen + forwardLen))
            m_pCurrentMark += forwardLen;
        else // 다시 좌표를 처음으로.
        {
            // 순환되기전 마지막 좌표를 저장.
            m_pLastMoveMark = m_pCurrentMark;
            ::CopyMemory(m_pBeginMark, m_pCurrentMark - (remainLen - forwardLen), remainLen);
            m_pCurrentMark = m_pBeginMark + remainLen;
        }
        m_usedBufferSize += forwardLen;
        m_allUsedBufSize += forwardLen;
    }
    return m_pCurrentMark;
}

위의 함수는 오버로딩한 함수인데 인자가 2개 더 늘은것을 알 수 있다.

이전의 함수와 역할은 비슷하지만 이 함수는 데이터를 받았을 때, 링 버퍼로부터 버퍼를 할당받는다는 것에 대해 차이가 있다.

즉 이전의 함수는 데이터를 보낼 때, 이번의 함수는 데이터를 받았을 때의 역할 차이가 있다는 것.

 

이렇게 나눈 이유는 데이터를 보낼 때는 보낼 데이터의 길이를 알 수 있어 버퍼를 할당받는 것이 간단하지만.(이전 함수)

데이터를 받을 때는 데이터를 받기위해 설정한 길이와 실제 데이터를 받은 길이에 차이가 있기 때문이다.

 -> TCP의 데이터 경계가 없다는 특징!

즉 WSARecv. 비동기 Overlapped I/O를 요청하고 함수로 1024bytes의 데이터를 받겠다고 설정을 해놓으면

실제로 수신된 데이터는 1~1024bytes사이가 될 수 있다.

 

그래서 이 함수에서는 데이터를 수신한 후에 수신된 데이터 길이만큼 링 버퍼의 현재 사용된 버퍼의 포인터를 증가시키고 또 다음에 수신할 데이터의 최대 크기만큼 링 버퍼가 충분한 버퍼를 확보하고 있는지 알아보는 역할을 한다.

 

여기서 만약 다음에 수신할 데이터의 최대 크기만큼 링 버퍼가 버퍼를 확보하지 못했을 경우. 두가지로 나누는데

1. 사용할 수 있는 링버퍼의 크기보다 수신할 데이터의 최대 크기가 큰 경우 (오버플로)

2. 현재까지 사용된 버퍼를 가리키는 포인터가 링 버퍼의 마지막을 가리키는 포인터까지 도달하여 앞으로는 더 이상 할당할 버퍼 공간이 없는 경우. -> 순환

 

오버플로우는 NULL을 전달한다. 

두번째 경우는 링버퍼 전체크기를 넘어서는건 아니지만, 패킷이 잘려서 들어와 아직 수신할 데이터가 남아있지만

링버퍼의 마지막 포인터를 넘어서는 경우다. 이 경우에는 순환을 해야한다.

위의 그림인 상황. 즉 현재 잘려서 수신한 저 위의 파란색 영역을 링버퍼의 처음 시작부분으로 옮긴다.

즉 인자가 하나인 ForwardMark함수는 단순히 보낼 데이터의 버퍼를 할당받기 위해 바로 얻어오면 되지만.

데이터를 받아와 버퍼를 할당하는 경우는 도중에 데이터가 덜 들어올 경우를 대비하여 위와 같이 뒤처리가 필요하다.

 

마지막으로 GetBuffer 함수.

이 함수는 데이터를 보낼 때 링버퍼의 현재까지 사용된 버퍼에서 인자를 통해 전달된 길이만큼 보낼 데이터를 가져오는 역할을 한다.

데이터를 보낼 때 인자가 하나인 ForwardMark함수를 호출함으로써 링 버퍼에서 버퍼를 할당받는다고 했는데

이렇게 얻어온 버퍼에 보낼 데이터 값들을 설정하고 나중에 데이터를 실제로 전송할때는 ForwardMark함수 호출로 할당받은 버퍼를 읽어와야 하는데 이 떄 GetBuffer함수가 쓰인다.

 -> 읽을 버퍼의 크기가 현재 할당된 버퍼의 크기보다 크다면 현재 할당된 버퍼의 크기만큼 읽고 그 버퍼의 포인터 반환.

char* RingBuffer::GetBuffer(int readSize, int* pReadSize)
{
    char* pRet = nullptr;
    Monitor::Owner lock(m_csRingBuffer);
    {
        // 마지막까지 다 읽었다면 그 읽어드릴 버퍼의 포인터는 맨 앞으로 옮긴다.
        if (m_pLastMoveMark == m_pGettedBufferMark)
        {
            m_pGettedBufferMark = m_pBeginMark;
            m_pLastMoveMark = m_pEndMark;
        }

        // 현재 버퍼에 있는 size가 읽어드릴 size보다 크다면
        if (m_usedBufferSize > readSize)
        {
            // 링 버퍼의 끝인지 판단한다.
            if ((m_pLastMoveMark - m_pGettedBufferMark) >= readSize)
            {
                *pReadSize = readSize;
                pRet = m_pGettedBufferMark;
                m_pGettedBufferMark += readSize;
            }
            else
            {
                // 링버퍼의 끝과 읽은 시점까지의 차이가 readSize보다 작은 경우.
                *pReadSize = (int)(m_pLastMoveMark - m_pGettedBufferMark);
                pRet = m_pGettedBufferMark;
                m_pGettedBufferMark += *pReadSize;
            }
        }
        else if (m_usedBufferSize > 0)
        {
            // 링 버퍼의 끝인지 판단.
            if ((m_pLastMoveMark - m_pGettedBufferMark) >= m_usedBufferSize)
            {
                *pReadSize = m_usedBufferSize;
                pRet = m_pGettedBufferMark;
                m_pGettedBufferMark += m_usedBufferSize;
            }
            else
            {
                *pReadSize = (int)(m_pLastMoveMark - m_pGettedBufferMark);
                pRet = m_pGettedBufferMark;
                m_pGettedBufferMark += *pReadSize;
            }
        }
    }
    return pRet;
}

m_pLastMoveMark는 순환되기전 마지막 좌표를 기억한다 (pCurrentMark가 있던 자리.)
왜냐하면 링버퍼가 순환 되었을 때 endMark까지 남은 버퍼의 공간이 있는데 그 시작점부터 endMark까지는 빈공간이기 때문. m_pLastMoveMark가 그 시작점을 가르키게 된다.

 -> 디폴트는 endMark를 가르키게 된다.

 

 

본문은 강정중님의 온라인 게임 서버 서적을 참고하였습니다.

반응형
Comments