[Part 4 게임 서버] 네트워크 프로그래밍 2 (Listener)

<Listener>

1. 이전 시간의 코드를 분리해서 정리 

: Program.cs의 main 함수에 모든 것을 넣는 것이 아닌 Listener 소켓을 따로 빼어 정리 

 

1) SeverCore 프로젝트 우클릭 -> 추가 -> 새항목 추가: Listener.cs 만들기 

2) Listener 클래스에서 _listenSocket을 만든다 .

3) Init 함수를 구현하여 해당 함수 내에 _listenSocket 객체를 생성한다. -> 문지기기를 교육(endPoint 주소 할당) -> 영업을 시작하겠다는 Listen을 한다. 

class Listener
    {
        Socket _listenSocket;

        public void Init(IPEndPoint endPoint)
        {
            _listenSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);

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

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

4) Accept함수를 만들어서 _listenSocket.Accept()를 한다. 

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;

        public void Init(IPEndPoint endPoint)
        {
            _listenSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);

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

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

        public Socket Accept()
        {
            return _listenSocket.Accept();
        }
    }
}

 5) Program.cs의 Main 함수에서 Listener 클래스를 이용하여 코드를 수정하면 아래와 같다.

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 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 번호 

            try
            {
                _listener.Init(endPoint);

                while (true)
                {
                    Console.WriteLine("Listening....");

                    // 손님을 입장시킨다. 
                    Socket clientSocket = _listener.Accept(); // 세션의 소켓

                    // 받는다. 
                    byte[] recvBuff = new byte[1024];
                    int recvBytes = clientSocket.Receive(recvBuff);
                    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                    Console.WriteLine($"[From Client] {recvData}");

                    // 보낸다.
                    byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server!");
                    clientSocket.Send(sendBuff);

                    // 쫓아낸다.
                    clientSocket.Shutdown(SocketShutdown.Both);
                    clientSocket.Close();
                }
            }
            catch(Exception e)
            {
                Console.WriteLine(e.ToString());
            }


        }
    }
}

2. 문제: Blocking 계열의 함수를 사용한다. ex) Accept 함수

: SeverCore의 listenerSocket의 Accept 함수는 Client의 소켓이 Connect 함수를 이용하여 서버와 연결하겠다고 말하지않는 이상 무한정 기다리게 된다

-> Blocking 계열의 함수를 사용하여 Send와 receive를 하게 되면 상대방이 나에게 메세지를 보내기 전까지 무한정 대기를 하게됨 

-> NoneBlocking 함수로 바꿔야함

 

1) 비동기함수: AcceptAsync 사용

 - 기존의 함수코드처럼 동기적으로 코드의 위에서 아래로 실행되는 것이 아닌 비동기적으로 특정 코드의 처리가 끝나기 전에 다음 코드를 실행할 수 있는 것을 뜻한다. 

 - 비동기함수를 이용하여 우선 Accept를 한 후, 클라이언트에서 콜백으로 연락을 주도록 구현

 -> Accept 하는 부분과 실제로 처리되어 완료되는 부분, 2개로 나눠져야한다. 

 

2) RegisterAccept 함수: 당장 완료되었다는 보장 없이 Accept 하겠다. 

 a) AcceptAsync 함수의 return값이 bool: pending 여부를 알려줌 

 b) pending이 false이면 pending 없이 바로 클라이언트의 connect이 와서 accept을 했다는 것을 의미 -> OnAcceptComplete 호출

     만일 pending이 true이면, SocketAsyncEventArgs의 Event Handler를 통해 AcceptAsync가 완료되었을때, 거꾸로 알려주도록 코드를 구현해야한다. 

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

 c) init 함수에서 SocketAsyncEventArgs를 만들어준다. 

 args.CompletedEventHandler 이기 때문에 델리게이트로 콜백으로 OnAcceptCompleted라는 것을 받아주고 싶다고 코딩

 * OnAcceptCompleted 함수의 형식을 맞춰준다. 

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

d) 초기화시에 RegisterAccept함수에 args 등록 

-> 클라이언트에서 Connect 요청이 오면 콜백방식으로 OnAcceptCompleted가 호출이 된다. 

* AcceptAsync로 비동기적으로 실행: 만일 클라이언트의 Connect이 없으면 다음으로 넘어갔다가 Connect이 있을때 콜백으로 OnAcceptCompleted를 실행한다. (EventHandler ~(OnAcceptCompleted)에서 실행)

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;

        public void Init(IPEndPoint endPoint)
        {
            _listenSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);

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

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

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

        }

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

    }
}

