네트워크 프로그래밍
지난 시간에는 TCP와 UDP 각각의 개념들에 대해서 알아보고, 또 '차이점은 무엇이 있는가?'에 대해서 학습하였다. 오늘은 이전 시간에 학습한 것과 마찬가지로 TCP 특성 중 현재 처리 가능한 패킷만 받고, 나머지는 나중에 처리하는 흐름제어 및 혼잡제어 기능을 구현하여 어떻게 처리하는지 알아보며 구현한다.
기존 코드의 문제점
기존 코드는 시작 시 SetBuffer의 위치를 0으로 설정해놓았기 때문에 TCP 특성상 100byte를 보내어도, 80byte만 받았을 경우 문제가 발생한다. 왜냐하면 80byte의 위치부터 나머지 20byte을 다 받은 후에 처리를 해야하기 때문이다. 따라서 현재 얼마나 받았고, 받아왔는지를 처리하며 표시할 방법이 필요하다.
RecvBuffer
클래스 제작
먼저 서버 코어에 새로운 클래스인 RecvBuffer.cs 스크립트를 생성하여 만들어주자. _buffer의 타입형을 byte로 선언하여 사용해도 상관없지만, 나중에 혹시 큰 배열의 일부만을 사용할 수 있으므로 ArraySegment 타입을 이용하여 사용한다.
public class RecvBuffer
{
ArraySegment<byte> _buffer;
public RecvBuffer(int bufferSize)
{
_buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
}
}
또한 현재 byte를 어디까지 받았고, 또 앞으로 얼마나 더 남았는지를 파악하기 위한 변수와 프로퍼티를 만들어준다.
public class 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; } }
}
writerPos
받아온 byte의 위치를 표시해주는 변수이다. 100byte 중 80byte를 받았다면 현재 위치는 80에 있는 상태이다.
readerPos
받아온 byte의 크기에 따라 작업을 처리해주기 위한 변수이다. 100byte 중 80byte를 받았을 경우 남은 byte를 대기하다 100byte가 모두 채워졌을 때 하나의 패킷이 채워진 것을 확인한다.
DataSize는 버퍼의 유효 사이즈를 의미하며, 현재 데이터가 얼마나 쌓였는지를 체크한다. FreeSize는 버퍼의 남은 공간을 의미한다.
ReadSegment & WriteSegment
// 유효 범위의 Segment를 어디부터 데이터를 읽으면 되는가?
public ArraySegment<byte> ReadSegment
{
get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _readPos, DataSize); }
}
public ArraySegment<byte> WriteSegment
{
get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _writePos, FreeSize); }
}
ReadSegment는 유효 범위의 Segment를 어디부터 데이터를 읽으면 되는지 확인하기 위한 함수이며, WriteSegment는 버퍼의 남은 공간 크기를 나타낸다.
Clean
// 중간 정리를 하며, R/W 커서를 맨 앞으로 당겨준다.
public void Clean()
{
int dataSize = DataSize;
if (dataSize == 0)
{
// 남은 데이터가 없어 복사하지 않고 커서 위치만 리셋한다. (=클라이언트에서 보낸 데이트를 모두 처리한 상태)
_readPos = _writePos = 0;
}
else
{
// 남은 데이터가 있으면 시작 위치로 복사한다.
Array.Copy(_buffer.Array, _buffer.Offset + _readPos, _buffer.Array, _buffer.Offset, dataSize);
_readPos = 0;
_writePos = dataSize;
}
}
Clean은 Read/Write를 하며 Cursor의 값을 계속 이동시키다 보면, 나중에는 buffer의 크기보다 커지는 경우가 생길 수 있으므로, 중간중간 커서의 위치를 초기화시키기 위해 필요한 함수이다.
dataSize가 0인 경우와 그렇지 않은 경우가 존재하는데 0인 경우는 현재 Read, Write Cursor의 위치가 같은 위치에 있기 때문에 두 값을 모두 0으로 이동시켜 주고, 그렇지 않은 경우는 Array.Copy()를 이용하여 buffer 자체를 현재 readPos에 있는 위치만큼에서 Offset의 위치까지로 옮겨주는 기능을 담당한다.
OnRead & OnWrite
// 데이터를 성공적으로 처리했을 경우 OnRead를 호출하여 R 커서 이동
public bool OnRead(int numOfBytes)
{
if (numOfBytes > DataSize)
{
return false;
}
_readPos += numOfBytes;
return true;
}
// 클라이언트에서 데이트럴 보낸 상황에서 성공적으로 처리했을 경우 OnWrite를 호출하여 W 커서 이동
public bool OnWrite(int numOfBytes)
{
if (numOfBytes > FreeSize)
{
return false;
}
_writePos += numOfBytes;
return true;
}
두 함수는 Read, Write가 성공적으로 실행된 경우, 해당 함수를 호출하여 커서의 위치를 옮기는 역할을 맡고 있다.
📜 Session.Cs
기존에 Start 함수에서 사용하던 SetBuffer() 메서드는 더 이상 사용하지 않고, 새로 만들어준 RecvBuffer를 사용하여 마찬가지로 처음 값을 1024로 세팅한다.
이 후, 작업이 시작되는 Register에서 값을 초기화시켜준 후, Wirte Cursor 부터 남은 범위까지의 Segment인 WriteSegment를 가져와 SetBuffer로 설정하여 준다.
void RegisterRecv()
{
// 커서가 뒤로 이동하는 것을 방지
_recvBuffer.Clean();
// 유효 범위 설정
ArraySegment<byte> segment = _recvBuffer.WriteSegment;
_recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count);
// 전역변수로 선언한 _recvArgs의 값을 이용한다.
bool pending = _socket.ReceiveAsync(_recvArgs);
if (pending == false)
{
OnRecvCompleted(null, _recvArgs);
}
OnRecvCompleted
기존의 OnRecvCompleted는 OnRecv를 통해 args로 받아온 Segment를 string으로 단순 변환하여 출력하는 작업을 하고 있었다.
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
// Write 커서 이동
if (_recvBuffer.OnWrite(args.BytesTransferred) == false)
{
Disconnect();
return;
}
// 컨텐츠 쪽으로 데이터를 넘겨주고 얼마나 처리했는지 받는다.
int processLen = OnRecv(_recvBuffer.ReadSegment);
if (processLen < 0 || _recvBuffer.DataSize < processLen)
{
Disconnect();
return;
}
// 처리한 Read 커서 이동
if (_recvBuffer.OnRead(processLen) == false)
{
Disconnect();
return;
}
RegisterRecv();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompleted Fail {e}");
}
}
else
{
Disconnect();
}
}
그러나 새로운 코드는 Write Cursor의 이동을 args.BytesTransferred 만큼 처리한다. 단 예외를 위한 조건문들이 조금 추가 한다. OnRecv에서 작업했던 buffer의 Count 만큼을 뱉어주는 방법으로 변경하여 수정한다.마지막으로 넘겨준 값을 int형으로 받은 뒤, 예외처리를 진행하고 문제가 없다면 이를 넘기어 ReadPos의 커서 위치 또한 옮겨주면 끝이다.
영상도 긴데, 블로그 글까지 같이 작성하려고 하니 시간이 배가 소요되는 것 같다. 그래도 나의 커리어를 위해 조바심 내지말고 천천히 꾸준히 작성하자. 😎
'공부 > 인프런 - Rookiss' 카테고리의 다른 글
Part 4-3-13. 네트워크 프로그래밍 : PacketSession (0) | 2023.11.08 |
---|---|
Part 4-3-12. 네트워크 프로그래밍 : SendBuffer (0) | 2023.11.05 |
Part 4-3-10. 네트워크 프로그래밍 : TCP/UDP (0) | 2023.10.31 |
Part 4-3-9. 네트워크 프로그래밍 : Connector (1) | 2023.10.30 |
Part 4-3-8. 네트워크 프로그래밍 : Session #4 (Event Handler) (0) | 2023.10.29 |