[Part 4 게임 서버 ] 네트워크 프로그래밍 4 (Session #3, Session #4)

<Session #3>

1. 이전시간 강의 회고 

 - Receive Buffer는 Buffer로 연결 vs Send Buffer는 보낼 데이터가 담겨있는 Buffer

 - _sendArgs를 재사용하기 위해 클래스의 멤버 변수로 선언 -> rcvArgs도 재사용을 위해 멤버 변수로 가능(기호의 차이)

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

namespace SeverCore
{
    class Session
    {
        Socket _socket;
        int _disconnected = 0;

        object _lock = new object();
        Queue<byte[]> _sendQueue = new Queue<byte[]>();
        bool _pending = false; // 누가 RegisterSend를 하고 있으면 True, OnSendComplete 완료 후에 False로 바꾼다: Send에서 누가 Registr를 사용하고 있으면 Queue에다가 저장만 하겠다. 
        SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
        SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();

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

            _recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
            // Recv Buffer 설정
            _recvArgs.SetBuffer(new byte[1024], 0, 1024);

            _sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
            RegisterRecv();
        }

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

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

            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }
        #region 네트워크통신 

        // 비동기는 두단계로 나눠짐
        // Send
        void RegisterSend()
        {
            _pending = true;

            byte[]buff = _sendQueue.Dequeue();
            _sendArgs.SetBuffer(buff, 0, buff.Length);

            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
                    {
                        if(_sendQueue.Count > 0)
                        {
                            RegisterSend();
                        }
                        else
                            _pending = false;
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine($"OnSendCompleted Failed{e}");
                    }
                }
                else
                {
                    Disconnect();
                }
            }  
        }

        // Receive
        void RegisterRecv()
        {
            bool pending = _socket.ReceiveAsync(_recvArgs);
            if (pending == false) // pending이 false이면 기다릴필요없이 바로 메세지를 받을 수 있음 
                OnRecvCompleted(null, _recvArgs);
        }

        void OnRecvCompleted(Object sender, SocketAsyncEventArgs args)
        {
            if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
            {
                try
                {
                    // 성공 -> 버퍼의값을 받는다. 
                    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
            {
                // TO DO Disconnect
                Disconnect();
            }
        }
        #endregion
    }
}

2. 더 최적화 가능

:이전과 같이 Buffer를 하나만 두고 Buffer 별로 SendAsync를 하는 것이 아닌 BufferList를 통해 여러개의 버퍼를 리스트로 달아놓고 한번에 보낼 수도 있다. 

* 이때, SetBuffer와 BufferList는 둘 중에 하나만 써야함

 1) while문으로 _sendQueue.Count가 0일때까지 Dequeue로 꺼내온 byte[]를 buff로 _sendArgs.BufferList에 추가한다. 

  a) ArraySegment: Array의 일부를 나타내는 구조체: Stack에다가 할당 

    -> 사용하는 이유: C#에는 포인터 개념이 없기때문에 배열의 첫주소만 알 수 있다. 따라서, index를 넘겨줘서 몇번째부터 시작해야하는지 알려줘야한다. 

        void RegisterSend()
        {
            _pending = true;

            //
            while(_sendQueue.Count > 0)
            {
                byte[] buff = _sendQueue.Dequeue();
                _sendArgs.BufferList.Add(new ArraySegment<byte>(buff, 0, buff.Length));

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

 2) 이때, 위에 처럼 BufferList에 바로 Add를 해서 사용하면 안되고 무조건 BufferList = list; 이꼬르 상태로 List를 넘겨줘야한다.

* 이유는 없다. 

        void RegisterSend()
        {
            _pending = true;

            List<ArraySegment<byte>> list = new List<ArraySegment<byte>>();
            //
            while(_sendQueue.Count > 0)
            {
                byte[] buff = _sendQueue.Dequeue();
                list.Add(new ArraySegment<byte>(buff, 0, buff.Length));

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

 3) RegisterSend 함수를 호출할때마다 List를 만들지 말고 클래스의 멤버 변수로 하여 재사용하도록 한다. (중간 생략)

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

namespace SeverCore
{
    class Session
    {
        Socket _socket;
        int _disconnected = 0;

        object _lock = new object();
        Queue<byte[]> _sendQueue = new Queue<byte[]>();
        bool _pending = false; // 누가 RegisterSend를 하고 있으면 True, OnSendComplete 완료 후에 False로 바꾼다: Send에서 누가 Registr를 사용하고 있으면 Queue에다가 저장만 하겠다. 
        // BufferList를 위한 List
        List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
        SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
        SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();

        #region 네트워크통신 

        // 비동기는 두단계로 나눠짐
        // Send
        void RegisterSend()
        {
            _pending = true;
            _pendingList.Clear();

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

        #endregion
    }
}

4) _pending bool 값을 통해 송신을 위해 대기 중인 정보의 여부를 판단 -> _pendingList.Count를 통해 대기 중인 정보가 있는지 없는지를 알 수 있다. 

 a) bool _pending값 필요없다. 

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

 5) OnSendCompleted 함수의 흐름도 약간 바꿈 

   a) Send한 후에 OnSendCompleted 함수로 돌아오면, _pending을 false로 하지않고 _sendArgs.BufferList를 null로 밀어버리고 이미 보낸 데이터가 저장되어 있는 _pendingList를 Clear 한다. -> _pending = false와 똑같은 동작 

   b) 완료된 동안에 또 _sendQueue에 데이터가 쌓여있으면 RegisterSend로 다시 등록한다.  

   c) OnSendCompleted 함수에서 _pendingList를 Clear하기 때문에RegisterSend 함수에서 Clear할 필요없다. 

        void OnSendCompleted(object sender, SocketAsyncEventArgs args)
        {
            lock (_lock)
            {
                if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
                {
                    try
                    {
                        // BufferList 와 _pendingList 초기화 
                        _sendArgs.BufferList = null;
                        _pendingList.Clear();

                        Console.WriteLine($"Transferred bytes: {_sendArgs.BytesTransferred}");

                        if(_sendQueue.Count > 0)
                        {
                            RegisterSend();
                        }
                        
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine($"OnSendCompleted Failed{e}");
                    }
                }
                else
                {
                    Disconnect();
                }
            }  
        }
        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);    
        }

