수업

using System.Net.Sockets;
using System.Net;
using System.Text;

namespace ServerCore
{
    public 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(1000);
            Disconnect();
        }

        public override void OnDisconnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnDisconnected: {endPoint}");
        }

        public override void OnRecv(ArraySegment<byte> buffer)
        {
            string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
            Console.WriteLine($"[From Client] {recvData}");
        }

        public override void OnSend(int numOfBytes)
        {
            Console.WriteLine($"Transferred bytes: {numOfBytes}");
        }
    }
    class Program
    {

        static Listener _listener = new Listener();

        //static void OnAcceptHandler(Socket clientSocket)
        //{
        //    try
        //    {



        //        GameSession session = new GameSession();
        //        session.Start(clientSocket);

        //        // 2️ 클라이언트에 응답 전송
        //        byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server!");

        //        session.Send(sendBuff);

        //        Thread.Sleep(1000);

        //        // disconnect를 두번하면
        //        session.Disconnect();

        //    }
        //    catch (Exception e)
        //    {
        //        Console.WriteLine($"[Error] {e}");
        //    }
        //}
        // DNS (Domain Name System)
        // IP 주소를 숨겨주는 도메인 네임을 설정해줌
        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 초기화 및 Accept 핸들러 등록
            _listener.Init(endPoint, () => { return new GameSession(); });
            Console.WriteLine("Server Listening...");

            while (true)
            {
                // 서버가 종료되지 않도록 유지
            }



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

namespace ServerCore
{
    class Listener
    {
        Socket _listenSocket;
        //Action<Socket> _onAcceptHandler; // Accept 완료 시 실행할 함수
        Func<Session> _sessionFactory;

        public void Init(IPEndPoint endPoint, Func<Session> sessionFactory)
        {
            // 문지기 교육
            _listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            _sessionFactory += sessionFactory;

            // 문지기 교육
            _listenSocket.Bind(endPoint);
            // 영업 시작
            // 최대 대기수
            _listenSocket.Listen(10); // 최대 대기 큐 크기 설정
            
            // 낚시대를 여러개 던짐
            for (int i = 0; i < 10; i++)
            {
                SocketAsyncEventArgs args = new SocketAsyncEventArgs();
                args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
                // 최조 한번은 등록을 해줘야함
                RegisterAccept(args);
            }

        }
        // pending이 계속 물고 있으면 stackoverflow에 걸리지 않을까?
        // 현실적으로는 일어날 수 없음
        void RegisterAccept(SocketAsyncEventArgs args)
        {
            args.AcceptSocket = null; // 이전 Accept 소켓 초기화

            bool pending = _listenSocket.AcceptAsync(args); // 비동기 Accept 실행 // 펜딩을 뱉어줌
            if (pending == false) // 즉시 완료된 경우
                OnAcceptCompleted(null, args);
        }

        // 위험한 상황
        // 멀티스레드로 동작할 수 있다.
        void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
        {
            if (args.SocketError == SocketError.Success)
            {
                Session session = _sessionFactory.Invoke();
                session.Start(args.AcceptSocket);
                session.OnConnected(args.AcceptSocket.RemoteEndPoint);
                // 콜백 방식을 이용해서 인자로 받아서 완료되면 호출
                // 클라이언트 Accept 완료 후 처리
                //_sessionFactory.Invoke(args.AcceptSocket);
            }
            else
            {
                Console.WriteLine($"Accept Error: {args.SocketError}");
            }
            // 모든 일이 끝남 다음 턴을 위해 준비해둠
            RegisterAccept(args); // 다음 Accept 예약 (낚시줄 다시 던지기)
        }
        public Socket Accept()
        {
            return _listenSocket.Accept();// 비동기 의미 조금 나중에 처리될 수 있다.
            // 어떤식으로든 알려줘야함
            // 비동기는 많은 언어에서 사용함
            // 우선 알아야할 것은 실제로 처리되는 부분이 둘로 쪼개져서 두번 만들어야함

        }

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


namespace ServerCore
{
    // 1번 압버
    //class SessionHandler
    //{
    //    public void OnConnected(EndPoint endPoint)
    //    {

    //    }

    //    public void OnDisconnected(EndPoint endPoint)
    //    {
    //    }

    //    public void OnRecv(ArraySegment<byte> buffer)
    //    {

    //    }

    //    public void OnSend(int numOfBytes)
    //    {
    //    }
    //}
    abstract public class Session
    {
        Socket _socket;
        SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs(); // 비동기 Receive 처리
        SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs(); // 비동기 Receive 처리
        Queue<byte[]> _sendQueue = new Queue<byte[]>();
        List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();

        bool _pending = false;
        int _disconnected = 0;
        object _lock = new object();


        public abstract void OnConnected(EndPoint endPoint);
        

        public abstract void OnDisconnected(EndPoint endPoint);
        

        public abstract void OnRecv(ArraySegment<byte> buffer);
        

        public abstract void OnSend(int numOfBytes);
        
        // 좀더 최적화 하는 방법
        public void Start(Socket socket)
        {
            _socket = socket;


            _recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);


            _recvArgs.SetBuffer(new byte[1024],0,1024);
            _sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);

            RegisterRecv();
        }

        public void Send(byte[]sendBuff)
        {
            //_socket.Send(sendBuff);
            // 이 시점에 resigter 한다.
            lock (_lock)
            {
                // 일감을 줌
                _sendQueue.Enqueue(sendBuff);

                // Send 작업이 진행 중이지 않다면 RegisterSend 호출
                //if(_pending == false)
                //{
                //    RegisterSend();
                //}
                // 먼저면 보내
                if (_pendingList.Count == 0)
                {
                    RegisterSend();
                }

                //if (_pendingList.Count == 0)
                //{
                //    RegisterSend();
                //}
            }
        }

        public void Disconnect()
        {
            if (Interlocked.Exchange(ref _disconnected, 1) == 1)
                return;
            // 널체크는 멀티스레드 환경에서 위험함
            OnDisconnected(_socket.RemoteEndPoint);
            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }

        #region 네트워크 통신


        void RegisterSend()
        {
            // 이 부분이 느려짐
            // 어떤식으로든 뭉쳐서 보내는게 좋음
            //_pending = true;
            //_pendingList.Clear();
            // 여기에 리스트 지역변수를 만들어서 연결해서 사용
            // List<ArraySegment<byte>> list = new List<ArraySegment<byte>>(); // 낭비같다는 생각이 듬 전역 변수로 만듬
            // 더 세련된 방법
            // 리스트로 보낸다.
            // 아래 두방법 중 하나만 사용해야함
            //byte[] buff = _sendQueue.Dequeue();
            //_sendArgs.SetBuffer(buff, 0 ,buff.Length);
            // 코드
            while (_sendQueue.Count > 0)
            {
                // 데이터가 10
                // [][][][][][][][][][]
                // C++은 포인터가 있어서 시작주소로 넘겨줄 수 있음
                // C#은 첫주소만 알 수 있음
                // 어디서 시작하는지 따로 넘겨줘야함
                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);
            }
            //bool pending = _socket.SendAsync(_sendArgs);
            //if (!pending)
            //{
            //    OnSendCompleted(null, _sendArgs);
            //}
            //while (_sendQueue.Count > 0)
            //{
            //    byte[] buff = _sendQueue.Dequeue();
            //    _pendingList.Add(new ArraySegment<byte>(buff, 0, buff.Length));
            //}

            //_sendArgs.BufferList = _pendingList; // 여러 개의 데이터를 한 번에 보낼 수 있도록 설정

           
        }
        void OnSendCompleted(object sender, SocketAsyncEventArgs args)
        {

            lock (_lock)
            {
                if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
                {
                    try
                    {
                        _sendArgs.BufferList = null; // 버퍼 초기화
                        _pendingList.Clear(); // 대기 리스트 초기화

                        OnSend(_sendArgs.BytesTransferred);
                        //Console.WriteLine($"Transferred bytes : {_sendArgs.BytesTransferred}");
                        if (_sendQueue.Count > 0)
                        {
                            RegisterSend();
                        }
                        else
                        {
                            _pending = false;
                        }
                        

                        //_sendArgs.BufferList = null; // 버퍼 초기화
                        //_pendingList.Clear(); // 대기 리스트 초기화

                        //if (_sendQueue.Count > 0)
                        //{
                        //    RegisterSend();
                        //}
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine($"OnSendCompleted Failed: {e}");
                    }
                }
                else
                {
                    Disconnect();
                }
            }

        }
        void RegisterRecv()
        {
            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)
            {
                try
                {
                    // TODO
                    OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
                    //string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
                    //Console.WriteLine($"[From Client] {recvData}");

                    RegisterRecv();
                }
                catch(Exception e)
                {
                    Console.WriteLine($"OnRecvCompleted Failed {e}");
                }
            }
            else
            {
                // disconnect
            }
        }
        #endregion
    }
}

