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

<Session #1>

1. 이전 시간 의문

 a) 만약 RegisterAccept에서 계속 pending이 false면 OnAcceptCompleted 함수와 RegisterAccept 함수가 재귀적으로 호출되어 스택오버플로우 발생? 

  - ListenSocket의 Listen에서 backlog를 통해 최대 대기수를 정해줬기때문에 동시다발적으로 false만 뜰 수 없음 

    -> 의도적으로 많은 User들이 공격하지 않는 이상 발생할 수 없음

 

 b) Listen Socket을 하나만 만들었다. -> 굉장히 많은 User가 게임에 들어와서 서버와 통신을 하려고 할때 어떻게?

  - SocketAsyncEventArgs를 여러개 만들 수 있다. -> SocketAsyncEventArgs는 독립적으로 실행되기때문에 아래와 같이 for 문으로 여러개 만들 수 있다. (Init 함수에서)

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

 c) 프로그램을 실행하면, main 함수의 While문을 계속 돌고 있어야하는데 어떻게 콜백으로 OnAcceptComplete 함수를 실행시키는가?

 - OnAcceptComplete 함수의 if문에 breakpoint를 두고 debug를 하면 스레드가 주 스레드작업 스레드로 나뉘게 된다. 

    -> 비동기적으로 수행할 경우, 자동적으로 Task Pool에서 Task를 가져와 각자 실행된다는 것을 알 수 있다. 

    -> 만약 주 스레드와 Task 스레드가 같은 데이터를 건들게 될 경우 Race Condition이 발생할 수 있다 (OnAcceptComplete 함수 부분이 레드 존, Danger 존이 된다. )

    * 멀티 스레드로 실행되기 때문에

 

2. Receive 부분 빼기 

 : Receive 부분도 Listen과 같이 비동기 방식으로 바꿔준다. 

 1) SeverCore프로젝트 우클릭 -> 새항목 추가 -> Session.cs 만들기: Session 클래스를 만든다. 

   - Init 함수의 인자로 socket을 받는다. 

   - 이전시간과 마찬가지로 비동기 방식에서는 RegisterRecv와 OnRecvCompleted의 두가지 함수가 필요하다

  2) Receive에서도 AcceptAsync처럼 ReceiveAsync가 있다. 

   - 마찬가지로 SocketAsyncEventArgs를 Init 함수에 선언하고 EventHandler로 OnRecvCompleted를 콜백으로 받도록한다. 

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

namespace SeverCore
{
    class Session
    {
        Socket _socket;

        public void Init(Socket socket)
        {
            _socket = socket;
            SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
            recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);


        }

        // 비동기는 두단계로 나눠짐

        void RegisterRecv(SocketAsyncEventArgs args)
        {
            _socket.ReceiveAsync(args);
        }

        void OnRecvCompleted(Object sender, SocketAsyncEventArgs args)
        {

        }
    }
}

 3) Receive 할때는 메세지를 받아서 저장할 Buffer가 필요했었다. 

  - 아래의 recvBuff가 필요: recvArgs.SetBuffer 함수를 통해 버퍼를 설정한다. 

  - 후에 마찬가지로 RegisterRecv를 등록한다. 

		// 받는다. 
                byte[] recvBuff = new byte[1024];
                int recvBytes = clientSocket.Receive(recvBuff);
        public void Init(Socket socket)
        {
            _socket = socket;
            SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
            recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
            // Buffer 설정
            recvArgs.SetBuffer(new byte[1024], 0, 1024);
            RegisterRecv(recvArgs);
        }

 4) RegisterRecv 함수에서는 마찬가지로 ReceiveAsync 함수의 반환값을 bool으로 받는다. 

  - pending 값이 false이면 기다릴 필요없이 바로 받을 메세지가 왔다는 것을 뜻한다. 

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

 5) OnRecvCompleted 함수에서 버퍼의 값을 가져온다 

   a) args.BytesTransferred: 클라이언트로 가져온 Bytes의 수로 0을 넘는지 확인 -> 클라이언트가 예기치 못하게 연결을 끊는 경우 0바이트로 통신이 오기때문에 해당 값이 0이 넘어야 정상 작동

   b) 그리고 SocketError가 없을 시에만 버퍼의 값을 Encoding 한다. 

      - 이때, args에 Buffer, Offset, BytesTransferred의 내용이 다 담겼으므로 해당 사항을 이용 

   c) 후 다시 RegisterRecv를 등록한다. 

        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(args);
                }
                catch(Exception e)
                {
                    Console.WriteLine($"OnRecvCompleted Failed{e}");
                }
            }
            else
            {
                // TO DO Disconnect
            }
        }

 6) Send 하는 부분은 어렵기 때문에 그냥 간단하게 blocking 방식으로 우선 코딩한다. 

        public void Send(byte[] sendBuff)
        {
            _socket.Send(sendBuff);
        }

 7) Disconnect 부분도 이전과 같이 Shutdown과 Close를 사용

        public void Disconnect()
        {
            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }

