지금까지 수신버퍼를 임시로 단순 배열로 만들어 사용했었다.
하지만 정말 임시적인 방편이고 제대로 된 처리를 위해 버퍼 클래스를 만들 것이다.
송신버퍼는 내용이 많으므로 별도의 글로 정리할 것이다.
1. 버퍼 클래스 작성
일단 기존 세션 헤더에 있던 버퍼 관련 내용은 다 날리고 아래와 같이 수신버퍼를 선언한다.
// Session.h
//...
private:
weak_ptr<Service> _service;
SOCKET _socket = INVALID_SOCKET;
NetAddress _netAddress = {};
Atomic<bool> _connected = false;
private:
USE_LOCK;
/* 수신 관련 */
RecvBuffer _recvBuffer;
/* 송신 관련 */
private:
/* IocpEvent 재사용 */
ConnectEvent _connectEvent;
DisconnectEvent _disconnectEvent;
RecvEvent _recvEvent;
};
이제 RecvBuffer라는 클래스를 작성하자.
#pragma once
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;
};
기본적으로 버퍼는 벡터로 관리된다.
여기서 Read나 Write 위치는 커서로 관리한다.
앞의 데이터에 손을 대지 않고도 뒤쪽 남은 버퍼에 데이터들을 때려 넣을 수 있다.
enum으로 버퍼 카운트를 설정한 것 등에 대해선 함수들을 정의하며 설명한다.
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;
}
}
}
분기문으로 R커서와 W커서가 동일한 곳을 가리킬 때와 아닐 때로 나눴다.
만약 같은 위치에 있다면 둘 다 버퍼의 가장 앞을 가리키게 한다.
이렇게 복사를 하지 않게 해 비용을 없앰으로써, 코스트를 거의 들이지 않아도 된다는 이점을 얻을 수 있다.
만약 다른 위치일 때 버퍼에 남은 공간(_capacity)이 원래 버퍼 사이즈(_buffer) 보다 작을 경우,
기존의 데이터를 맨 앞으로 옮기고 포인터를 새로이 위치시킨다.
데이터를 맨 앞으로 옮긴다는 것은 복사 비용이 발생한다는 뜻이다.
버퍼의 크기가 작을 수록 R커서와 W커서가 같은 위치에 있을 확률이 떨어지게 되기 때문에,
BUFFER_COUNT를 통해 _capacity를 늘려서 R커서와 W커서가 같은 위치에 있을 확률을 높이고자 한 것이다.
같은 위치에 있어서 복사 비용이 들지 않을수록 효율적으로 동작하게 된다.
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;
}
OnRead/Write 시엔 읽을 데이터가 비정상적으로 크진 않은지,
쓸 데이터가 여유공간보다 많지 않은지 체크하고 포인터를 이동하게 한다.
2. 기존 버퍼 코드 대체
기존 단순 배열 형태로 사용하던 시절의 버퍼를 사용하는 부분들을 고쳐줘야 한다.
지금은 고칠 부분 중의 한 곳에 대해서만 보자.
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();
}
ProcessRecv() 함수를 보자.
OnWrite()를 호출할 때 문제가 생긴다면 접속을 종료한다.
누적된 데이터의 크기를 dataSize로 정의하고, OnRecv()에서 받은 데이터의 길이를 받아온다.
문제가 될 수 있는 사안에 대해 예외처리를 하고 문제가 생기면 접속을 종료시킨다.
문제가 없다면 Clean()를 호출해 커서를 정렬하고 다른 수신을 받기 위해 RegisterRecv()를 걸어준다.
3. 실행
수신 버퍼의 교체는 성공적으로 이루어 졌다. 문제없이 받는다.
다음엔 송신버퍼에 관해 공부한다.
'Study > C++ & C#' 카테고리의 다른 글
[C++] SendBuffer Pooling (0) | 2023.06.30 |
---|---|
[C++] SendBuffer (0) | 2023.06.29 |
[C++] Session (0) | 2023.06.24 |
[C++] Service (0) | 2023.06.23 |
[C++] IOCP Core (0) | 2023.06.21 |