[게임 서버] SetBuffer를 RecvBuffer, SendBuffer로 따로 빼내기 #1
패킷 단위로 데이터를 주고 받기 위해서 해야 할 첫 관문은 server 내에 있는 recvBuff를 따로 빼서 관리하는 것이다. 기존 프로그램에서는 SetBuffer를 통해 버퍼 크기, offset을 미리 설정해준 다음에 어떤 변화도 없이 쭉 그대로 사용해왔다. 이렇게 사용하게 되면 문제가 발생한다.
TCP 특성상 클라이언트가 보낸 내용이 한 번에 다 오지 않을 수 있다. 앞서 보낸 데이터 중 일부가 버퍼에 데이터가 남아있을 경우 남은 데이터와 새로 보낼 데이터를 함께 보내는데 이때 매우 곤란한 상황이 펼쳐진다. 변화값인 offset을 0으로 설정했기 때문에 남은 데이터를 무시하고 그 위에 새로운 데이터를 덮어쓰게 되는 것이다.
문제를 방지하기 위해서 앞으로는 SetBuffer를 사용하지 않고 RecvBuffer와 SendBuffer를 사용할 것이다.
우선 새로운 클래스인 RecvBuffer를 만들어준다.
ArraySegment<byte> _buffer;
int _readPos; // 읽는 커서
int _writePos; // 쓰는 커서
public RecvBuffer(int bufferSize)
{
_buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
}
public int DataSize { get { return _writePos - _readPos; } } // 유효범위. 데이터가 버퍼에 얼마나 쌓여있나.
public int FreeSize { get { return _buffer.Count - _writePos; } } // 버퍼에 남은 공간.
이때 ArraySegment<byte>는 바이트 배열로 들고 있어도 사용하는데 상관 없지만 굉장히 큰 바이트 배열에서 원하는 만큼의 크기를 뽑아서 쓸 수 있도록 하기 위해 ArraySegment로 설정해준다. 각기 다른 클라이언트가 다양한 데이터를 전송하기 때문에 개별적인 buffer를 설정해주어야 한다.
만약 사진처럼 5개 만큼의 크기를 가진 버퍼가 있을 경우 r을 읽는 커서, w을 쓰는 커서로 상황을 설정해보겠다.
DataSize는 데이터가 버퍼에 얼마나 쌓여있나를 묻는 변수다. 버퍼에 들어있는, 아직 처리되지 않은 데이터의 크기를 묻는다. 우리의 예시에서는 w에서 r을 뺀 값, 2 - 0 = 2가 DataSize가 되는 것이다.
FreeSize는 버퍼에 남은 공간을 묻는 변수다. r 옆에 있는 빈 공간은 어떤 작업을 할 지 모르기 때문에 건드릴 수 없다. 자유롭게 작업할 수 있는 공간은 wrtiePos가 있는 공간부터 끝 공간까지이다. 버퍼의 총 크기에서 w을 빼면 5 - 2 = 3이 되는 것이다.
public ArraySegment<byte> ReadSegment // 데이터가 들어있는 범위?
{
get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _readPos, DataSize); }
}
public ArraySegment<byte> WriteSegment // 다음에 recv를 할 때 어디서부터 유효범위인지?
{
get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _writePos, FreeSize); }
}
ReadSegment라는 이름으로 데이터가 들어있는 범위가 얼마나 되는가를 알아보고, WriteSegment라는 이름으로 다음 recv를 할 때 유효범위가 어디서부터인가를 알아본다.
ArraySegment에는 위에서 설정한 버퍼 배열과 그 offset(변화값), DataSize, FreeSize를 넣게 된다.
사진에서처럼 데이터가 들어있다면, ReadSegment에서 offset(변화값)은 버퍼의 변화값에서 readPos(0)를 더한 값이 된다. count값은 DataSize(2)다(ReadSegment를 다른 이름으로 하자면 DataSegment라고도 할 수 있다).
WriteSegment는 RecvSegment라고도 한다. 다음 recv를 할 때 유효범위를 묻는 함수이다. 이때 offset은 offset과 writePos(2)를 합한 값이 되고, count는 FreeSize(3)가 된다.
public void Clean() // 점점 뒤로 밀리는 w과 r을 앞으로 땡겨주기
{
int dataSize = DataSize;
if (dataSize == 0) // w과 r이 똑같은 위치에 있다. 즉, 클라에서 보낸 데이터를 다 읽어서 더 읽을 게 없다.
{
_readPos = _writePos = 0;
}
else
{
// 클라에서 보낸 데이터가 남아있는 상황, 데이터를 복사해서 처음 위치에 넣기.
Array.Copy(_buffer.Array, _buffer.Offset + _readPos, _buffer.Array, _buffer.Offset, dataSize);
_readPos = 0;
_writePos = dataSize;
}
}
데이터를 계속 받으면 당연히 buffer에서 writePos와 readPos가 뒤로 밀려나게 된다. 계속 밀려나는 커서를 앞으로 땡겨주기 위해 Clean 함수를 활용한다. 주석에서와 같이 w과 r이 같은 위치에 있다면 모든 데이터를 처리했다는 뜻이므로 둘 다 처음 위치로 초기화해준다. 클라에서 보낸 데이터가 남아있는 상황이라면 데이터를 복사해서 초기 위치로 옮겨준다.
// 데이터 가공을 성공적으로 했다면 readPos를 뒤로 움직여준다. (다 읽었다는 의미로)
public bool OnRead(int numOfBytes)
{
if (numOfBytes > DataSize)
return false;
_readPos += numOfBytes;
return true;
}
// 클라에서 데이터를 보냈을 때 버퍼에 그 데이터를 넣은만큼 writePos를 뒤로 빼주기.
public bool OnWrite(int numOfBytes)
{
if (numOfBytes > FreeSize)
return false;
_writePos += numOfBytes;
return true;
}
데이터 가공이 끝난 상황이라면 OnRead 함수를 호출하게 된다. 데이터의 크기가 DataSize보다 크다면 데이터를 받을 수 없으니 false한다. 데이터 크기가 DataSize보다 작거나 같으면 readPos를 데이터 크기만큼 늘려 자리를 이동시킨다.
OnWrite도 마찬가지로 크기를 확인하고, 데이터 크기가 FreeSize보다 작거나 같으면 writePos를 데이터 크기만큼 늘려 자리를 이동시킨다.