3. 코드의 전체 흐름 파악

 1) Send를 했다고 가정하면 lock을 잡고 _sendQueue에 보낼 데이터를 넣어준다. 후에 _pendingList.Count 가 0이어서 보낼 데이터가 없었다고 하면 바로 RegisterSend를 하고 이미 보낼 데이터가 있었으면 단순히 Queue에 sendBuff만 저장한다. 

 2) RegisterSend 함수에 들어오면 (_pendingList에 아무것도 없으면) _sendQueue.Count가 0이 될 때까지 _sendQueue에서 데이터를 꺼내서 BufferList에 저장 

 3) 해당 BufferList를 SendAsync로 보내준다. 만약 바로 보낼 수 있으면 pending = false로 OnSendComplete를 해야하고 바로 보낼 수 없다면 나중에 보내진 후(비동기적으로) OnSendComplete를 한다 .

 4) OnSendComplete 함수가 불려지면 현재 Send를 완료한 것이므로 다음 Send를 할 준비를 해야한다. 

   a) BufferList와 _pendingList를 초기화한다. (BufferList = null을 굳이 해줄 필요없지만 혹시 몰라 해줌)

   b) _sendQueue의 Count가 0이 아니면 보내는 사이에 어떤 스레드가 보내야할 데이터를 Queue에 저장한 것이므로 또 한번 RegisterSend를 해준다. 

* 위에는 굳이 안해줘도 어차피 OnSendComplete 함수가 종료되면 _pendingList.Count가 0이 되면서 RegisterSend가 되겠지만 먼저 해주면서 우아하게 코딩이 가능하다. 

 

* Sessionc.cs 전체 코드 

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

namespace SeverCore
{
    class Session
    {
        Socket _socket;
        int _disconnected = 0;

        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 void Start(Socket socket)
        {
            _socket = socket;

            _recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
            // Recv Buffer 설정
            _recvArgs.SetBuffer(new byte[1024], 0, 1024);

            _sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
            RegisterRecv();
        }

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

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

            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }
        #region 네트워크통신 

        // 비동기는 두단계로 나눠짐
        // Send
        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
                    {
                        // BufferList 와 _pendingList 초기화 
                        _sendArgs.BufferList = null;
                        _pendingList.Clear();

                        Console.WriteLine($"Transferred bytes: {_sendArgs.BytesTransferred}");

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

        // Receive
        void RegisterRecv()
        {
            bool pending = _socket.ReceiveAsync(_recvArgs);
            if (pending == false) // pending이 false이면 기다릴필요없이 바로 메세지를 받을 수 있음 
                OnRecvCompleted(null, _recvArgs);
        }

        void OnRecvCompleted(Object sender, SocketAsyncEventArgs args)
        {
            if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
            {
                try
                {
                    // 성공 -> 버퍼의값을 받는다. 
                    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
            {
                // TO DO Disconnect
                Disconnect();
            }
        }
        #endregion
    }
}

4. 결론 

 a) 짧은 시간동안 얼마의 데이터를 보냈는지를 추적하여 너무 심하게 많이 보내지 않도록 조절해야한다. (Throughput 조절해야함)