✅ 전체 구성

주요 주제
1부Session 클래스의 설계 목적, 비동기 수신 흐름, 추상 콜백 구조
2부비동기 송신 처리, Send 큐 구조, Disconnect 처리, GameSession 확장
3부Listener와 세션 생성 흐름, Func<Session> 팩토리 패턴, 엔진-컨텐츠 단 분리 설계

📘 1부 – Session 클래스 구조와 비동기 수신 흐름


✅ 주제

  • C# TCP 서버에서 클라이언트와의 통신을 처리하는 Session 클래스의 도입 목적
    비동기 방식의 ReceiveAsync 수신 처리 구조를 이해한다.

📚 개념

  • 서버는 Accept 이후 각 클라이언트와 데이터를 주고받을 Session 객체가 필요하며,
    이 객체는 소켓을 보관하고 직접 수신/송신을 담당한다.
  • 수신은 비동기 이벤트 기반 구조로 구현되어야 다수의 클라이언트와의 동시 처리가 가능하다.
  • 따라서 SocketAsyncEventArgs를 활용한 낚시대 패턴(이벤트 완료시 다시 등록)을 채택한다.

🧾 용어정리

용어설명
Session클라이언트 연결 하나를 담당하는 통신 객체
ReceiveAsync()비동기 방식으로 데이터를 수신
SetBuffer()수신에 사용할 버퍼 설정
Completed수신 완료 시 호출되는 이벤트 핸들러
Interlocked.Exchange()멀티스레드 환경에서 원자적 변수 교체
BytesTransferred수신된 바이트 수

