이전에 정리한 것처럼 TCP는 경계선 개념이 없기 때문에 데이터가 쪼개져서 들어올 가능성이 있다.
이러한 상황을 대비한 receive buffer를 만들어보자.
1차원 배열 공간의 read위치, write위치를 기억하고 이를 이용한 버퍼를 만든다. packet을 받으면 write 위치를 받은 데이터 크기만큼 이동한다. packet을 읽으면 read위치를 이동한다. 만약 write, read 위치가 동일하면 더 이상 읽을 데이터가 없다는 의미이므로 read, write 위치를 모두 첫 번째 위치로 옮긴다. 다른 경우, 만약 더 이상 write할 공간이 부족하면 read와 write사이의 데이터를 복사하여 처음 위치로 옮기고 write, read 위치를 각각 업데이트 한다.
RecvBuffer.h
#pragma once
/*---------------
RecvBuffer
---------------*/
// * [r][][][w][][][][][][][][][][][][]
// 1. [][][][][][][][][][][rw][][][][][]
// -> [rw][][][][][][][][][][][][][][][]
// 2. [][][][][][][][][][][][][r][][][w]
// -> [r][][][w][][][][][][][][][][][][]
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;
};
RecvBuffer.cpp
#include "pch.h"
#include "RecvBuffer.h"
/*---------------
RecvBuffer
---------------*/
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;
}
Session을 수정해서 RecvBuffer를 사용해보자.
Session.h
#pragma once
#include "IocpCore.h"
#include "IocpEvent.h"
#include "NetAddress.h"
#include "RecvBuffer.h"
class Session : public IocpObject
{
friend class Listener;
friend class IocpCore;
friend class Service;
enum
{
BUFFER_SIZE = 0x10000 // 64KB
};
...
private:
USE_LOCK;
/* 수신 관련 */
RecvBuffer _recvBuffer;
/* 송신 관련 */
...
};
Session.cpp
#include "pch.h"
#include "Session.h"
#include "SocketUtils.h"
#include "Service.h"
/*----------------
Session
----------------*/
Session::Session() : _recvBuffer(BUFFER_SIZE)
{
_socket = SocketUtils::CreateSocket();
}
...
void Session::RegisterRecv()
{
if (IsConnected() == false)
return;
_recvEvent.Init();
_recvEvent.owner = shared_from_this(); // ADD_REF
WSABUF wsaBuf;
wsaBuf.buf = reinterpret_cast<char*>(_recvBuffer.WritePos());
wsaBuf.len = _recvBuffer.FreeSize();
DWORD numOfBytes = 0;
DWORD flags = 0;
if (SOCKET_ERROR == ::WSARecv(_socket, &wsaBuf, 1, OUT &numOfBytes, OUT &flags, &_recvEvent, nullptr))
{
int32 errorCode = ::WSAGetLastError();
if (errorCode != WSA_IO_PENDING)
{
HandleError(errorCode);
_recvEvent.owner = nullptr; // RELEASE_REF
}
}
}
...
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();
}
...
1. Receive 동작과 관련하여 Receive가 멀티 쓰레딩 가능성이 없는 이유는?
- 한 세션당 WSARecv를 한 번만 걸어주기 때문
2. Session에 Recv buffer를 두고 쓸 때 문제는?
- Tcp 연결이라 한 번에 모든 데이터를 받지 않을 수 있다. 지금은 한 번에 모든 데이터를 받는다고 가정하고 다음 Recv 데이터를 버퍼에 덮어쓰는 문제가 있음.
3. Recv buffer 사이즈는 유한할 수 밖에 없는데, 이를 해결하는 방법은?
- 순환 버퍼
4. Recv buffer 의 규칙은?
- W 포인터로 Write 한 마지막 위치를 가리키고, R 포인터로 Read 한 마지막 위치를 표시함.
- W과 R 사이의 버퍼가 앞으로 읽어야 할 데이터
5. Recv buffer W과 R 포인터를 최대한 겹치게 만들기 위해서 사용하는 방법은?
- 사용하고자 하는 사이즈 보다 Buffer_count 배 만큼 버퍼를 크게 잡아서 가능성을 높임.
참조 : [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 - 인프런 | 강의 (inflearn.com)
SendBuffer Pooling (0) | 2023.04.05 |
---|---|
SendBuffer (0) | 2021.12.21 |
네트워크 라이브러리 만들기(Session #3) (0) | 2021.12.13 |
네트워크 라이브러리 만들기(Session #2) (0) | 2021.12.08 |
네트워크 라이브러리 만들기(Session #1) (0) | 2021.12.06 |
댓글 영역