- Send,ReceiveBuffer를 사용.
- 기초적인 Packet을 만들어 테스트 하기.
1.ReceiveBuffer
RecvBuffer.cs
🕵️ DataSize,FreeSize
1. DataSize = 유효한 데이터 범위
2. FreeSize = 사용 가능한 ReadBuffer 남은 사이즈.
🕵️ Readsegment()
1. 유요한 데이터 범위를 ArraySegment<byte>로 반환.
🕵️ WriteSegment()
1. 사용가능한 범위를 ArraySegment<byte>로 반환.
🕵️ Clean()
1. 버퍼를 정리하지 않고 사용할 경우 언젠가 버퍼가 꽉차 사용하지 못 하게 됨으로 버퍼를 정리.
2. dataSize == 0일 경우 read,write position을 처음 인덱스인 0 으로 옮기면 끝.
dataSize > 0 일 경우 readPosition을 버퍼 인덱스 0으로 옮기고 writePosition은
dataSize만큼 으로 이동.
🕵️ OnRead()
1. 버퍼에서 읽은 데이터가 dataSize보다 큰지 확인 후 크다면 false를 반환 후 연결을 종료 시킴.
🕵️ OnWrite()
1. Receive한 데이터길이가 남은 공간과 비교 후 그보다 클경우 false를 반환 후 연결종료
🕵️ RecvBuffer.cs 소스
using System;
using System.Collections.Generic;
using System.Text;
namespace ServerCore
{
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 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
1. SendBufferHelper
- send는 Receive와 다르게 버퍼를 정리하지 않고 버퍼의 공간이 부족하면 새로 메모리를 할당해 사용.
이유로는 각 send 데이터를 각 Session에서 Queue에 저장하고 전송을 하는데 아직 전송되지 않고 queue에 남아있는 상태인데 buffer의 데이터를 수정 하면 엉뚱한 데이터가 날라가게 되기 때문.
- receivebuffer는 Session안에서 구현을 하지만 SendBuffer는 Session을 상속 받아 구현한 GameSession에서 관리 함.
내부가 아닌 외부에서 관리하는 이유는 동일한 데이터를 수백 ~ 수천개의 client에 전송하는 경우
외부에서 관리 하게 될 경우 한번만 buffer에 복사 후 전송을 하면되지만 내부에서 관리하게 되면
클라이언트 수만큼 복사가 일어나기 때문(효율 문제)
🕵️ public static ThreadLocal CurrentBuffer
- ThreadLocal은 Thread 내부 메모리에 저장되며 같은 쓰레드 안에서만 접근 가능.
- Send의 경우 여러 Thread에서 접근하기 때문에 쓰레드간의 경쟁을 방지 하기위해 위와 같이 공유
메모리가 아닌 Thread마다 내부 메모리에 buffer를 할당.
2. SendBuffer
🕵️ Open()
1. 요청한 사이즈의 버퍼를 반환.
2. 실제 사용한 버퍼사이즈를 모르기 때문에 _usedSize는 변화시키지 않음.
🕵️ Close()
1. buffer에 데이터를 복사 한 이후 단계
2. usedSize를 사용한 데이터 사이즈만큼 추가.
🕵️ SendBuffer.cs 소스
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace ServerCore
{
public class SendBufferHelper
{
public static ThreadLocal<SendBuffer> CurrentBuffer = new System.Threading.ThreadLocal<SendBuffer>(() => { return null; });
public static int ChunkSize { get; set; } = 4096;
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);
}
}
public class SendBuffer
{
byte[] _buffer;
int _usedSize = 0;
public SendBuffer(int chunkSize)
{
_buffer = new byte[chunkSize];
}
public int FreeSize { get { return _buffer.Length - _usedSize; } }
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;
}
}
}
3. Session.cs
1. PacketSession
- 보통 패킷은 [size(2)][PacketId(2)][..... 내용] 으로 구성함.
- 그래서 OnRecv()에서 처음 2바이트로 패킷 size를 가져오고 전체 버퍼 사이즈가 size만큼의 길이가 되는지 체크 후 유효할 경우 데이터를 콘텐츠단으로 넘김.
2. Session
🕵️ OnRecvCompleted()
- 컨텐츠 단에서 처리한 데이터 사이즈와 패킷 첫 2바이트의 데이터 사이즈와 동일한지
확인 후 동일 할 경우 버퍼의 readPos를 옮김.
패킷 전체 count가 패킷 사이즈보다 작을 경우 buffer에 쓰기만 하고 데이터를 콘텐츠 단으로 넘기지 않음.
이후 전체 데이터가 들어온 경우에만 데이터를 처리함.
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace ServerCore
{
public abstract class PacketSession : Session
{
public static readonly int HeaderSize = 2;
public sealed override int OnRecv(ArraySegment<byte> buffer)
{
int processLen = 0;
while(true)
{
if (buffer.Count < HeaderSize)
break;
ushort dataSize = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
if (buffer.Count < dataSize)
break;
OnRecvPacket(new ArraySegment<byte>(buffer.Array,buffer.Offset,dataSize));
processLen += dataSize;
buffer = new ArraySegment<byte>(buffer.Array, buffer.Offset + dataSize, buffer.Count - dataSize);
}
return processLen;
}
abstract public void OnRecvPacket(ArraySegment<byte> buffer);
}
public abstract class Session
{
Socket _socket;
int _disconneted = 0;
RecvBuffer _recvBuffer = new RecvBuffer(1024);
List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
Queue<ArraySegment<byte>> _sendQue = new Queue<ArraySegment<byte>>();
SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
SocketAsyncEventArgs _receiveArgs = new SocketAsyncEventArgs();
object _sendLock = new object();
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 clientSocket)
{
_socket = clientSocket;
_receiveArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
RegisterRecv();
_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
}
public void Disconneted()
{
if (Interlocked.CompareExchange(ref _disconneted, 1, 0) != 0)
return;
OnDisconnected(_socket.RemoteEndPoint);
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
public void Send(ArraySegment<byte> sendBuffe)
{
lock (_sendLock)
{
_sendQue.Enqueue(sendBuffe);
if (_pendingList.Count == 0)
RegisterSend();
}
}
#region 네트워크 통신
void RegisterSend()
{
while (_sendQue.Count > 0)
{
ArraySegment<byte> buff = _sendQue.Dequeue();
_pendingList.Add(buff);
}
_sendArgs.BufferList = _pendingList;
bool pending = _socket.SendAsync(_sendArgs);
if (!pending)
OnSendCompleted(null, _sendArgs);
}
void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
lock (_sendLock)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
_pendingList.Clear();
_sendArgs.BufferList = null;
OnSend(args.BytesTransferred);
if (_sendQue.Count > 0)
RegisterSend();
}
catch (Exception e)
{
Console.WriteLine($"OnSendCompleted Failed {e}");
}
}
else
{
Disconneted();
}
}
}
void RegisterRecv()
{
_recvBuffer.Clean();
ArraySegment<byte> segment = _recvBuffer.WriteSegment;
_receiveArgs.SetBuffer(segment.Array, segment.Offset, segment.Count);
bool pending = _socket.ReceiveAsync(_receiveArgs);
if (!pending)
OnRecvCompleted(null, _receiveArgs);
}
void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
{
try
{
if (_recvBuffer.OnWrite(args.BytesTransferred) == false)
{
Disconneted();
return;
}
int ProcessLen = OnRecv(_recvBuffer.Readsegment);
if (ProcessLen < 0 || _recvBuffer.DataSize < ProcessLen)
{
Disconneted();
return;
}
if (_recvBuffer.OnRead(ProcessLen) == false)
{
Disconneted();
return;
}
RegisterRecv();
}
catch (Exception e)
{
Console.WriteLine($"OnRecvCompleted Failed {e}");
}
}
else
{
Disconneted();
}
}
#endregion
}
}
4. ClientDummy.Program.cs
1. PacketSession
- send.open()는 버퍼에서 필요한 사이즈만큼 사이즈를 예약 후 데이터를 쓰고 send.close()에 사용한 버퍼 사이즈만큼 전달.
using ServerCore;
using System;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace DummyClient
{
class Packet
{
public ushort size;
public ushort packetId;
}
class GameSession : Session
{
public override void OnConnected(EndPoint endPoint)
{
Console.WriteLine($"Conneted to {endPoint.ToString()}");
for (ushort i = 0; i < 5; i++)
{
Packet knight = new Packet() { size = 4, packetId = i };
byte[] buffer = BitConverter.GetBytes(knight.size);
byte[] buffer2 = BitConverter.GetBytes(knight.packetId);
ArraySegment<byte> openSegment = SendBufferHelper.Open(buffer.Count() + buffer2.Count());
Array.Copy(buffer, 0, openSegment.Array, openSegment.Offset, buffer.Length);
Array.Copy(buffer2, 0, openSegment.Array, openSegment.Offset + buffer.Length, buffer2.Length);
ArraySegment<byte> sendBuffe = SendBufferHelper.Close(knight.size);
Send(sendBuffe);
}
Thread.Sleep(1000);
Disconneted();
Disconneted();
}
public override void OnDisconnected(EndPoint endPoint)
{
}
public override int OnRecv(ArraySegment<byte> buffer)
{
string ReceiveData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
Console.WriteLine($"[From Server] {ReceiveData}");
return buffer.Count;
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"SendData Byte : {numOfBytes }");
}
}
class Program
{
static void Main(string[] args)
{
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipaddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipaddr, 7777);
Connector conneter = new Connector();
conneter.Connect(endPoint, () => { return new GameSession(); });
while (true)
{
try
{
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Thread.Sleep(100);
}
}
}
}
5. Server.Program.cs
using System;
using System.Net;
using System.Text;
using System.Threading;
using ServerCore;
namespace Server
{
class Packet
{
public ushort size;
public ushort packetId;
}
class GameSession : PacketSession
{
public override void OnConnected(EndPoint endPoint)
{
Thread.Sleep(1000);
Disconneted();
Disconneted();
}
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + 2);
Console.WriteLine($"ReceiveId : {id} , size : {size}");
}
public override void OnDisconnected(EndPoint endPoint)
{
}
public override void OnSend(int numOfBytes)
{
Console.WriteLine($"SendData Byte : {numOfBytes }");
}
}
class Program
{
static Listener _listener = new Listener();
static void Main(string[] args)
{
string host = Dns.GetHostName();
IPHostEntry ipHost = Dns.GetHostEntry(host);
IPAddress ipaddr = ipHost.AddressList[0];
IPEndPoint endPoint = new IPEndPoint(ipaddr, 7777);
_listener.init(endPoint, () => { return new GameSession(); });
while (true)
{
}
}
}
}
같은 강의 듣고 있어서 정리하신거 보면서 빠진거 있는지 파악할 수 있었습니다~ 다음 정리도 기대하고 있어요😁