tmd1 2024. 11. 10. 23:57
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ServerCore
{
    public class RecvBuffer
    {
        // [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
        ArraySegment<Byte> _buffer;

        // readPos == writePos면 들어온 data 없음
        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; } }

        // _buffer에서 _readPos 부분에서 시작하여 처리할 데이터 크기만큼 가져옴
        public ArraySegment<byte> ReadSegment
        {
            get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _readPos, DataSize); }
        }

        // _buffer에서 _writePos 부분에서 시작하여 남은 버퍼 사이즈만큼 가져옴 (다음 Recv의 유효범위)
        public ArraySegment<byte> WriteSegment
        {
            get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _writePos, FreeSize); }
        }

        // 계속 채우다보면 _readPos와 _writePos가 버퍼 사이즈 이상으로 밀리게 될 수 있으니
        // 기존 데이터와 r,w의 위치를 앞으로 끌어당김
        public void Clean()
        {
            int dataSize = DataSize;

            
            if(dataSize == 0)
            {
                // _readPos와 _writePos가 같은 경우, 클라에서 보낸 모든 데이터를 처리한 경우
                // 즉 처리할 데이터가 없는 경우 r,w를 맨앞으로 당김
                // 남은 데이터가 없으면 복사하지 않고 커서 위치만 리셋
                _readPos = _writePos = 0;
            }
            else
            {
                // 남은 데이터가 있으면 시작 위치로 복사
                // 버퍼의 _readPos 위치부터 dataSize만큼 버퍼의 오프셋 부분 부터 복사
                Array.Copy(_buffer.Array, _buffer.Offset + _readPos, _buffer.Array, _buffer.Offset, dataSize);
                _readPos = 0;
                _writePos = dataSize;
            }
        }

        public bool OnRead(int numOfBytes)
        {
            // numOfBytes : 처리에 성공한 데이터 크기
            
            // 처리 가능한 데이터 크기보다 더 큰 데이터를 처리할 수는 없으므로 false
            if (numOfBytes > DataSize)
                return false;

            // numOfBytes 만큼 처리했으므로 _readPos 위치를 옮김
            _readPos += numOfBytes;
            return true;
        }

        public bool OnWrite(int numOfBytes)
        {
            // numOfBytes : 클라이언트로 부터 받은 데이터 크기

            // 빈 공간보다 더 큰 데이터를 받을 수는 없으므로 false
            if (numOfBytes > FreeSize)
                return false;

            // numOfBytes 만큼 데이터를 받았으므로 _writePos의 위치를 옮김
            _writePos += numOfBytes;
            return true;
        }
    }
}

 

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace ServerCore
{
    public abstract class Session
    {
        Socket _socket;
        int _disconnectd = 0;

        RecvBuffer _recvBuffer = new RecvBuffer(1024);

        // Aysnc 횟수가 늘어날수록 시간이 오래걸리므로(부하가 큼) Queue를 통해 쌓아놨다가 한번에 처리
        object _obj = new object();
        Queue<byte[]> _sendQueue = new Queue<byte[]>();

        // send할 정보들을 모아서 보내기 위한 list
        List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();

        // send 할때마다 _sendArgs를 만들면 매우 부담이 높기에 따로 뺌(재사용)
        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 OnDisconnect(EndPoint endPoint);

        public void Start(Socket socket)
        {
            _socket = socket;

            _recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnReceiveCompleted);
            _sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);

            RegisterReceive();
        }

        public void Send(byte[] sendBuff)
        {
            // 여러 스레드에서 동시에 호출 될 수 있으므로 lock 걺
            lock (_obj)
            {
                _sendQueue.Enqueue(sendBuff);

                // 이미 send가 진행 중이라면 RegisterSend 하지 않음
                if (_pendingList.Count == 0)
                    RegisterSend();
            }
        }

        public void Disconnect()
        {
            if (Interlocked.Exchange(ref _disconnectd, 1) == 1)
                return;

            OnDisconnect(_socket.RemoteEndPoint);
            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }

        #region 네트워크 통신

        void RegisterSend()
        {

            // sendQueue에 넣은 데이터를 모두 빼서 _pendingList에 모음
            while (_sendQueue.Count > 0)
            {
                byte[] buff = _sendQueue.Dequeue();
                _pendingList.Add(new ArraySegment<byte>(buff, 0, buff.Length));
            }

            // BufferList의 경우 각 byte buffer를 add하는 식으로 하면 이상하게 작동
            // 반드시 = 연산으로 리스트를 한번에 집어넣어야 함
            // BufferList에 sendQueue 내용을 모두 넣어 Aysnc를 걸면
            // BufferList에 있는 내용을 한번에 보내게 됨
            _sendArgs.BufferList = _pendingList;

            bool pending = _socket.SendAsync(_sendArgs);
            if (pending == false)
                OnSendCompleted(null, _sendArgs);
        }

        void OnSendCompleted(object sender, SocketAsyncEventArgs args)
        {
            lock (_obj)
            {
                if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
                {

                    // SendAysnc가 처리되는 사이에 또 다른 Send()가 있었다면 
                    // 다시 RegisterSend() 실행

                    // 기존엔 sendQueue가 비어있을 경우 _pending = false(send 진행중이 아님)로 하여 줬음
                    // sendQueue가 빌때까지 하나씩 Dequeue하여 send를 진행하였기 때문
                    // 지금은 send 진행 여부를 _pendingList가 비어있냐로 확인 가능
                    // OnSendCompleted로 들어온 경우 sendQueue는 찼을지 몰라도(새로운 send)
                    // 기존 send는 마무리 되었으므로 _pendingList를 비워줌

                    // 사실 비슷하긴한데 비유하자면 현재 send는 박스에 한번에 모아서 전달하니 박스 내용물 있는지 여부로 전송 중 판단하고
                    // 기존 send는 물품 하나하나 보내니 send 여부를 판별하는 flag가 필요했던 것

                    // Recv는 RegisterReceive 계속 실행시켜서 recv를 기다리지만
                    // Send는 딱히 들어오는 걸 기다리는 건 아니니
                    // Send의 경우 Send() 함수가 실행되면 처리가 시작됨


                    _sendArgs.BufferList = null;
                    _pendingList.Clear();

                    OnSend(_sendArgs.BytesTransferred);


                    if (_sendQueue.Count > 0)
                        RegisterSend();
                    
                }
                else
                {
                    Disconnect();
                }
            }
           
        }

        
        void RegisterReceive()
        {
            _recvBuffer.Clean();
            ArraySegment<byte> segment = _recvBuffer.WriteSegment;
            _recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count);


            bool pending = _socket.ReceiveAsync(_recvArgs);
            if (pending == false)
                OnReceiveCompleted(null, _recvArgs);
        }

        void OnReceiveCompleted(object sender, SocketAsyncEventArgs args)
        {
            // BytesTransferred -> 몇 byte를 전송 받았는가
            // 0인 경우 상대방이 연결을 끊은 것
            if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
            {
                // Write 커서 이동
                if (_recvBuffer.OnWrite(args.BytesTransferred) == false)
                {
                    Disconnect();
                    return;
                }


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

                // Read 커서 이동
                if(_recvBuffer.OnRead(processLen) == false)
                {
                    Disconnect();
                    return;
                }


                // accept와 다르게 if문 밖에서 호출하지 않는 이유는
                // accpet는 연결 안되면 걍 버리고 다른 연결 기다리면 되는데
                // 얘는 연결 상태로 에러 난거니까 다시 receive하면 안됨
                RegisterReceive();
            }
            else
            {
                Disconnect();
            }

        }
        #endregion

    }
}