원래 코드
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
public void Start(Socket socket)
{
_recvArgs.SetBuffer(new byte[1024],0,1024);
RegisterRecv();
}
setBuffer를 한 다음 어떤 변화도 주지 않았다.
그러므로 _socket.ReceiveAsync(_recvArgs)에서 우리가 건네주는 버퍼는 위의 Setbuffer이다.
void RegisterRecv()
{
bool pending = _socket.ReceiveAsync(_recvArgs);
}
TCP
우리가 저번에 TCP에대해 알아보았는데 클라이언트 쪽에서 100바이트를 보낸다고 가정한다면 무조건 100바이트가 온다는 보장이 없다. 클라이언트에서 보낸 패킷이 100바이트여서 100바이트가 와야지만 서버에서 분석한 후 처리할 수 있는데 불구하고 TCP 특성 때문에 80바이트만 올수도 있다.
그러면 어떻게 처리해야 할까?
80바이트만 온 경우 Receive버퍼에 저장하고 있다가 나중에 20바이트가 오면 Offset의 변경을 통해 조립해가지고 한번에 처리할 수 있도록 한다. 이를 ArraySegment를 사용해서 데이터를 저장하고 조립하는 것이 효과적이다.
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
OnReceive(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
RegisterRecv();
}
... 생략
RecvBuffer
01 원본 배열의 일부분에 대한 참조 배열 설정
원본 배열 : new byte[bufferSize]
참조 배열 : _buffer
ArraySegment<byte> _buffer;
public RecvBuffer(int bufferSize)
{
_buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
}
ArraySegment 알아보러 가기
02 readPos, writePos
readPos : 서버가 클라이언트로부터 데이터를 읽고 처리하면 _readPos는 읽은 후의 위치로 변경된다.
writePos : 클라이언트에서 데이터를 전송하면 _writePos는 데이터가 저장된 후의 위치로 변경된다.
일반적인 상황
1) 초기 상태 버퍼
2) 5byte 패킷을 받았을 때
클라이언트가 서버로 5byte 데이터를 전송한다. 이 때 _writePos를 5칸 뒤로 이동시켜 새로운 데이터를 저장할 위치를 나타낸다.
3) 컨텐츠 처리
서버는 이 부분의 데이터를 컨텐츠 처리를 위해 사용한다. 컨텐츠 쪽에서 패킷 처리 크기(Packet Processing Size)에 따라 처리여부가 결정된다.
[패킷 처리 크기(Packet Processing Size)는 데이터를 처리하는 단위인 각각의 패킷이 가지는 크기이다. ]
패킷 처리 크기가 5인 경우 처리하고 이동한다.
패킷 처리 크기가 8인 경우 데이터를 완전히 처리하는데 필요한 정보가 부족하므로 다음 패킷을 기다린다.
밀리는 상황
1) 초기 상태 버퍼
2) 패킷 처리 크기가 2byte이라 가정하고 3byte 패킷을 받은 경우
클라이언트가 서버로 3byte 데이터를 전송한다. 이 때 _writePos를 3칸 뒤로 이동시켜 새로운 데이터를 저장할 위치를 나타낸다.
3) 컨텐츠 처리
서버는 이 부분의 데이터를 컨텐츠 처리를 위해 사용한다.
패킷 처리 크기가 2byte이므로 서버는 데이터를 처리하고 남은 위치를 읽기 위해 _readPos를 이동시킨다.
하지만 이렇게 계속해서 뒤로 밀리면 버퍼 공간이 부족하게되는 문제가 발생한다.
4) 위치 조정
서버가 처리 가능한 데이터만을 유지하기 위해 _writePos와 _readPos를 앞으로 이동시킨다. 이를 통해 처리가 완료된 데이터는 유효하지않은 데이터로 간주되며 다음으로 처리해야할 데이터만을 유지시킬 수 있다.
이러한 방식으로 서버는 클라이언트로부터 수신한 데이터를 순차적으로 처리한다. 데이터가 도착하는 상황에 맞게 _readPos와 _writePos를 사용하면 버퍼 공간을 효율적으로 사용할 수 있다.
03 DataSize
얼마나 데이터가 쌓여있는지 ( w - r )
public int DataSize { get { return _writePos - _readPos; } }
04 FreeSize
버퍼에 남은 공간 (_buffer.Count - w)
public int FreeSize { get { return _buffer.Count - _writePos; } }
05 DataSegment
현재 버퍼에서 어디서부터 읽어야할 데이터의 유효 범위
public ArraySegment<byte> DataSegment
{
get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _readPos, DataSize); }
}
06 RecvSegment
다음 Receive호출할 때 어디서부터 어디까지가 유효범위인지 나타냄
public ArraySegment<byte> RecvSegment
{
get { return new ArraySegment<byte>(_buffer.Array,_buffer.Offset+_writePos,FreeSize); }
}
07 Clean
밀리는 상황을 방지하기 위해 중간중간 정리해주어야 한다.
1) r이랑 w가 겹치는 경우
데이터를 다 처리한 상황이므로 복사가 필요 없고 위치만 리셋시켜준다.
if(dataSize == 0)
{
_readPos = _writePos = 0;
}
2) r이랑 w가 겹치지 않는 경우
Array.Copy(_buffer.Array, _buffer.Offset + _readPos, _buffer.Array, _buffer.Offset,dataSize);
_readPos = 0;
_writePos = dataSize;
과정 설명
1) 초기 설정
2) 데이터의 유효 범위인 3byte는 건드리면 안된다.
3) 데이터를 복사해서 시작위치로 가져온 후 r을 시작위치로 가져온다.
Array.Copy(_buffer.Array, _buffer.Offset + _readPos, _buffer.Array, _buffer.Offset,dataSize);
_readPos = 0;
Array.Copy
public static void Copy(Array sourceArray, int sourceIndex, Array destinationArray, int destinationIndex, int length);
매개변수
sourceArray : 복사할 원본 배열
sourceIndex : 원본 배열에서 복사 시작 위치
destinationArray : 복사할 대상 배열
destinationIndex : 대상 배열에서 복사된 데이터 저장 시작 위치
length : 복사할 데이터 길이
4) 마지막으로 w를 가져온다.
_writePos = dataSize;
08 OnRead
데이터를 가공해서 처리한 후 성공적으로 처리 했다면 _readPos 커서 위치를 변경한다.
public bool OnRead(int numofBytes)
{
if (numofBytes > DataSize) return false;
_readPos += numofBytes;
return true;
}
numofBytes : 처리한 데이터 바이트의 수
if (numofBytes > DataSize) return false;
처리한 데이터가 현재 버퍼에 있는 유효한 데이터 크기보다 작아야 한다.
09 OnWrite
클라이언트에서 데이터를 전송하여 Receive했을 때 _writePos 커서 위치를 변경한다.
public bool OnWrite(int numOfBytes)
{
if (numOfBytes > FreeSize) return false;
_writePos += numOfBytes;
return true;
}
numofBytes : 클라이언트가 서버로 전송한 데이터의 크기
if (numOfBytes > FreeSize) return false;
데이터의 크기가 버퍼에 남은 공간 크기보다 작아야한다.
Session 수정
01 RecvBuffer 객체 생성하기
RecvBuffer _recvBuffer = new RecvBuffer(1024);
02 RegisterRecv 수정
void RegisterRecv()
{
_recvBuffer.Clean();
ArraySegment<Byte> segment = _recvBuffer.WriteSegment;
_recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count);
}
1) 버퍼 초기화
_recvBuffer.Clean();
2) 데이터 저장을 위한 유효범위 가져오기
ArraySegment<Byte> segment = _recvBuffer.WriteSegment;
3) 버퍼 설정
_recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count);
03 OnRecvCompleted 수정
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
if(_recvBuffer.OnWrite(args.BytesTransferred) == false)
{
Disconnect();
return;
}
int processLen =OnReceive(_recvBuffer.ReadSegment);
if(processLen < 0 || _recvBuffer.DataSize < processLen)
{
Disconnect();
return;
}
// Read 커서 이동
if(_recvBuffer.OnRead(processLen) == false)
{
Disconnect();
return;
}
RegisterRecv();
}
... 생략
1) Write 커서 이동
데이터의 크기가 버퍼에 남은 공간 크기보다 작은지 확인하고 만약 크다면 해당 소켓의 연결을 종료한다.
if(_recvBuffer.OnWrite(args.BytesTransferred) == false)
{
Disconnect();
return;
}
args.BytesTransferred : SocketAsyncEventArgs객체를 통해 수신된 데이터의 크기이다.
2) 컨텐츠 쪽으로 데이터를 념겨주고 얼마나 처리했는지 받는다.
void OnReceive에서 int OnReceive 수정을 통해 처리된 데이터의 양을반환한다.
public override int OnReceive(ArraySegment<byte> buffer)
{
return buffer.Count;
}
int processLen = OnReceive(_recvBuffer.ReadSegment);
처리된 데이터의 양이 음수이거나 데이터의 길이가 객체의 버퍼 크기를 초과한 경우 소켓의 연결을 종료한다.
if(processLen < 0 || _recvBuffer.DataSize < processLen)
{
Disconnect();
return;
}
3) Read 커서 이동
처리한 데이터가 현재 버퍼에 있는 유효한 데이터 크기보다 크기 때문에 데이터를 가공해서 처리가 성공적으로 이루어지지 않았다. 해당 소켓의 연결을 종료한다
if(_recvBuffer.OnRead(processLen) == false)
{
Disconnect();
return;
}
참고 : 본 내용은 MMORPG PART4 강의를 수강하여 작성하였습니다.
https://www.inflearn.com/course/%EC%9C%A0%EB%8B%88%ED%8B%B0-mmorpg-%EA%B0%9C%EB%B0%9C-part4
'cs공부 > 네트워크프로그래밍' 카테고리의 다른 글
네트워크 프로그래밍 - PacketSession (0) | 2023.07.31 |
---|---|
네트워크프로그래밍 - SendBuffer 개선하기 (0) | 2023.07.22 |
네트워크프로그래밍 - TCP vs UDP (0) | 2023.07.19 |
네트워크프로그래밍 - Session이란 (0) | 2023.07.15 |
네트워크프로그래밍 - C# Non - blocking을 사용한 Server 소켓프로그래밍의 Connector (0) | 2023.07.15 |
댓글