320x100
728x90

 

앞서 진행한 소켓 프로그래밍 구현에서 ServerCore 폴더에 

Listener 파일을 추가한다. 

 

 

문지기 즉, 소켓을 만드는 것을 따로 파일로 빼서 관리할 예정이다. 

 

 

 

 

 

생성된 Listener 클래스 안에 소켓을 생성한다. 

Socket _listenSocket;

 

 

 

 

소켓을 초기화 하는 함수는 따로 만들 것이다. 

기존 코드를 보면 (빨간 줄 무시)

 

 

 

 

endPoint를 받아서 소켓을 생성하고 Bind도 하는 것을 볼 수 있다. 

(*기존 코드 : 전 글 확인..)

 

 

 

따라서 Init함수 인자로 endPoint를 넣어주며 Init() 함수를 다음과 같이 만들어 준다. 

 

 

 

Init()

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

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

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

}

 

 

 

그 후 Accept는 따로 Accept 함수를 만들어 빼준다. 

 

 

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

 

 

 

 

이제 Program.cs 파일로 돌아가서 문지기 역할을 하는 Listener를 생성해준다. 

그리고 문지기를 초기화 하는 코드는 listener의 Init 함수와 Accept 함수 호출로 대체해준다. 

class Program
{

    static Listener _listener = new Listener();

    static void Main(string[] args)
    {
        ... 

        _listener.Init(endPoint,OnAcceptHandler);
        Console.WriteLine("Listening...");

        while (true)
        {
            Socket clientSocket = _listener.Accept();
        }
        
    }
    
}

 

하지만 현재 형태는 블로킹 함수로 이루어진 상황이다. 

이런 경우 사용자가 10000명이라고 가정했을 때, 무한 대기를 계속 하고 있는 상황이 발생할 수 있으므로

논 블로킹 형태로 구현해야 한다. 

 

 

 

(*블로킹 : 요청받는 함수가 작업을 모두 마치고 나서야 요청자에게 제어권을 넘겨줌 (그동안 요청자는 아무것도 하지않고 기다림)

                                                                    논블로킹 : 요청받은 함수가 요청자에게 제어권을 바로 넘겨줌 (그동안 요청자는 다른 일을 할 수 있음)

 

 

 

 

 

따라서 Accept , Receive, Send는 모두 비동기 형식 (논 블로킹) 으로 바꿔보자 

 

 

 

 

 


 

 

 

 

AcceptAsync()로 비동기 형태로 만들어준다.

이렇게 되면, 클라이언트 요청이 올때까지 무작정 기다리는 것이 아니라 다른 작업을 쭉 수행하다가

요청이 들어오면 그 때 CallBack 방식으로 알려주는 형태이다. 

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

 

 

 

좀 더 세부적으로 함수를 만들어보자.

Accpet 메소드 안에 있는 _listenSocket.AcceptAsync() 코드를 지우고 RegisterAccept 매소드 안에 다음과 같이 넣는다.

요청 여부에 대한 리턴값을 pending에 할당하여 확인하면서 그 값이 false이면 상황을 종료하면 된다. 

 

void RegisterAccept(SocketAsyncEventArgs args)
{
    bool pending = _listenSocket.AcceptAsync(args);
    if (pending == false)
        OnAcceptCompleted(null, args);

}

void OnAcceptCompleted(SocketAsyncEventArgs args)
{
    ...
}

 

 

 

pending이 true일 때는, 아까 말한 거꾸로 메시지를 알려주도록 하는 작업을 해보자. 

초기화 함수에 넣어주어 처음 시작 때 무조건 한번 실행되도록 할 것이다. 

public void Init(IPEndPoint endPoint)
{
    ...      //(위 코드 참고)

    SocketAsyncEventArgs args = new SocketAsyncEventArgs();
    args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
    RegisterAccept(args);

}

 

 

 

EventHandler를 사용하기 위해 콜백 함수인 OnAcceptCompleted를 넣어주고, 

형식을 맞춰주기 위해 object sender라는 인자를 추가한다. 

void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
    if (args.SocketError == SocketError.Success)
    {
        //TODO
        _onAcceptHandler.Invoke(args.AcceptSocket);
    }else
        Console.WriteLine(args.SocketError.ToString());

    RegisterAccept(args);
}

 

 

 

 

accept가 되면 그 이후에 데이터를 보내고 받는 작업을 TODO에서 진행한다. 