 b) 작은 패킷으로 보내지않고 큰 Buffer를 보내어 성능을 개선할 수 있다. 

   - 아주 짧은 시간동안 100명의 User들이 어떤 행위를 하였는지에 대해 각각의 패킷을 뭉쳐서 큰 Buffer로 각 Users들에게 뿌리면 성능이 훨씬 개선된다. 

   - 콘텐츠 단에서 모아서 보내야하는지 vs 서버에서 모아서 보내야하는지에 대해서는 게임마다 다르다. 

 

<Session #4>

1. Listener에서 OnAcceptHandler를 통해 Accept 한 후에 어떤 행동을 해줘야하는지 정의 

 -> Receive도 마찬가지로 어떻게 행동해야하는지 정의를 해줘야한다.

 1) Listener.cs에서 Action<Socket> _onAcceptHandler를 통해 onAcceptHandler를 받고 콜백으로 OnAcceptComplete 함수에 들어오면 해당 함수에서 _onAcceptHandler를 Invoke 시킨다. 

 

2. ServerCore는 서버 엔진 같은 존재, 콘텐츠는 Server에다가 만들게 됨

 1) Session을 바로 사용하지 않고 Interface로 사용한다. 

 2) Event를 받는 방식을 어떻게 하는가? 

   - EventHandler를 받아서 연결 

   - Session을 상속받아서 만드는 방법 

 3) 우리가 필요한 부분 (인터페이스로 정리)

   - OnConnected: 클라이언트의 접속이 완료되었다 

   - OnRecv: 클라이언트에서 받은 패킷을 받았다. 

   - OnSend: 보내는 것에 성공했다.

   - OnDisconnected: 클라이언트와의 연결 해제 

 => 외부에서는 위의 인터페이스만을 이용하지 이전시간에 구현했던 Register..., On~Complete 사용하지않는다. 

        // 인터페이스
        public void OnConnected(EndPoint endPoint) { }
        public void OnRecv(ArraySegment<byte> buffer) { }
        public void OnSend(int numOfBytes) { }
        public void OnDisconnected(EndPoint endPoint) { }

 d) 위의 인터페이스를 Event를 받는 방식으로 한다면 

   - EventHandler로 받아서 연결: 아래와 같이 SessionHandler 클래스를 정의하고 Session 클래스에 SessionHandler를 받아서 연결

    class SessionHandler
    {
        // 인터페이스
        public void OnConnected(EndPoint endPoint) { }
        public void OnRecv(ArraySegment<byte> buffer) { }
        public void OnSend(int numOfBytes) { }
        public void OnDisconnected(EndPoint endPoint) { }
    }
    class Session
    {
        Socket _socket;
        int _disconnected = 0;

        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();
        SessionHandler

  - 상속을 받아서 정의 -> 더 편함 

namespace SeverCore
{
    class GameSession: Session
    {

    }

3. Session 클래스를 abstract 클래스로 

 1) 아래와 같이 추상클래스로 만들어서 Program.cs와 Session 부분을 연결시켜줘야한다. 

 -> 엔진과 컨텐츠를 분리를 하는 작업 

    abstract class Session
    {
        Socket _socket;
        int _disconnected = 0;

        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 void OnRecv(ArraySegment<byte> buffer);
        public abstract void OnSend(int numOfBytes);
        public abstract void OnDisconnected(EndPoint endPoint);
namespace SeverCore
{
    class GameSession : Session
    {
        public override void OnConnected(EndPoint endPoint)
        {
        }

        public override void OnDisconnected(EndPoint endPoint)
        {
        }

        public override void OnRecv(ArraySegment<byte> buffer)
        {
        }

        public override void OnSend(int numOfBytes)
        {
        }
    }

 2) Disconnect 부분 

  a) Disconnect가 성공하면 OnDisconnected 호출하면 된다. 

  b) Program.cs에서  Disconnected한 후에 어떻게해야하는지 OnDisconnected에 정의하면 된다. 

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

            OnDisconnected(_socket.RemoteEndPoint);

            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }
        public override void OnDisconnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnDisconnected : {endPoint}");
        }

 

