서버/서버 이론
RecvBuffer 개선
잼잼재미
2025. 4. 24. 00:03
RecvBuffer 개선
TCP에서 클라이언트에서 100바이트를 보낸다고 가정하면 TCP의 특성 상, 무조건 100바이트가 온다는 보장이 없다. 만약, 80바이트만 도착했다면, RecvBuffer 에서 저장하고 있다가 나중에 20바이트가 도착하면 합쳐서 한번에 처리할 수 있도록 해야 한다. 우선, ServerCore에 RecvBuffer class를 추가하자.
RecvBuffer
using System;
using System.Collections.Generic;
using System.Text;
namespace ServerCore
{
public class RecvBuffer
{
// 얘가 버퍼가 되는 거. 10바이트짜리 배열이라고 가정을 하고 진행을 한다.
// [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
ArraySegment<byte> _buffer; // 엄청나게 큰 바이트 배열에서 부분적으로 잘라서 사용하고 싶을 수 있으니까, byte배열 대신 ArraySegment를 들고 있게 했다.
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 // 데이터 유효 범위의 세그먼트로 어디부터 데이터를 읽으면 되냐 요청, 이름이 마음에 안들면 DataSegment로 해도 된다.
{
get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _readPos, DataSize); }
// ArraySegment의 새 인스턴스를 초기화합니다.<T> 지정된 배열에 있는 요소의 지정된 범위를 구분하는 structure입니다.
// 버퍼의 시작위치, 시작할 수 있는 오프셋, 처리되지 않은 데이터 크기를 넣어준다. 이게 데이터의 범위라고 볼 수 있다.
}
public ArraySegment<byte> WriteSegment // 다음에 리시브를 할 때 어디부터 어디가 유효범위인지, DataSegment라 해도 된다.
{
get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _writePos, FreeSize); }
// 버퍼의 시작 위치, w의 위치, 크기는 FreSize를 넣어주면 된다.
}
public void Clean() // 정리를 안하면 r,w가 버퍼 끝까지 가기 때문에 한번씩 처음으로 당겨줄 필요가 있다. 버퍼 고갈 방지
{
int dataSize = DataSize;
if (dataSize == 0) // r과 w가 겹치는 상태, 클라에서 보낸 데이터를 모두 처리한 상태
{
// 남은 데이터가 없으면 복사하지 않고 커서 위치만 리셋
_readPos = _writePos = 0;
}
else
{
// 남은 찌끄레기가 있으면 시작 위치로 복사
Array.Copy(_buffer.Array, _buffer.Offset + _readPos, _buffer.Array, _buffer.Offset, dataSize);
//복사할 소스, 소스의 오프셋, 목적지, 목적지의 첫 위치, 크기
_readPos = 0; // 데이터를 시작 위치로 보냈으니 초기화
_writePos = dataSize; // 데이터 사이즈 만큼에 위치
}
}
public bool OnRead(int numOfBytes) // 컨텐츠 코드에서 데이터를 가공해서 처리를 할건데 성공적으로 처리했으면 OnRead를 호출해서 커서 위치를 이동해준다.
{
if (numOfBytes > DataSize) // numOfBytes만큼을 처리 했다 하는데 이게 DataSize보다 크면 문제있다는 것이니
return false;
_readPos += numOfBytes;
return true;
}
public bool OnWrite(int numOfByte) // 클라에서 데이터를 싸줘가지고 recive를 했을 때 그 때 write 커서를 이동시켜 주는 게 되는 거다.
{
if (numOfByte > FreeSize) // FreeSize보다 많이 받으면 말이 안될거야.
return false;
_writePos += numOfByte;
return true;
}
}
}
Session 수정
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace ServerCore
{
public abstract class Session
{
Socket _socket;
int _disconnect = 0;
RecvBuffer _recvBuffer = new RecvBuffer(1024);
object _lock = new object();
Queue<byte[]> _sendQueue = new Queue<byte[]>();
List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();
public abstract void OnConnected(EndPoint endPoint);
public abstract int OnRecv(ArraySegment<byte> buffer);
public abstract void OnSend(int numOfBytes);
public abstract void OnDisconnected(EndPoint endPoint);
public void Start(Socket socket)
{
_socket = socket;
_recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
RegisterRecv();
}
public void Send(byte[] sendBuff)
{
lock (_lock)
{
_sendQueue.Enqueue(sendBuff);
if (_pendingList.Count == 0)
RegisterSend();
}
}
void RegisterSend()
{
while (_sendQueue.Count > 0)
{
byte[] buff = _sendQueue.Dequeue();
_pendingList.Add(new ArraySegment<byte>(buff, 0, buff.Length));
}
_sendArgs.BufferList = _pendingList;
bool pending = _socket.SendAsync(_sendArgs);
if (pending == false)
OnSendCompleted(null, _sendArgs);
}
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
lock (_lock)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
_sendArgs.BufferList = null; // 더이상 굳이 pendingList를 갖고 있을 필요 없으니까
_pendingList.Clear(); // bool 역할을 얘가 대신 해주는 거.
OnSend(_sendArgs.BytesTransferred);
if (_sendQueue.Count > 0)
RegisterSend();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
}
public void Disconnect()
{
if (Interlocked.Exchange(ref _disconnect, 1) == 1)
return;
OnDisconnected(_socket.RemoteEndPoint);
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
#region 네트워크 통신
void RegisterRecv()
{
_recvBuffer.Clean(); // 혹시라도 커서가 너무 뒤로 이동하는 것을 방지한다.
ArraySegment<byte> segment = _recvBuffer.WriteSegment; // 버퍼에서 다음으로 데이터를 받을 공간을 WriteSegment로 관리하고 있었어.
_recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count); // segment.Count가 애당초 freeSize였어.
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) // 모든 게 잘 처리 됐다는 뜻
{
// TODO
try
{
// Write 커서 이동
if (_recvBuffer.OnWrite(args.BytesTransferred) == false)
{
Disconnect();
return;
}
// 컨텐츠 쪽으로 데이터를 넘겨주고 얼마나 처리했는지 받는다.
int processLen = OnRecv(_recvBuffer.ReadSegment);
if (processLen < 0 || _recvBuffer.DataSize < processLen) // 혹시 컨텐츠 단에서 이상한 값으로 넣어줘서 처리가 안됐거나, recvBuffer보다 처리된 데이터 사이즈가 크면 이상한 거니 체크
{
Disconnect();
return;
}// 여기까지 했으면 데이터를 처리 했거나 보류를 했거나 한 상태가 될건데 이제 Read커서를 이동 시키
// Read 커서 이동
if (_recvBuffer.OnRead(processLen) == false)
{
Disconnect();
return;
}
RegisterRecv();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompletedFailed {e}");
}
}
else
{
Disconnect();
}
}
#endregion
}
}
DummyClient 수정
using ServerCore;
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace DummyClient
{
class GameSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
// 보낸다
for (int i = 0; i < 5; i++)
{
byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello World! {i}");
Send(sendBuff);
}
}
public override void OnDisconnected(EndPoint endPoint)
{
Console.WriteLine($"OnDisconnected : {endPoint}");
}
public override int OnRecv(ArraySegment<byte> buffer)
{
string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
Console.WriteLine($"[From Server] {recvData}");
return buffer.Count;
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes: {numOfBytes}");
}
}
internal class Program
{
static void Main(string[] args)
{
// DNS ( Domain Name System )
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
Connector connector = new Connector();
connector.Connect(endPoint, () => { return new GameSession(); });
while (true)
{
try
{
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
Thread.Sleep(100);
}
}
}
}
Server 수정
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ServerCore;
namespace Server
{
class GameSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"OnConnected : {endPoint}");
byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !!");
Send(sendBuff);
Thread.Sleep(5000);
Disconnect();
}
public override void OnDisconnected(EndPoint endPoint)
{
Console.WriteLine($"OnDisconnected : {endPoint}");
}
public override int OnRecv(ArraySegment<byte> buffer)
{
string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
Console.WriteLine($"[FromClient] {recvData}");
return buffer.Count;
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"Transferred bytes: {numOfBytes}");
}
}
class Program
{
static Listener _listener = new Listener();
static void Main(string[] args)
{
// DNS ( Domain Name System )
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipAddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
// ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호
//_listener.Init(endPoint, OnAcceptHandler);
_listener.Init(endPoint, () => { return new GameSession(); });
Console.WriteLine("Listening...");
while (true)
{
;
}
}
}
}