💻 코드 흐름 분석

1. Start() — 수신 준비

public void Start(Socket socket)
{
    _socket = socket;
    _recvArgs.Completed += OnRecvCompleted;
    _recvArgs.SetBuffer(new byte[1024], 0, 1024);
    RegisterRecv();
}
  • 수신용 SocketAsyncEventArgs 객체에 버퍼를 설정하고 수신 이벤트 핸들러 등록
  • RegisterRecv() 호출로 첫 낚시대 투척

2. RegisterRecv() — 수신 등록

void RegisterRecv()
{
    bool pending = _socket.ReceiveAsync(_recvArgs);
    if (!pending)
        OnRecvCompleted(null, _recvArgs);
}
  • ReceiveAsync 호출
  • 즉시 완료되면 바로 OnRecvCompleted() 호출

3. OnRecvCompleted() — 수신 완료 시 처리

void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
    if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
    {
        OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
        RegisterRecv();
    }
    else
    {
        Disconnect();
    }
}
  • 데이터 수신 성공 시 OnRecv 콜백 호출 후 다시 수신 대기 등록
  • 실패 또는 연결 종료 시 Disconnect 호출

🧠 핵심

  • Receive는 낚시 패턴: ReceiveAsync → OnRecvCompleted → RegisterRecv 반복
  • Session은 추상 클래스로 정의되며, 이벤트 함수는 상속한 클래스에서 구현
  • 비동기 구조는 반드시 끊김 없이 이어지도록 설계

📘 2부 – 비동기 송신 흐름 및 GameSession 확장


✅ 주제

  • Session.Send() 구조를 개선하여 큐 기반의 비동기 송신 처리 구현
  • 멀티스레드 환경에서의 안전성을 확보하고,
    실제 사용자 로직을 구현할 GameSession 클래스 확장까지 연결한다