3) Receive 부분 

   a) OnRecvCompleted에서 args.Buffer를 인코딩해서 콘솔에 띄운 부분을 지우고 OnRecv를 호출하여 컨텐츠 단에서 해결하도록 한다. (Receive Handler와 연결)

   b) OnRecv 인터페이스에서는 컨텐츠가 해야할 일을 정의: 여기서는 기존의 콘솔에 띄운 것을 코딩

        void OnRecvCompleted(Object sender, SocketAsyncEventArgs args)
        {
            if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
            {
                try
                {
                    // Receive Handler 연결
                    OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));

                    RegisterRecv();
                }
                catch(Exception e)
                {
                    Console.WriteLine($"OnRecvCompleted Failed{e}");
                }
            }
            else
            {
                // TO DO Disconnect
                Disconnect();
            }
        }
        public override void OnRecv(ArraySegment<byte> buffer)
        {
            // 성공 -> 버퍼의값을 받는다. 
            string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
            Console.WriteLine($"[From Client] {recvData}");
        }

 4) Send 부분 

   a) 위에와 마찬가지로 OnSend로 Handler를 연결한다. 

        void OnSendCompleted(object sender, SocketAsyncEventArgs args)
        {
            lock (_lock)
            {
                if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
                {
                    try
                    {
                        // BufferList 와 _pendingList 초기화 
                        _sendArgs.BufferList = null;
                        _pendingList.Clear();

                        // Send Handler 연결
                        OnSend(_sendArgs.BytesTransferred);

                        if(_sendQueue.Count > 0)
                        {
                            RegisterSend();
                        }
                        
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine($"OnSendCompleted Failed{e}");
                    }
                }
                else
                {
                    Disconnect();
                }
            }  
        }
        public override void OnSend(int numOfBytes)
        {
            Console.WriteLine($"Transferred bytes: {numOfBytes}");
        }

 5) Connected 부분

   a) 클라이언트가 서버와 Connected를 하는 부분은 Program.cs에서 OnAcceptHandler로 정의하여 Listener.cs에서 OnAcceptComplete 함수에서 onAcceptHandler를 Invoke 켜서 클라이언트와 연결이 됐음을 알렸다. 

    - OnAcceptHandler는 현재 콘텐츠 단에 있는데 OnAcceptHandler 함수 안에 Session.Start하는 부분은 엔진 단으로 해야함 

   b) OnAcceptHandler에서 session.Start하는 부분을 Listener.cs의 OnAcceptCompleted로 옮겨서 콘텐츠단과 엔진 단 분리 

    - 문제: GameSession을 강제로 OnAcceptHandler에서 만들어주고 있음. 하지만 GameSession은 콘텐츠단에서 인터페이스를 정의 중 

       -> 나중에 GameSession이 아닌 다른 클래스의 이름으로 상속받을 수 있는데 Listener.cs(엔진)에서 GameSession으로 강제할 수 없다. 

        void OnAcceptComplete(object sender, SocketAsyncEventArgs args)
        {
            // 에러가 없이 실행이 되었다. 
            if (args.SocketError == SocketError.Success)
            {
                // TO DO
                GameSession session = new GameSession();
                session.Start(args.AcceptSocket);
                session.OnConnected(args.AcceptSocket.RemoteEndPoint);

            }
            else
                Console.WriteLine(args.SocketError.ToString());

            // 다음 턴을 위해 다시 Register 등록
            RegisterAccept(args);
        }

   c) Listener.cs에서 Action<Socket>으로 OnAcceptHandler를 받는게 아니라 Func<Session> _sessionFactory로 받도록한다. 

    a) Func 델리케이트와 Action 델리게이트는 같은 역할을 하지만, 가장 큰 차이점은 Func는 결과값이 반환되느냐의 차이 

      -> 반환값이 없이 사용하고 싶으면 Action을 사용한다. 

    b) 아래의 코드와 같이 Listener.cs.에서 Func<Session> _sessionFactory로 Session값을 return하는 Func를 만든다. 

      - Init 함수에서 OnAcceptHandler를 받지않고 Func<Session>을 인자로 받아서 delegate로 연결해준다. 

    class Listener
    {
        Socket _listenSocket;
        Func<Session> _sessionFactory;

        public void Init(IPEndPoint endPoint, Func<Session> sessionFactory)
        {
            _listenSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
            _sessionFactory += sessionFactory; // _onAcceptHandler에다가 연결

    c) OnAcceptComplete 함수에서 new GameSession 객체를 생성하는 것이 아닌 delegate로 연결된 _sessionFactory.Invoke() 로 생성된 Session 객체를 반환하도록한다. 

    d) Program.cs에서는 _listener.Init에서 람다로 GameSession을 생성하여 반환하도록한다.

    -> 이렇게하면 컨텐츠 단에서는 Session을 상속받은 GameSession을 생성하여 엔진에게 넘겨주고 엔진은 컨텐츠의 내용에 상관없이 클라이언트와 Connect, Receive, Send, Disconnect 하게 됨 

    -> 컨텐츠단과 서버 엔진단이 분리가 되었다. 

