서버/서버 이론

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)
            {
                ;
            }
        }
    }
}