* Session.cs의 전체 코드 

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

namespace SeverCore
{
    class Session
    {
        Socket _socket;

        public void Init(Socket socket)
        {
            _socket = socket;
            SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
            recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
            // Buffer 설정
            recvArgs.SetBuffer(new byte[1024], 0, 1024);
            RegisterRecv(recvArgs);
        }

        public void Send(byte[] sendBuff)
        {
            _socket.Send(sendBuff);
        }

        public void Disconnect()
        {
            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }
        #region 네트워크통신 
        // 비동기는 두단계로 나눠짐

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

        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(args);
                }
                catch(Exception e)
                {
                    Console.WriteLine($"OnRecvCompleted Failed{e}");
                }
            }
            else
            {
                // TO DO Disconnect
            }
        }
        #endregion
    }
}

3. ServerCore의 Program.cs 부분 수정

 1) 아래와 같이 수정한다. 

 2) Socket의 Accept을 하여 클라이언트와 Connect되면 Session 객체를 만든다. 

* Init -> Start로 이름 변경 

 3) Session.Start를 하면 비동기적으로 Receive 가능 

   - Send와 Disconnect는 아직까지 Blocking 함수를 사용하므로 따로 작성해준다. 

        static void OnAcceptHandler(Socket clientSocket)
        {

            try
            {
                Session session = new Session();
                session.Start(clientSocket);

                // SendBuff만 남겨둠 
                byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server!");
                session.Send(sendBuff);

                Thread.Sleep(1000);

                session.Disconnect();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
        }

4. Dummy Client의 Program.cs 수정

: for 문으로 5번 정도 send하도록 한다. 

                    for(int i = 0; i < 5; i++)
                    {
                        // 보낸다.
                        byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello World! {i}");
                        int sendBytes = socket.Send(sendBuff);
                    }

5. 실행

: 위의 코드로 실행하게 되면 Client와 Sever가 서로 잘 주고 받는다. 

6. Disconnect 문제

: OnRecvCompleted 함수는 인자로만 이뤄져서 코드를 짜기 때문에 문제될 부분이 없으나 멀티스레드 환경에서 동시에 여러번 Disconnect 함수가 발생할 수 있다. 

 1) Program.cs에서 두번 Session.Disconnect를 하게 되면 error 발생

                session.Disconnect();
                session.Disconnect();

 2) Session에서 flag로 현재 Session이 끊겼는지 여부 확인

  a) 멀티스레드 환경이기 때문에 Interlocked를 이용하여 원자적으로 flag 변경

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;

        public void Start(Socket socket)
        {
            _socket = socket;
            SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
            recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
            // Buffer 설정
            recvArgs.SetBuffer(new byte[1024], 0, 1024);
            RegisterRecv(recvArgs);
        }

        public void Send(byte[] sendBuff)
        {
            _socket.Send(sendBuff);
        }

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

            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }
        #region 네트워크통신 
        // 비동기는 두단계로 나눠짐

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

        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(args);
                }
                catch(Exception e)
                {
                    Console.WriteLine($"OnRecvCompleted Failed{e}");
                }
            }
            else
            {
                // TO DO Disconnect
            }
        }
        #endregion
    }
}

 b) 이렇게 하면 Disconnect를 두번하여도 문제없이 동작한다. 

 

 

** 중간 이해 

                                             Listen - Sever  ------------------------------------> Client

                                             Receive -       <------------------------------------

 

1) 서버와 클라이언트가 소켓 통신을 한다. 

2) 현재까지는 비동기적으로 Listen과 Receive를 함

3) 서버에서는 메세지를 Receive 하고 메세지를 Send한 후 통신을 Disconnect하고 Client에서는 while문으로 계속 메세지를 Send하고 Receive한다. (반대로)

4) 이때 서버가 이미 disconnect했는데 또 한번 Disconnect하면 문제 발생 

 

<Session #2>

1. 위의 코드에서 Receive Event(SocketAsynEventArgs를 하나만 선언하고 RegisterRecv에 할당)가 하나 밖에 없기 때문에 OnRecvCompleted 함수에 하나의 스레드밖에 들어올 수 없음.

-> But, Send는 다르다. 

 