* Program.cs에서 클라이언트한테 Send하고 Thread.Sleep하고 Disconnect하는 부분은 우선 OnConnected에다가 하도록: OnAcceptHandler 필요없게 됨.

 

 6) 실행하면 이전과 같이 프로그램이 동작 

 

4. 문제 사항

  - Listener.cs에서 OnAcceptComplete 함수에서 session.OnConnected의 인자로 args.AcceptSocket.RemoteEndPoint를 주고 있는데 그 사이에 클라이언트가 연결을 끊으면 문제가 발생한다. 

 

* Program.cs 전체 코드 

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

namespace SeverCore
{
    // 컨텐츠 단
    class GameSession : Session
    {
        public override void OnConnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnConnected : {endPoint}");

            // SendBuff만 남겨둠 
            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 Main(string[] args)
        {
            // DNS (Donmain Name System)
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            // IPEndPoint: 도착 IP 주소 
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // ip주소, port 번호 
            
            _listener.Init(endPoint, () => { return new GameSession(); });
            Console.WriteLine("Listening....");

            while (true)
            {
                
            }
        }
    }
}

* Listener.cs 전체 코드 

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

namespace SeverCore
{
    class Listener
    {
        Socket _listenSocket;
        Func<Session> _sessionFactory;

        public void Init(IPEndPoint endPoint, Func<Session> sessionFactory)
        {
            _listenSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
            _sessionFactory += sessionFactory; // _onAcceptHandler에다가 연결 

            // 문지기 교육
            _listenSocket.Bind(endPoint);

            // 영업 시작
            // backlog: 최대 대기수 
            _listenSocket.Listen(10);

            for(int i = 0; i < 10; i++)
            {
                // 초기화 시에 이벤트 등록 
                SocketAsyncEventArgs args = new SocketAsyncEventArgs();
                args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptComplete);
                RegisterAccept(args); // 최초 한번은 args를 등록 
            }
        }

        void RegisterAccept(SocketAsyncEventArgs args)
        {
            // 이전의 데이터 날리기 
            args.AcceptSocket = null; 

            // 당장 완료에 대한 보장없이 Accept
            bool pending = _listenSocket.AcceptAsync(args);
            if (pending == false) // pending 없이 바로 클라이언트의 connect이 와서 accept했다는 것을 의미 
                OnAcceptComplete(null, args);
        }

        void OnAcceptComplete(object sender, SocketAsyncEventArgs args)
        {
            // 에러가 없이 실행이 되었다. 
            if (args.SocketError == SocketError.Success)
            {
                // TO DO
                Session session = _sessionFactory.Invoke();
                session.Start(args.AcceptSocket); // AcceptSocket이 클라이언트의 소켓 
                session.OnConnected(args.AcceptSocket.RemoteEndPoint);

            }
            else
                Console.WriteLine(args.SocketError.ToString());

            // 다음 턴을 위해 다시 Register 등록
            RegisterAccept(args);
        }
        public Socket Accept()
        {
            return null;
        }
    }
}

* Session.cs 전체 코드 

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

namespace SeverCore
{
    abstract class Session
    {
        Socket _socket;
        int _disconnected = 0;

        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 void 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);
            // Recv Buffer 설정
            _recvArgs.SetBuffer(new byte[1024], 0, 1024);

            _sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
            RegisterRecv();
        }

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

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

            // Disconnected Handler 연결
            OnDisconnected(_socket.RemoteEndPoint);

            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }
        #region 네트워크통신 

        // 비동기는 두단계로 나눠짐
        // Send
        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
                    {
                        // BufferList 와 _pendingList 초기화 
                        _sendArgs.BufferList = null;
                        _pendingList.Clear();

                        // Send Handler 연결
                        OnSend(_sendArgs.BytesTransferred);

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

        // Receive
        void RegisterRecv()
        {
            bool pending = _socket.ReceiveAsync(_recvArgs);
            if (pending == false) // pending이 false이면 기다릴필요없이 바로 메세지를 받을 수 있음 
                OnRecvCompleted(null, _recvArgs);
        }

        void OnRecvCompleted(Object sender, SocketAsyncEventArgs args)
        {
            if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
            {
                try
                {
                    // Receive Handler 연결
                    OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));

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