반응형

세션을 작업하면서 버퍼를 임의의 배열로 만들어서 사용하였는데 이를 수정하는 작업을 하도록 하자.

(send보다 비교적 간단해서 recv를 먼저 수정.)

 

왜 지금 사용하는 _recvBuffer를 고쳐야 할까?

 

우선 recv같은 경우에는 멀티쓰레드 환경에서도 안전하다.

왜냐하면 한번에 한 쓰레드만 RegisterRecv()와 ProcessRecv()로 들어오도록 코드를 작성하였기 때문이다.

 

문제는 바로 _recvBuffer를 일반 포인터를 사용한다는 것에 문제가 있다.

우리가 작업한 Session에는 wsaBuf를 만들어서 수신할 수 있는 최대크기를 _recvBuffer만큼 받을 수 있도록 하였다.

하지만 TCP 특성상 100바이트를 보냈다고 한번에 100바이트를 받는게 아니라 나눠서 받게 되는 경우가 생길 수 도 있다.

 

그리고 무엇보다 패킷은 완전체로 받아야 처리를 할 수 있는데 만약 유저의 이동 정보가 들어있는 패킷(100바이트)를

30바이트만 받은 상태라면 유저의 이동 데이터는 아직 받지 못했기 때문에 처리를 할 수 없을 것이다.

하지만 우리가 작업한 Session에 RegisterRecv()는 항상 데이터가 처리 되었다고 가정하고 시작주소를 건네주어서 기존의 정보를 덮어 씌우게 된다.

 

 

이런 현상을 해결하기 위해서는 패킷이 완전체로 왔는지 판별해줘야 한다.

그래서 패킷이 완전하게 오지 않았을 경우에는 기존에 있던 데이터를 남기고 남은 데이터를 덧 붙여 줘야한다.

 


RecvBuffer

 

//RecvBuffer.h

#pragma once

/*==================
* 
*	RecvBuffer
* 
==================*/
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;
	xvector<BYTE> _buffer;
};
  • BYTE* ReadPos() : 읽을 데이터 위치

  • BYTE* WritePos() : 작성할 데이터 위치

  • int32 DataSize() : 데이터 크기 (_writePos - _readPos를 해서 남은 데이터 크기 구함)

  • int32 FreeSize() : 작성할 수 있는 데이터 크기

  • BUFFER_COUNT = 10 : 버퍼 크기를 늘려주기 위해 정한 임의의 변수

 


처음 시작은 read write 둘다 0
데이터를 복사해서 RecvBuffer에 데이터를 쓰게 되면 write가 앞으로 가게 된다.
예를 들어 4바이트를 쓰게 되면 write는 5바이트 부터 시작하게 된다.
그리고 read는 계속 write를 따라가면 데이터를 읽는다.

 

 


그러다가 어느순간 부터 버퍼가 꽉 차서 더이상 write를 할 수 없는 상황이 올텐데,
보통 이럴 때, 가장 오래되고 정석적인 방법으로 순환 버퍼를 사용한다.

 

 


만약 read와 write가 겹쳤다면 모든 데이터를 읽었다는 것이므로
read write 둘다 시작위치(0)로 옮겨준다. (복사 비용 없음)

 

 


만약 버퍼가 꽉 차서 더 이상 write하지 못한다면 남아있는 데이터를 시작위치로 복사한다.

 


 

//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;
}

 

코드가 간단해서 다른 함수는 볼 필요 없고 void RecvBuffer::Clean() 함수만 보도록 하자.

 

void RecvBuffer::Clean()

 

우선 데이터 크기를 가져온다.

 

만약 데이터 크기가 0일 경우, 읽기+쓰기 위치가 동일하다는 소리이기 때문에 둘 다 시작위치로 리셋시킨다.

(복사 비용 없음)

 

하지만 데이터 크기가 0이 아니고 여유 공간(데이터 작성 가능한 공간)이 버퍼 1개 크기 미만이면,

남은 데이터를 시작위치로 복사한다. (데이터를 앞으로 당김)

기존에 버퍼에 남아있는 데이터는 삭제할 필요 없다. (어차피 덮어쓸거기 때문)

 


버퍼 크기에 10을 곱한이유는?

복사비용을 줄이기 위해서이다.
우리가 사용하는 최대 버퍼 크기보다 더 크게 잡아주는 것이다.
예를 들어 우리가 버퍼의 최대크기를 5바이트로 잡아 놓았다면,
아래와 같이 한참 더 크게 만들어 놓고, 여기서 계속 read와 write를 하다보면
언젠간 read와 write가 겹치게 되고 그렇게 되면 복사비용 없어지면서 최적화가 될 것이다.
[   ][   ][   ][   ][   ]      [   ][   ][   ][   ][   ]      [   ][   ][   ][   ][   ]      [   ][   ][   ][   ][   ]      [   ][   ][   ][   ][   ]

 

(물론 writePos가 버퍼의 끝까지 갔는데 아직 데이터 남아있다면 어쩔 수 없이 데이터를 버퍼의 처음 위치로 복사해 와야 할 것이다.)

 

즉, 최대한 복사가 일어나지 않도록 해주기 위해 10을 곱해서 최대 버퍼 크기보다 더 크게 잡아주었다.


이제 우리가 만든 RecvBuffer를 기존의 Session으로 옮겨주자.

 

private:
	USE_LOCK;

	/* 수신 관련 */
	RecvBuffer _recvBuffer;

 

 

 

void Session::ProcessRecv(int32 numOfBytes)
{
	//성공적으로 비동기 WSARecv()가 완료된 상태이니까 레퍼런스 카운트를 1감소 시켜준다.
	_recvEvent.owner = nullptr;

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

	//write위치를 데이터 크기 만큼 옮겨줌
	if (_recvBuffer.OnWrite(numOfBytes) == false)
	{
		Disconnect(L"OnWrite Overflow");
		return;
	}

	int32 dataSize = _recvBuffer.DataSize();
	//버퍼에 있는 데이터를 읽음
	//읽은 데이터 만큼 recv 위치를 옮김.
	int32 processLen = OnRecv(_recvBuffer.ReadPos(), dataSize); //컨텐츠 코드에서 오버라이딩

	if (processLen < 0 || dataSize < processLen || _recvBuffer.OnRead(processLen) == false)
	{
		Disconnect(L"OnRead Overflow");
		return;
	}

	//커서 정리
	_recvBuffer.Clean();

	RegisterRecv();

	//기존의 있던 버퍼를 날리는게 아닌 계속 누적한 채로 동작하게 된다.
}
반응형

'네트워크 프로그래밍' 카테고리의 다른 글

SendBuffer Pooling  (0) 2023.04.07
SendBuffer  (0) 2023.03.24
Session (3)  (0) 2023.03.17
Session (2)  (0) 2023.03.17
Session (1)  (0) 2023.03.03