[server 프로그램 c#] TCP SendBuffer, ReceiveBuffer, packetSession 구현.

코찔찔이·2023년 10월 23일
0
  1. Send,ReceiveBuffer를 사용.
  2. 기초적인 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; } } // 사용가능한 버퍼사이즈

        //Readsegment, WriteSegment

        // 유효한 데이터 범위의 buffer반환
        public ArraySegment<byte> Readsegment
        {
            get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _readPos, DataSize); }
        }


        // write가능한 범위의 buffer반환
        public ArraySegment<byte> WriteSegment
        {
            get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _writePos, FreeSize); }
        }

        //buffer 정리. 
        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

  1. send는 Receive와 다르게 버퍼를 정리하지 않고 버퍼의 공간이 부족하면 새로 메모리를 할당해 사용.
    이유로는 각 send 데이터를 각 Session에서 Queue에 저장하고 전송을 하는데 아직 전송되지 않고 queue에 남아있는 상태인데 buffer의 데이터를 수정 하면 엉뚱한 데이터가 날라가게 되기 때문.
  2. receivebuffer는 Session안에서 구현을 하지만 SendBuffer는 Session을 상속 받아 구현한 GameSession에서 관리 함.
    내부가 아닌 외부에서 관리하는 이유는 동일한 데이터를 수백 ~ 수천개의 client에 전송하는 경우
    외부에서 관리 하게 될 경우 한번만 buffer에 복사 후 전송을 하면되지만 내부에서 관리하게 되면
    클라이언트 수만큼 복사가 일어나기 때문(효율 문제)

🕵️ public static ThreadLocal CurrentBuffer

  1. ThreadLocal은 Thread 내부 메모리에 저장되며 같은 쓰레드 안에서만 접근 가능.
  2. 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

  1. 보통 패킷은 [size(2)][PacketId(2)][..... 내용] 으로 구성함.
  2. 그래서 OnRecv()에서 처음 2바이트로 패킷 size를 가져오고 전체 버퍼 사이즈가 size만큼의 길이가 되는지 체크 후 유효할 경우 데이터를 콘텐츠단으로 넘김.

2. Session

🕵️ OnRecvCompleted()

  1. 컨텐츠 단에서 처리한 데이터 사이즈와 패킷 첫 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;
        //[size(2)][packeid(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();
            }
            //_socket.Send(sendBuffe);
        }

        #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
                {
                    //Write 커서 이동
                    if (_recvBuffer.OnWrite(args.BytesTransferred) == false)
                    {
                        Disconneted();
                        return;
                    }

                    //컨텐츠 쪽으로 데이터를 넘겨주고 얼마나 처리햇는지 받는다.
                    int ProcessLen = OnRecv(_recvBuffer.Readsegment);
                    if (ProcessLen < 0 || _recvBuffer.DataSize < ProcessLen)
                    {
                        Disconneted();
                        return;
                    }

                    //Read 커서이동
                    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

  1. 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]; //아이피가 여러개 있을수 있으며 배열로 ip를 반환함
            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 LoginIOkPacket : Packet
    //{

    //}

    class GameSession : PacketSession
    {
        public override void OnConnected(EndPoint endPoint)
        {
            //byte[] sendBuffe = Encoding.UTF8.GetBytes("welcome to MMORPG Server !...");

            //Packet knight = new Packet() { size = 100, packetId = 10 };
            //ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
            //byte[] buffer = BitConverter.GetBytes(knight.size);
            //byte[] buffer2 = BitConverter.GetBytes(knight.packetId);
            //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(buffer.Length + buffer2.Length);

            //Send(sendBuffe);


            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 int OnRecv(ArraySegment<byte> buffer)
        //{
        //    string ReceiveData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
        //    Console.WriteLine($"[From Client] {ReceiveData}");

        //    return buffer.Count;
        //}

        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]; //아이피가 여러개 있을수 있으며 배열로 ip를 반환함
            IPEndPoint endPoint = new IPEndPoint(ipaddr, 7777);

            _listener.init(endPoint, () => { return new GameSession(); });

            while (true)
            {

            }
        }
    }
}

1개의 댓글

comment-user-thumbnail
2024년 10월 30일

같은 강의 듣고 있어서 정리하신거 보면서 빠진거 있는지 파악할 수 있었습니다~ 다음 정리도 기대하고 있어요😁

답글 달기