728x90
반응형
기존의 프로젝트 코드는 TCP를 사용하여 네트워크 통신을 하는 경우,
상황을 고려하여 분할되어 전송된 패킷을 온전히 받지 못하는 문제가 있으므로
이를 수정하도록 하겠습니다.
이때 RecvBuffer와 SendBuffer는 설계구조에서 약간의 차이가 존재합니다.
RecvBuffer의 경우,
클라이언트(=유저)별로 보내는 데이터가 다르므로 Session 별로 다르게 생성하여 갖고 있습니다.
하지만 SendBuffer의 경우 다른데,
일단 크기가 일정하고 또한 SendBuffer의 경우
Session이 갖는 것이 아닌 Server가 직접 갖고 있게 됩니다.
이러한 이유는 외부에서 Send할 데이터를 만든 다음 해당 데이터를 내부로 넘기는 방식은
성능적인 문제를 발생시키므로 외부에서 Send를 처리하도록 하겠습니다.
1) RecvBuffer
public class RecvBuffer
{
ArraySegment<byte> _buffer;
int _readPos; // 서버가 클라이언트로부터 읽을 데이터의 시작위치
int _writePos; // 클라이언트가 쓸 데이터의 시작위치
// RecvBuffer를 생성
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); }
}
// RecvBuffer를 비워주는 함수
public void Clean()
{
int dataSize = DataSize;
// dataSize가 0이라면 클라이언트에서 보낸 데이터를 전부 처리하였으므로
// 데이터를 복사해놓을 필요없이 시작위치들만 0으로 초기화
if (dataSize == 0)
{
_readPos = _writePos = 0;
}
// 0이 아니라면 남은 데이터가 존재하므로 시작 위치로 복사
else
{
Array.Copy(_buffer.Array, _buffer.Offset + _readPos, _buffer.Array, _buffer.Offset, dataSize);
_readPos = 0;
_writePos = dataSize;
}
}
// 서버가 클라이언트로부터 데이터를 성공적으로 읽은 경우 호출
public bool OnRead(int numOfBytes)
{
if (numOfBytes > DataSize)
return false;
_readPos += numOfBytes;
return true;
}
// 클라이언트가 성공적으로 데이터의 쓴 경우 호출
public bool OnWrite(int numOfBytes)
{
if (numOfBytes > FreeSize)
return false;
_writePos += numOfBytes;
return true;
}
}
2) SendBuffer
// SendBuffer를 래핑하는 클래스
public class SendBufferHelper
{
// 멀티 네트워크에서 다른 스레드의 간섭을 막고자 ThreadLoacl로 사용
public static ThreadLocal<SendBuffer> CurrentBuffer = new ThreadLocal<SendBuffer>(() => { return null; });
public static int ChunkSize { get; set; } = 4096;
// SendBuffer에 보낼 메세지를 작성하는 함수
public static ArraySegment<byte> Open(int reserveSize)
{
// 아직, SendBuffer가 사용되지 않았다면 새로 만들어줌
if(CurrentBuffer.Value == null)
CurrentBuffer.Value = new SendBuffer(reserveSize);
// SendBuffer가 사용되었지만 남은 공간이 쓸 메세지보다 적다면, 쓸 메세지만큼의 SendBuffer를 생성
if (CurrentBuffer.Value.FreeSize < reserveSize)
CurrentBuffer.Value = new SendBuffer(reserveSize);
return CurrentBuffer.Value.Open(reserveSize);
}
// SendBuffer에 작성한 메세지를 보내는 함수
public static ArraySegment<byte> Close(int reserveSize)
{
return CurrentBuffer.Value.Close(reserveSize);
}
}
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 reservedSize)
{
// 쓸 메세지의 크기가 버퍼의 남은 공간보다 크다면 null을 리턴
if (reservedSize > FreeSize)
return null;
return new ArraySegment<byte>(_buffer, _usedSize, reservedSize);
}
// 서버에서 클라이언트로 전송할 메세지를 보내기 위해 버퍼를 닫아둠
public ArraySegment<byte> Close(int usedSize)
{
ArraySegment<byte> segment = new ArraySegment<byte>(_buffer, _usedSize, usedSize);
_usedSize += usedSize;
return segment;
}
}
SendBuffer의 경우, RecvBuffer와는 다르게 정리를 하는 함수가 따로 존재하지 않습니다.
그렇기에 SendBuffer는 일회용으로서 한번 Send한 후에 다시 새로 생성하도록 하겠습니다.
RecvBuffer와 SendBuffer를 추가하였으므로
기존의 Session과 Server 코드를 수정하도록 하겠습니다.
3) Session
public abstract class Session
{
.
.
.
// 새롭게 추가한 RecvBuffer
RecvBuffer _recvBuffer = new RecvBuffer(1024);
object _lock = new object();
Queue<ArraySegment<byte>> _sendQueue = new Queue<ArraySegment<byte>>();
List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
.
.
.
public void Send(ArraySegment<byte> sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pendingList.Count == 0)
RegisterSend();
}
}
.
.
.
// 클라이언트의 메세지를 받기 위해 비동기로 대기를 수행하는 RegisterRecv에서
// _recvBuffer를 사용하여 매번 새로운 버퍼를 만들지 않고 WritePos를 기준으로 클라이언트의 메세지를 저장함
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);
}
// 클라이언트의 메세지를 처리하는 함수 OnRecvCompleted에서는
// 클라이언트가 RecvBuffer에 작성하는 위치인 WritePos를 수정해줌
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
if (_recvArgs.BytesTransferred > 0 && _recvArgs.SocketError == SocketError.Success)
{
try
{
// 이때 클라이언트가 보낸 메세지가 버퍼의 범위를 넘어서면 연결 해제
if (_recvBuffer.OnWrite(args.BytesTransferred) == false)
{
Disconnect();
return;
}
// 이때 클라이언트가 보낸 메세지의 길이를 확인
// 길이가 0보다 작거나, RecvBuffer의 데이터 사이즈를 넘어서면 연결 해제
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($"From Client Recv Error : {e.ToString()}");
}
}
else
{
Disconnect();
}
}
// RegisterSend도 바꾼 SendBuffer에 맞춰 수정
void RegisterSend()
{
while (_sendQueue.Count > 0)
{
ArraySegment<byte> buff = _sendQueue.Dequeue();
_pendingList.Add(buff);
}
_sendArgs.BufferList = _pendingList;
bool pending = _socket.SendAsync(_sendArgs);
if (pending == false)
OnSendCompleted(null, _sendArgs);
}
// 마찬사지로 수정
void OnSendCompleted(object handler, SocketAsyncEventArgs args)
{
lock (_lock)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
_sendArgs.BufferList = null;
_pendingList.Clear();
OnSend(_sendArgs.BytesTransferred);
if (_sendQueue.Count > 0)
RegisterSend();
}
catch (Exception e)
{
Console.WriteLine($"From Server Send Error : {e.ToString()}");
}
}
else
{
Disconnect();
}
}
}
}
4) Server
class Knight
{
public int hp;
public int attack;
}
class GameSession : Session
{
// 연결되는 경우 호출
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"[Connect from] : {endPoint}");
Knight knight = new Knight() { hp = 10, attack = 100};
ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
byte[] buffer1 = BitConverter.GetBytes(knight.hp);
byte[] buffer2 = BitConverter.GetBytes(knight.attack);
Array.Copy(buffer1, 0, openSegment.Array, openSegment.Offset, buffer1.Length);
Array.Copy(buffer2, 0, openSegment.Array, openSegment.Offset + buffer1.Length, buffer2.Length);
ArraySegment<byte> sendBuffer = SendBufferHelper.Close(buffer1.Length + buffer2.Length);
Send(sendBuffer);
Thread.Sleep(1000);
Disconnect();
}
.
.
.
}
class Program
{
.
.
.
}
SendBuffer를 OnConnected에서 위와 같이 외부에서 생성하도록 하겠습니다.
이제 최종 결과를 확인해보겠습니다.
위와 같이 네트워크 통신이 이뤄지는 것을 확인할 수 있습니다.
728x90
반응형
'C#' 카테고리의 다른 글
직렬화(Serialization) 1 (1) | 2025.01.01 |
---|---|
PacketSession (0) | 2024.12.31 |
TCP - UDP의 이해 (0) | 2024.12.25 |
Connect를 위한 Connector 생성 (0) | 2024.12.25 |
Session_2 (0) | 2024.12.24 |