2. Receive와 기본적인 구조는 동일하게 코딩한다. 

-> Receive는 클라이언트가 메세지를 보낼때 실행이되지만, Send는 메세지를 보낼 시점알 수 없다. (미래를 예측하여 Send할 수 없다.)

       // Send
        void RegisterSend(SocketAsyncEventArgs args)
        {
            bool pending = _socket.SendAsync(args);
            if (pending == false)
                OnSendCompleted(null, args);
                
        }
        void OnSendCompleted(object sender, SocketAsyncEventArgs args)
        {
            if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
            {
                try
                {

                }
                catch(Exception e)
                {
                    Console.WriteLine($"OnSendCompleted Failed{e}");
                }
                
            }
            else
            {
                Disconnect();
            }

        }

3. Send하는 시점에 SocketyAsyncEventArgs가 RegisterSend에 등록이 되어야함

 1) 따라서, Start(초기화)함수가 아니라 Send 함수에 코드를 넣어줘야한다. 

   - Buffer 설정만 sendBuff 인자로 한다. 

        public void Send(byte[] sendBuff)
        {
            //_socket.Send(sendBuff);
            SocketAsyncEventArgs sendArgs = new SocketAsyncEventArgs();
            sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
            // Buffer 설정
            sendArgs.SetBuffer(sendBuff, 0 , sendBuff.Length);

            RegisterSend(sendArgs);
        }

 2) Receive했을 때처럼 OnRecvComplete 함수에서 버퍼값을 완전히 받은 후(완료한 후), RegisterRecv로 다시 던져줄 수 없다. 

  a) Send 함수가 호출된 시점에 Socket Event가 연결되기 때문에 OnSendCompleted에서 try문에 RegisterSend(args)를 하면 이미 Send한 메세지에 대한 args에 대해 등록하는 것

  -> Args를 재사용할 수 없다. 

  b) 물론 위의 코드로 실행하면 이전처럼 실행은 됨: session.Send 함수 호출할때마다 SocketAsyncEventArgs가 생성되고 RegisterSend(sendArgs)로 등록면서 바로 보내진다. (Send 함수가 호출됐다는 것이 Send Event가 발생했다는 것을 의미 SetBuffer에서 해당 글자만큼 버퍼를 설정하면 보내면됨 * 딱히 콜백으로 OnSendCompleted 함수에서 할 것이 없다.)

 

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;

        public void Start(Socket socket)
        {
            _socket = socket;
            SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
            recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
            // Buffer 설정
            recvArgs.SetBuffer(new byte[1024], 0, 1024);
            RegisterRecv(recvArgs);
        }

        public void Send(byte[] sendBuff)
        {
            //_socket.Send(sendBuff);
            SocketAsyncEventArgs sendArgs = new SocketAsyncEventArgs();
            sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
            // Buffer 설정
            sendArgs.SetBuffer(sendBuff, 0 , sendBuff.Length);

            RegisterSend(sendArgs);
        }

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

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

        // 비동기는 두단계로 나눠짐
        // Send
        void RegisterSend(SocketAsyncEventArgs args)
        {
            bool pending = _socket.SendAsync(args);
            if (pending == false)
                OnSendCompleted(null, args);
                
        }
        void OnSendCompleted(object sender, SocketAsyncEventArgs args)
        {
            if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
            {
                try
                {

                }
                catch(Exception e)
                {
                    Console.WriteLine($"OnSendCompleted Failed{e}");
                }
                
            }
            else
            {
                Disconnect();
            }

        }

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

        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(args);
                }
                catch(Exception e)
                {
                    Console.WriteLine($"OnRecvCompleted Failed{e}");
                }
            }
            else
            {
                // TO DO Disconnect
                Disconnect();
            }
        }
        #endregion
    }
}

3) Send를 많이 호출하는데 SendAsync로 하면 부화가 일어난다

   - User1이 왼쪽으로 움직이면, 서버는 나머지 모든 유저들에게 왼쪽으로 움직였다는 메세지를 줘야한다. + 다른 유저들도 스킬과 위치 이동등 다양한 기술을 사용. 그럴때마다 SendAsync를 사용하면 부화 발생 

 

4. 해결 방안

 1) SocketAsyncEventArgs _sendArgs를 아래와 같이 클래스의 멤버 변수로 선언한 후, Srtart에서 EventHandler를 추가한다. 

 2) Send 함수에 SetBuffer로 Buffer만 설정하고 RegisterSend로 등록한다. 

