[소켓 프로그래밍 - Session]에서 Send와 Receive를 구현할 때 버퍼의 사이즈를 1024
로 고정해 구현했는데 실제 서버에는 가변길이의 버퍼가 필요할 것이다.
예를들어, 8바이트 데이터를 받아도 실제 처리되는 바이트의 길이는 1024가 되어 여러번의 연산이 진행될수록 불필요한 시간이 더 소요되는 상황이 벌어질 수 있다.
Buffer의 전체적인 아이디어는 다음과 같다.
C#에서 Buffer를 구현하기 위해 byte[]
대신 ArraySegment<byte>
를 사용하는데 배열의 부분배열을 다루는데 더 적합하다. 부분배열로 새로운 배열을 생성하는 것이 아니라 기존배열의 부분집합을 참조하는 형식으로 작동시킴으로 메모리할당에 대한 추가적인 연산을 제거할 수 있다.
ArraySegment의 주요 속성은 다음과 같다.
ReceiveBuffer를 구현하기 위한 아이디어는 다음과 같다.
Buffer는 Read Cursor
와 Write Cursor
로 현재 Receive로 할당된 부분과 사용가능한 부분을 구분한다.
ArraySegment의 할당된 부분은 Write Cursor
왼쪽 빨간색 영역이며 오른쪽으로 옮기고 오른쪽 초록색 영역은 이후에 추가적인 Receive요청에 대한 Buffer를 나타낸다.
Receive작업이 끝나면 Read Cursor
를 오른쪽으로 옮겨 할당받은 Buffer를 반환한다.
필요가 있을 경우, [Read Cursor
, Write Cursor
]에 있는 데이터들을 왼쪽 정렬해 남은 Buffer를 최적화한다.
Receive Buffer를 사용하기 위해서 Receive Buffer는 다음과 같은 기능을 가지고 있어야 한다.
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; } }
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); }
}
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;
}
}
public bool OnRead(int numOfByte)
{
if (numOfByte > DataSize)
return false;
_readPos += numOfByte;
return true;
}
public bool OnWrite(int numOfByte)
{
if (numOfByte > FreeSize)
return false;
_writePos += numOfByte;
return true;
}
}
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; } }
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); }
}
위의 아이디어를 구현하기 위한 변수들은 다음과 같다.
Read Cursor
, Write Cursor
에 해당하는 변수위 코드에서 사용하는 ArraySegment
의 생성매개변수는 원본 Array
, 시작 Index
, 길이
로 Offset + Cursor
는 Cursor가 가리키는 Index이다.
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;
}
}
현재 사용하고 있는 영역을 왼쪽으로 정렬시키는 함수로 아래 그림과 같은 기능을 하는 함수이다.
이를 구현하기 위해 두가지 경우를 생각해야 하는데사용하고 있는 영역이 없는 경우와
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;
}
영역을 복사하고 옮기는 과정에서 Array.Copy()를 사용하였으며 커서의 상태는 Read Cursor
는 0
, Write Curosr
는 DataSize
가 되어야 한다.
public bool OnRead(int numOfByte)
{
if (numOfByte > DataSize)
return false;
_readPos += numOfByte;
return true;
}
public bool OnWrite(int numOfByte)
{
if (numOfByte > FreeSize)
return false;
_writePos += numOfByte;
return true;
}
RecvBuffer를 사용하는 Class들이 Cursor를 옮기기 위한 함수로 각각 Read Cursor
, Write Cursor
를 옮긴다. 몇 Byte를 사용했는지 입력하면 예외상황을 제외하고 bool형식으로 결과를 반환한다.
위 내용을 적용한 Session의 Recv코드는 아래와 같다. 기존에는 Session의 Buffer의 Start에 1024크기로 고정하였지만 적용을 한 코드는 Receive연산 전후에 Buffer를 수정하는 형식으로 구현할 수 있다.
void RegisterRecv()
{
_recvBuffer.Clean();
ArraySegment<byte> segment = _recvBuffer.WriteSegment;
_recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count);
bool pending = _socket.ReceiveAsync(_recvArgs);
if (pending == false)
OnRecvCompleted(null, _recvArgs);
}
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 = OnRecv(_recvBuffer.ReadSegment);
if (processLen < 0 || _recvBuffer.DataSize < processLen)
{
Disconnect();
return;
}
if(_recvBuffer.OnRead(processLen) == false)
{
Disconnect();
return;
}
RegisterRecv();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompleted Failed {e}");
}
}
else
{
Disconnect();
}
}
SendBuffer는 ReceiveBuffer와 다르게 기존의 Buffer가 Session내부 변수로 있지 않고 Send를 호출할 때 Buffer를 매개변수로 넣는 방식을 사용한다.
이러한 사실을 반영한 SendBuffer를 구현하기 위한 아이디어는 다음과 같다.
Buffer는 Used Cursor
로 현재 할당된 부분의 크기를 나타낸다.
Buffer를 얻기 전에 최대로 필요한 크기만큼 Buffer를 예약한다.
Send에 사용한 실제 데이터만큼 Buffer를 사용한다.
남은 부분을 SendBuffer Class에 돌려주고 Cursor를 이동시킨다.
물론, Receive와 같이 Session에 SendBuffer를 사용할수는 있지만 하나의 요청에 대해서 byte[]를 복사해 전송하는 방식은 전체 성능의 저하를 일으킬수 있다. 예를들어, 100명의 이동정보를 100명에게 보낸다면 총 1만번의 Array복사 작업이 필요할 수 있다.
public class SendBuffer
{
byte[] _buffer;
int _usedSize = 0;
public int FreeSize { get { return _buffer.Length - _usedSize; } }
public SendBuffer(int chunkSize)
{
_buffer = new byte[chunkSize];
}
public ArraySegment<byte> Open(int reserveSize)
{
if (reserveSize > FreeSize)
return null;
return new ArraySegment<byte>(_buffer, _usedSize, reserveSize);
}
public ArraySegment<byte> Close(int usedSize)
{
ArraySegment<byte> segment = new ArraySegment<byte>(_buffer, _usedSize, usedSize);
_usedSize += usedSize;
return segment;
}
}
byte[] _buffer;
int _usedSize = 0;
public int FreeSize { get { return _buffer.Length - _usedSize; } }
public SendBuffer(int chunkSize)
{
_buffer = new byte[chunkSize];
}
위의 아이디어를 구현하기 위한 변수들은 다음과 같다.
Used Cursor
에 해당하는 변수 public ArraySegment<byte> Open(int reserveSize)
{
if (reserveSize > FreeSize)
return null;
return new ArraySegment<byte>(_buffer, _usedSize, reserveSize);
}
특정 크기의 Buffer를 예약하는 함수로 Open을 호출하는 쓰레드는 Send하고자하는 데이터의 최대 예상 크기를 매개변수로 넘겨 Buffer를 예약한다.
public ArraySegment<byte> Close(int usedSize)
{
ArraySegment<byte> segment = new ArraySegment<byte>(_buffer, _usedSize, usedSize);
_usedSize += usedSize;
return segment;
}
실제 사용한 만큼의 Buffer를 차지하고 남은 예약공간을 반환하는 함수로 실제 부분배열을 반환받으며 Cursor를 이동시킨다.
SendBuffer를 전역으로 사용할 수도 있지만 멀티쓰레드 환경에서 쓰레드마다 각각의 SendBufffer를 가지고 있는 구조를 위해 Helper클래스를 사용할 수 있다.
public class SendBufferHelper
{
public static ThreadLocal<SendBuffer> CurrentBuffer = new ThreadLocal<SendBuffer>(() => { return null; });
public static int ChunkSize { get; set; } = 4096 * 100;
public static ArraySegment<byte> Open(int reserveSize)
{
if (CurrentBuffer.Value == null)
CurrentBuffer.Value = new SendBuffer(ChunkSize);
if (CurrentBuffer.Value.FreeSize < reserveSize)
CurrentBuffer.Value = new SendBuffer(ChunkSize);
return CurrentBuffer.Value.Open(reserveSize);
}
public static ArraySegment<byte> Close(int usedSize)
{
return CurrentBuffer.Value.Close(usedSize);
}
}
SendBufferHelper의 모든 변수와 메소드들은 public static으로 SendBufferHelper.Open()과 방식으로 사용 가능하다. SendBufferHelper는 SendBuffer를 ThreadLocal로 사용하는 구조로 쓰레드마다 하나의 SendBuffer를 가질 수 있도록 한다.
public static ArraySegment<byte> Open(int reserveSize)
{
if (CurrentBuffer.Value == null)
CurrentBuffer.Value = new SendBuffer(ChunkSize);
if (CurrentBuffer.Value.FreeSize < reserveSize)
CurrentBuffer.Value = new SendBuffer(ChunkSize);
return CurrentBuffer.Value.Open(reserveSize);
}
SendBuffer가 아직 생성되지 않은경우와 예약하고자하는 크기를 충족못할 경우 새로운 SendBuffer를 생성해 SendBuffer.Open을 제공하는 함수이다.
public static ArraySegment<byte> Close(int usedSize)
{
return CurrentBuffer.Value.Close(usedSize);
}
Close는 Open과 다르게 먼저 처리해줄 작업이 없기 때문에 바로 SendBuffer.Close를 제공한다.
정리가 잘 된 글이네요. 도움이 됐습니다.