아래 작업을 수행한다고 보면 된다. 

 

 

 

 

 

accept가 완료 됐을 때 이를 처리하는 변수를 Action으로 만들어준다. 

다음과 같이 _onAcceptHandler를 만들자. 

 

 

 

 

 

그리고 Init 함수에서 onAcceptHandler를 인자로 추가하고 핸들러를 연결해준다. 

public void Init(IPEndPoint endPoint, Action<Socket> onAcceptHandler)
{
    _listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, 
    ProtocolType.Tcp);
    _onAcceptHandler += onAcceptHandler;


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

    //영업 시작
    //backlog : 최대 대기수 
    _listenSocket.Listen(10);
    
    
    SocketAsyncEventArgs args = new SocketAsyncEventArgs();
    args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
    RegisterAccept(args);

}

 

 

 

그리고 OnAcceptCompleted 매소드를 다음과 같이 수정한다.  

_onAcceptHandler를 Invoke 시키고 소켓을 넣어주면 된다. 

 

Socket clientSocket = listenSocket.Accept();

 

원래 Accept할 때 받은 소켓을 전달해 주는 것이다. 

void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
    if (args.SocketError == SocketError.Success)
    {
        //TODO
        _onAcceptHandler.Invoke(args.AcceptSocket);
    }else
        Console.WriteLine(args.SocketError.ToString());

    RegisterAccept(args);
}

 

 

 

_onAcceptHandler의 Invoke 결과는 다시 프로그램 클래스에게 전달해 주어야 한다. 

다음과 같이 프로그램안에 넣어준다. 

 

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

 

 

 

Main 문은 다음과 같이 수정한다. 

리스너를 통해 초기화 한 부분에 OnAcceptHandler를 넣어준다. 

 

클라이언트가 들어오면 onAcceptHandler를 통해 알려달라는 의미이다. 

static void Main(string[] args)
{
    ...

    _listener.Init(endPoint,OnAcceptHandler);

}

 

 

중요한 것..! 

이벤트를 재사용 하기 위해서는 소켓을 null로 초기화 해 주어야 한다. 

RegisterAccept 메소드에 다음과 같이 추가하자 

 

args.AcceptSocket = null; //이벤트 재사용 시, 초기화 필수

 

 


 

 

 

최종 코드 

 

Program.cs

더보기
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.ToString());
            }
        }
        static void Main(string[] args)
        {
            //DNS (Domain Name System)
            //도메인 등록 후 -> IP주소 찾게끔 만들면 유지보수가 용이함 
            string host = Dns.GetHostName();    //PC의 호스트명을 받는다 (호스트명은 IP 주소 대신 사용할 수 있는 식별 이름) 

            IPHostEntry ipHost = Dns.GetHostEntry(host);  //해당 호스트의 IP 관련 주소들을 받는다. 
            IPAddress ipAddr = ipHost.AddressList[0];   //여러개의 주소를 담고있는 리스트를 반환하며 이중 첫번째 값을 할당한다(여러개의 주소를 뱉어주는 경우도 있음) 
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);  //7777은 포트 번호 , IP 주소와 포트 번호를 할당한다. 



            _listener.Init(endPoint,OnAcceptHandler);
            Console.WriteLine("Listening...");

            while (true)
            {
                ;
                   
            }
            
            }

        }

 

 

Listener.cs

더보기
class Listener
    {
        Socket _listenSocket;
        Action<Socket> _onAcceptHandler;

        public void Init(IPEndPoint endPoint, Action<Socket> onAcceptHandler)
        {
            _listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            _onAcceptHandler += onAcceptHandler;

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

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

            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
            RegisterAccept(args);

        }

        void RegisterAccept(SocketAsyncEventArgs args)
        {
            args.AcceptSocket = null; //이벤트 재사용 시, 초기화 필수 

            bool pending = _listenSocket.AcceptAsync(args);
            if (pending == false)
                OnAcceptCompleted(null, args);

        }
        void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
        {
            if (args.SocketError == SocketError.Success)
            {
                //TODO
                _onAcceptHandler.Invoke(args.AcceptSocket);
            }else
                Console.WriteLine(args.SocketError.ToString());

            RegisterAccept(args);
        }


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

 

 

클라이언트는 동일  

 

 

 

출처 : https://joooing.tistory.com/entry/동기비동기-블로킹논블로킹

https://www.inflearn.com/course/유니티-mmorpg-개발-part4

728x90
반응형