📚 개념

  • Send는 바로 실행하면 안 된다!
    → 큐에 쌓고 하나씩 순차적으로 처리해야 함
  • 한 번에 하나의 송신만 활성화되도록 제어
    _pendingList.Count == 0일 때만 전송 시작
  • BufferList를 활용해 여러 패킷을 한 번에 전송 가능

🧾 용어정리

용어설명
_sendQueue보낼 데이터를 대기시키는 큐
_pendingList현재 전송 중인 패킷 리스트
BufferList여러 세그먼트를 한 번에 전송할 수 있게 해주는 SocketAsyncEventArgs 속성
SendAsync()비동기 송신
lock멀티스레드에서 자원 충돌 방지

💻 송신 흐름 코드

1. Send() — 전송 요청

public void Send(byte[] sendBuff)
{
    lock (_lock)
    {
        _sendQueue.Enqueue(sendBuff);
        if (_pendingList.Count == 0)
            RegisterSend();
    }
}

2. 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)
        OnSendCompleted(null, _sendArgs);
}

3. OnSendCompleted() — 전송 완료 시 다음 전송 또는 종료

void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
    lock (_lock)
    {
        if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
        {
            _sendArgs.BufferList = null;
            _pendingList.Clear();
            OnSend(args.BytesTransferred);

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

💬 GameSession 구현

class GameSession : Session
{
    public override void OnConnected(EndPoint endPoint)
    {
        Console.WriteLine($"OnConnected: {endPoint}");
        Send(Encoding.UTF8.GetBytes("Welcome to MMORPG Server!"));
        Thread.Sleep(1000);
        Disconnect();
    }

    public override void OnRecv(ArraySegment<byte> buffer)
    {
        string data = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
        Console.WriteLine($"From Client: {data}");
    }

    public override void OnSend(int bytes) => Console.WriteLine($"Sent: {bytes} bytes");

    public override void OnDisconnected(EndPoint endPoint) => Console.WriteLine($"Disconnected: {endPoint}");
}

🔑 핵심 정리

  • 송신은 큐에 쌓고, _pendingList로 전송
  • 완료 시 다음 전송 예약 (OnSendCompleted → RegisterSend)
  • GameSession은 추상 메서드를 오버라이드하여 실제 로직 작성

📘 3부 – Listener, 세션 팩토리 구조, 전체 연결 흐름


✅ 주제

  • Listener 클래스에서 클라이언트 Accept 처리
  • Func<Session>으로 GameSession 주입
  • 프로그램 전체 연결 구조를 하나로 통합

📚 개념

  • Listener는 더 이상 세션을 직접 생성하지 않음
  • 외부에서 Func<Session>을 통해 생성 책임을 위임받음
  • 엔진(ServerCore) 은 구조만 제공하고,
    컨텐츠(Server) 는 행동 정의만 담당

🧾 용어정리

용어설명
Listener클라이언트 Accept 처리 담당 클래스
Func<Session>세션 객체를 외부에서 생성해 넘기는 델리게이트
GameSessionSession을 상속하여 게임 특화 로직을 구현
OnAcceptCompletedAccept 완료 시 호출되는 이벤트 함수

💻 핵심 코드 흐름

1. Listener Init

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

2. OnAcceptCompleted

void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
    if (args.SocketError == SocketError.Success)
    {
        Session session = _sessionFactory.Invoke();
        session.Start(args.AcceptSocket);
        session.OnConnected(args.AcceptSocket.RemoteEndPoint);
    }

    RegisterAccept(args); // 낚시대 다시 던지기
}

🌐 프로그램 전체 흐름

[Main()]
  → Listener.Init(endpoint, GameSession 생성자 등록)
     → RegisterAccept() (낚시대)
         → AcceptAsync 완료 → OnAcceptCompleted()
             → GameSession 생성
             → session.Start(socket)
             → session.OnConnected()
                 → Send("Welcome") → Disconnect()

🧠 핵심

구성요소설명
ListenerAccept 수신 및 Session 생성
Session엔진 단의 송수신/해제 추상 처리 클래스
GameSession컨텐츠 단에서 세션 이벤트 구현
Func<Session>외부에서 세션을 주입받기 위한 팩토리
분리 설계엔진-컨텐츠 역할 분리, 유지보수 및 확장성 향상

profile
李家네_공부방

0개의 댓글