3) OnAcceptComplete 함수 

 a) 결국 클라이언트가 Connect을 하게 되면 OnAcceptComplete 함수로 오게된다. SocketError를 확인한 후, 모든 일을 끝내면 다시 클라이언트와의 Connect을 위해 RegisterAccept을 등록해야한다. 

        void OnAcceptComplete(object sender, SocketAsyncEventArgs args)
        {
            // 에러가 없이 실행이 되었다. 
            if (args.SocketError == SocketError.Success)
            {
                // TO DO
            }
            else
                Console.WriteLine(args.SocketError.ToString());

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

b) TO DO 에서 이전 시간의 Program.cs의 메인 함수에서 클라이언트와 메세지를 Recieve & Send 했던 것을 구현해야한다

 - 위의 처럼 콜백 방식을 이용하기 위해 Action<Socket> _onAcceptHandler를 선언한다.

 - Init 함수에서도 Action을 인자로 받도록하여 _onAcceptHandler와 onAcceptHandler를 델리게이트로 연결되도록한다. 

    class Listener
    {
        Socket _listenSocket;
        Action<Socket> _onAcceptHandler;

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

 c) 이전 Program.cs의 main 함수에서 소켓이 Accept한 후에 클라이언트의 Socket을 반환하였다. 

  - 위에 처럼 Client Socket을 반환하기 위해 args.AcceptSocket 이용: ClientSocket이 저장되어 있음 

  - OnAcceptCompleted 함수의 TO DO에서 _onAcceptHnadler.Invoke의 인자로 args.AcceptSocket을 넘겨준다. 

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;
        Action<Socket> _onAcceptHandler;

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

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

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

            // 초기화 시에 이벤트 등록 
            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
                _onAcceptHandler.Invoke(args.AcceptSocket); // ClientSocket을 return해주는 부분이 AcceptSocket이다 .
            }
            else
                Console.WriteLine(args.SocketError.ToString());

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

 d) Program.cs의 main 함수에서 OnAcceptHandler 함수를 정의한다. 

  - ClientSocket을 인자로 받고 Receive & Send를 해준 부분을 다음과 같이 옮긴다. 

  - Main 함수에서는 이제 콜백으로 Event로 클라이언트와 통신하기 때문에  while문에서 필요한 코드가 없으나 프로그램을 종료시키지 않기 위해 작성

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 Program
    {
        static Listener _listener = new Listener();

        static void OnAcceptHandler(Socket clientSocket)
        {
            try
            {
                // 받는다. 
                byte[] recvBuff = new byte[1024];
                int recvBytes = clientSocket.Receive(recvBuff);
                string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                Console.WriteLine($"[From Client] {recvData}");

                // 보낸다.
                byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server!");
                clientSocket.Send(sendBuff);

                // 쫓아낸다.
                clientSocket.Shutdown(SocketShutdown.Both);
                clientSocket.Close();
            }
            catch(Exception e)
            {
                Console.WriteLine(e);
            }

        }
        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, OnAcceptHandler);
            Console.WriteLine("Listening....");

            while (true)
            {
                
            }
        }
    }
}

4) RegisterAccept 함수에서 SocketAsyncEventArgs의 args를 계속해서 재사용하기 때문에 초기화 하는 작업이 중요하다. 

 - 아래와 같이 args.AcceptSocket을 null로 초기화 

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

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

 

3. Test

 - 아래와 같이 DummyClient에서 여러개의 메세지를 보내도록 While 문으로 감싼다

 - Thread.Sleep으로 약간의 타임을 늦춘다. (너무 빠르면 과부화)

 - 결과 아래와 같이 계속 통신

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 DummyClient
{
    class Program
    {
        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 번호 

            while (true)
            {
                // 휴대폰 설정
                Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);

                try
                {

                    // 문지기에게 문의
                    socket.Connect(endPoint);
                    Console.WriteLine($"Connect To {socket.RemoteEndPoint.ToString()}");

                    // 보낸다.
                    byte[] sendBuff = Encoding.UTF8.GetBytes("Hello World!");
                    int sendBytes = socket.Send(sendBuff);

                    // 받는다
                    byte[] recvBuff = new byte[1024];
                    int recvBytes = socket.Receive(recvBuff);
                    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                    Console.WriteLine($"[From Server] {recvData}");

                    // 나간다
                    socket.Shutdown(SocketShutdown.Both);
                    socket.Close();
                    
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.ToString());
                }

                Thread.Sleep(100);
            }
        }
    }
}

etc-image-0

** 전체 코드

- SeverCore에서 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;
        Action<Socket> _onAcceptHandler;

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

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

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

            // 초기화 시에 이벤트 등록 
            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
                _onAcceptHandler.Invoke(args.AcceptSocket); // ClientSocket을 return해주는 부분이 AcceptSocket이다 .
            }
            else
                Console.WriteLine(args.SocketError.ToString());

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

- SeverCore에서 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 Program
    {
        static Listener _listener = new Listener();

        static void OnAcceptHandler(Socket clientSocket)
        {
            try
            {
                // 받는다. 
                byte[] recvBuff = new byte[1024];
                int recvBytes = clientSocket.Receive(recvBuff);
                string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                Console.WriteLine($"[From Client] {recvData}");

                // 보낸다.
                byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server!");
                clientSocket.Send(sendBuff);

                // 쫓아낸다.
                clientSocket.Shutdown(SocketShutdown.Both);
                clientSocket.Close();
            }
            catch(Exception e)
            {
                Console.WriteLine(e);
            }

        }
        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, OnAcceptHandler);
            Console.WriteLine("Listening....");

            while (true)
            {
                
            }
        }
    }
}

- Dummy Client의 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 DummyClient
{
    class Program
    {
        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 번호 

            while (true)
            {
                // 휴대폰 설정
                Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);

                try
                {

                    // 문지기에게 문의
                    socket.Connect(endPoint);
                    Console.WriteLine($"Connect To {socket.RemoteEndPoint.ToString()}");

                    // 보낸다.
                    byte[] sendBuff = Encoding.UTF8.GetBytes("Hello World!");
                    int sendBytes = socket.Send(sendBuff);

                    // 받는다
                    byte[] recvBuff = new byte[1024];
                    int recvBytes = socket.Receive(recvBuff);
                    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                    Console.WriteLine($"[From Server] {recvData}");

                    // 나간다
                    socket.Shutdown(SocketShutdown.Both);
                    socket.Close();
                    
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.ToString());
                }

                Thread.Sleep(100);
            }
        }
    }
}

* 참고 자료 

https://www.howdy-mj.me/javascript/async/

 

자바스크립트 비동기 함수 알아보기

비동기(Asynchronous…

www.howdy-mj.me