* 클래스내의 멤버변수 _sendArgs이기 때문에 RegisterSend의 인자로 넘겨주지 않아도됨 

    class Session
    {
        Socket _socket;
        int _disconnected = 0;
        SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();

        public void Start(Socket socket)
        {
            _socket = socket;
            SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
            recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
            // Buffer 설정
            recvArgs.SetBuffer(new byte[1024], 0, 1024);

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

        public void Send(byte[] sendBuff)
        {
            //_socket.Send(sendBuff);
            // Buffer 설정
            _sendArgs.SetBuffer(sendBuff, 0 , sendBuff.Length);
            RegisterSend();
        }

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

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

        // 비동기는 두단계로 나눠짐
        // Send
        void RegisterSend()
        {
            bool pending = _socket.SendAsync(_sendArgs);
            if (pending == false)
                OnSendCompleted(null, _sendArgs);
                
        }

 3) Send 함수에서 매번마다 Register를 하는 것이 아니라 Queue에다가 데이터를 차곡 차곡 쌓은 후에 OnSendComplete 함수가 완료된 후에 한번에 보내도록 하는 방식

  - OnSendComplete가 완료되기 전까지는 Queue에다가 쌓아놓는다.  

  a) Session의 클래스 멤버로 Queue와 bool pending을 선언한다. 

   - _pending은 누가 Register를 하고 있으면 True로 하고 OnSendComplete를 완료한 후에 False로 바꾼다. 

   - Send에서 _pending 값이 false이면 RegisterSend를 등록한다. (_peding이 true이면 누가 아직 보내고 있다는 뜻으로 Queue에다가 쌓기만 한다.)

   - 이때 여러 스레드가 Send 함수에 접근할 수 있기 때문에 lock을 걸어준다. 

     -> 한번에 한명씩만

    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에다가 저장만 하겠다.
        public void Send(byte[] sendBuff)
        {
            lock (_lock)
            {
                _sendQueue.Enqueue(sendBuff);
                if (_pending == false)
                    RegisterSend();
            }
        }

  b) RegisterSend 함수 

  - 보내려는 중이라는 것을 알려야하기 때문에 _pending = true로 변경한다.  

  - _sendQueue에서 Dequeue()로 byte를 꺼내온 후 _sendArgs.SetBuffer로 꺼내온 byte에 대한 정보로 버퍼를 설정한다. 

  - 그 이후 SendAsync로 비동기적으로 송신한다. 이때, pending이 false (Send할것이 없으면) 바로 OnSendCompleted를 하여 보내고 true면 비동기적으로 나중에 송신을 하겠다는 것을 의미

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

 c) OnSendCompleted 함수

  - Send가 완료된 후에 콜백으로 OnSendCompleted 함수가 실행됨 

    -> 완료됨을 알리기 위해 _pending = false로 변경

        void OnSendCompleted(object sender, SocketAsyncEventArgs args)
        {
            if(args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
            {
                try
                {
                    _pending = false;
                }
                catch(Exception e)
                {
                    Console.WriteLine($"OnSendCompleted Failed{e}");
                }
                
            }
            else
            {
                Disconnect();
            }
        }

  - RegisterSend와 콜백 두가지 경우로 OnSendCompleted가 호출되기 때문에 lock으로 묶어준다. 

  - 현재 누군가가 RegisterSend를 등록하여 _pending이 true인 상태에서 어떤 스레드가 Send를 호출하였는데 _pending이 true여서 RegisterSend를 못할 수도 있다. (Enqueue에만 쌓인다.)

    -> OnSendComplete 함수에서 내가 보내고 있는 사이에 Queue가 쌓이면 다시 RegiseterSend를 해주면 더 우아하게 코딩 가능 

* 위의 작업이 없으면 계속하여 Queue에 쌓인다. 

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

=> 주목적: sendArgs 재사용 & 누가 보내고 있는 작업을 완료하지않았다면 Queue에 쌓이도록 

* 물론 여전히 Send를 100번 하면 SendAsync를 100번해야하는 문제가 있다. 

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

        public void Start(Socket socket)
        {
            _socket = socket;
            SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
            recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
            // Recv Buffer 설정
            recvArgs.SetBuffer(new byte[1024], 0, 1024);

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

        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(SocketAsyncEventArgs args)
        {
            bool pending = _socket.ReceiveAsync(args);
            if (pending == false) // pending이 false이면 기다릴필요없이 바로 메세지를 받을 수 있음 
                OnRecvCompleted(null, args);
        }

        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(args);
                }
                catch(Exception e)
                {
                    Console.WriteLine($"OnRecvCompleted Failed{e}");
                }
            }
            else
            {
                // TO DO Disconnect
                Disconnect();
            }
        }
        #endregion
    }
}