서론
네트워크 프로그래밍에서 소켓으로부터 받은 데이터를 임시로 저장하고 관리하는 버퍼 가 필요함 우리는 이전에 임시로 해둔거임
전부 Session에서 처리를 해도 되지만 강사님은 따로 모듈화해서 처리해뒀다고 함
이 클래스는 OS에서 들어온 Recv데이터 와
게임/서버 로직에서 처리하는 데이터(Read)의 경계를
ReadPos와 WritePos로 나눠서 관리한다
즉,
ReadPos : 콘텐츠가 어디까지 읽었는지
WritePos : OS가 어디까지 버퍼를 썼는지
나타내는것이다
주된 목적 : TCP가 데이터 경계를 보장하지 않아 조각난 데이터를 모으기 위해
사용되는 변수
private ArraySegment<byte> _buffer;
private int _readPos;
private int _writePos;buffer는 왜 byte배열이 아니에요?
byte[] 배열을 사용해도 되지만
나중에 엄청 큰 바이트 배열을 부분적으로 잘라서 쓰고싶을때도있으니
ArraySegment 변수 사용하는걸로함
동작 원리 (read / write 포인터 흐름)
배열 표시는 [ ] 로 진행하겠음
기본 개념
[rw][ ][ ][ ][ ][ ][ ][ ][ ][ ]
- 처음엔 r=w=0, 즉 아무 데이터도 안들어있는거
클라가 5바이트를 보냄
[r][ ][ ][ ][ ][w][ ][ ][ ][ ]
- recv 성공 → _writePos 가 5 증가.
- 아직 콘텐츠단에서 처리 안 했으니 _readPos 는 그대로.
콘텐츠에서 패킷을 처리 완료
[ ][ ][ ][ ][rw][ ][ ][ ][ ][ ]
- 콘텐츠단이 5바이트 소비 완료 → _readPos = _writePos = 5.
부분적 데이터가 넘어오거나 데이터를 배열을 초과하는 문제를 해결해보자
문제 상황 예시
- 2바이트짜리 패킷을 클라이언트가 두 개 연속 보냄 (총 4바이트)
- 근데 첫 recv에선 3바이트만 옴
[r][ ][ ][w][ ][ ][ ][ ][ ][ ] (유효 데이터 3바이트)
- 콘텐츠는 “2바이트짜리 패킷” 하나만 처리 가능함 → 첫 2바이트 소비
- 남은 1바이트는 다음 패킷이 도착하기 전까진 처리 불가
readPos = 2, _writePos = 3 대기
이 상태가 누적되면:
- r이 계속 증가하고
- w도 뒤로 밀리면서 뒤쪽 여유 공간이 점점 줄어듦
해결 방안 Clean 처리
이때, 남은 데이터(write - read) 를 앞으로 복사해서
read / write를 처음으로 돌려주는 것이 Clean() 함수의 역할이다.
public void Clean()
{
int dataSize = DataSize;
if (dataSize == 0)
{
_readPos = 0;
_writePos = 0;
}
else
{
Array.Copy(_buffer.Array, _buffer.Offset + _readPos, _buffer.Array, _buffer.Offset, dataSize);
_readPos = 0;
_writePos = dataSize;
}
}데이터를 읽고 쓸 때 사용하는 기능 모음
| 함수 | 역할 |
|---|---|
DataSize | 처리되지 않은 데이터의 길이 |
FreeSize | 버퍼의 남은 공간 |
OnRead() | 처리 완료 후 read 포인터 이동 |
OnWrite() | 수신 완료 후 write 포인터 이동 |
ReadSegment | 콘텐츠단에서 읽을 구간 |
WriteSegment | 다음 recv에서 쓸 구간 |
로직
//유효범위 얼마나 데이터가 쌓여있는지 (처리가 안된 데이터)
public int DataSize { get { return _writePos - _readPos; } }
//버퍼의 남은 공간
public int FreeSize { get { return _buffer.Count - _writePos; } }
//유효 범위의 세그먼트라해서
//어디부터 데이터를 콘텐츠 단에넘겨준건지에 대한 Segment
public ArraySegment<byte> ReadSegment
{
//데이터 배열
//어디부터 데이터를 넘겨줄지
//데이터의 크기는 얼만큼 보내줄지
get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _readPos, DataSize); }
}
// 다음에 Recv할 때 어디서부터 유효범위인지 나타내는것
// 예 : [ ][ ][ ][ ][r][ ][w][ ][ ][ ] 여기서 [w][ ][ ][ ] 이부분이 사용할 수 있는 범위
public ArraySegment<byte> WriteSegment
{
get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _writePos, FreeSize); }
}
//우리가 콘텐츠 코드에서 데이터를 가공해서 처리할껀대
//성공적으로 처리를 했으면 OnRead를 호출해서 커서위치를 바꿔줄것
public bool OnRead(int numOfBytes)
{
// numOfBytes 왜 받음?
// 범위를 넘어설 수 있기에 호출한 애가 numOfBytes 만큼 처리했다는걸 보내줌
// 근대 데이터 유효범위 이상을 처리하면 이상하게된거니까 false처리
if (numOfBytes > DataSize)
return false;
_readPos += numOfBytes;
return true;
}
// 클라에서 서버쪽으로 데이터를 쏴줘서 서버에서 Recv를 했을 때 _writePos를 이동시키기 위한 함수
public bool OnWrite(int numOfBytes)
{
if(numOfBytes > FreeSize)
return false;
_writePos += numOfBytes;
return true;
}Session의 사용 흐름
- Seesion에 recvBuff생성
- RigesterRecv 부분에서 Clean처리 후 OS에서 Recv데이터를 쌓아줄 버퍼를 셋 (Write버퍼)
- RecvCompleted 부분에서 Write커서이동
- 컨텐츠에서 처리할 수 있도록 데이터 넘김
- 컨텐츠 단에서 처리한 데이터 만큼 Read커서이동
1. Seesion에 recvBuff생성
RecvBuffer _recvBuffer = new RecvBuffer(1024);
2.RigesterRecv 부분에서 Clean처리 후 OS에서 Recv데이터를 쌓아줄 버퍼를 셋 (Write버퍼)
private void RegisterRecv()
{
_recvBuffer.Clean();
ArraySegment<byte> segment = _recvBuffer.WriteSegment;
_recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count);
bool pending = _socket.ReceiveAsync(_recvArgs);
if (!pending)
OnRecvCompleted(null, _recvArgs);
}
private void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
3. RecvCompleted 부분에서 Write커서이동
if(_recvBuffer.OnWrite(args.BytesTransferred) == false)
{
Disconnect();
return;
}
4.컨텐츠에서 처리할 수 있도록 데이터 넘김
int processLength = OnReceive(_recvBuffer.ReadSegment);
if (processLength < 0 || _recvBuffer.DataSize < processLength)
{
Disconnect();
return;
}
5.컨텐츠 단에서 처리한 데이터 만큼 Read커서이동
if(_recvBuffer.OnRead(processLength) == false)
{
Disconnect();
return;
}
}요약
-
RecvBuffer는 네트워크에서 조각나서 들어오는 데이터를 안전하게 누적, 처리, 정리하는 버퍼 클래스다.
-
읽기/쓰기 포인터를 분리함으로써 데이터 손실 없이 패킷 단위 처리를 구현할 수 있다.