C#

Listener

monstro 2024. 12. 18. 20:25
728x90
반응형

기존의 소켓 프로그래밍 예제에는 치명적인 문제가 남아있습니다.

치명적인 문제점은 바로 블로킹

즉, 동기 방식으로 서버가 클라이언트의 Connect 요청Accept를 수행한다는 것입니다.

 

동기 방식에서는 서버가 Connect 요청이 오기전

그러니까 서버가 Accept를 수행하기 전까지는 다른 작업을 하지 않고 계속 기다리게 됩니다.

그리고 이는 작업을 처리하는데 있어서 낭비로 이어지게 됩니다.

 

따라서 기존의 코드를 수정하여 논블로킹, 즉 비동기 방식으로 변경하여

클라이언트로부터 Connect 요청이 오는 경우, Accept를 수행하고

그렇지 않은 경우 다른 작업을 수행하는 방식으로 변경하겠습니다.

 

코드 작업을 하기 전,

간단하게 동기와 비동기 방식을 시각적으로 표현해보았습니다.

 

- 동기 방식(=블로킹)

 

서버에서 한번 Listen을 시작한 후,

클라이언트에서 Connect 요청을 하여 Accept하지 않는 경우 무한 대기합니다.

 

- 비동기 방식(=언블로킹)

 

비동기 방식의 경우 다르게 동작합니다.

Server에서 ClientSocket을 Listen으로 설정하여도, Accept를 수행할 때까지 무한히 대기하지 않고

다른 별개의 로직을 수행합니다.

그러다 Client로부터 Connect 요청이 들어오면 Accept를 수행하고 이후 Accept에 맞는 로직을 수행합니다.

이제 코드를 통해 이를 구현해보겠습니다.

 

- 비동기 방식으로 구현한 Socket 프로그래밍

기존의 Server 코드에서 전부 수행하던 동작을 2개로 쪼개어 관리합니다.

 

1) Server의 Client 소켓을 Listen 상태로 설정하고 이후 Accept까지 수행하는 Listener

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

    // Server의 Client 소켓의 초기 상태를 설정
    public void Init(IPEndPoint endPoint, Action<Socket> OnAcceptHandler)
    {
        _listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
        // ClientSocket에 대해 Accept 이벤트 발생 시 호출할 함수를 연결
        _onAcceptHandler += OnAcceptHandler;

        // ClientSocket에 대해 bind
        _listenSocket.Bind(endPoint);

        // Listen 상태 설정 - 최대 대기수
        _listenSocket.Listen(10);

        // 설정된 Client 소켓에 대해 비동기 방식으로 동작하도록 설정
        // 비동기 방식으로 성공적으로 연결되는 경우, OnAcceptCompleted를 호출
        SocketAsyncEventArgs args = new SocketAsyncEventArgs();
        args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
        RegisterAccept(args);
    }

    // Accept를 할 수 있도록 Listen 상태인 Client 소켓을 설정
    void RegisterAccept(SocketAsyncEventArgs args)
    {
        // Listen - Accept가 무한 반복되는 구조이므로 이를 위해 AceeptSocket을 null로 초기화
        args.AcceptSocket = null;
        // pending은 대기 여부를 의미함 -> true == 클라이언트가 즉시 Connect 요청
        // false == 클라이언트가 Connect 요청을 하지 않아 다른 로직을 수행가능함
        bool pending = _listenSocket.AcceptAsync(args);

        // 클라이언트로부터 Connect를 받아들일 준비를 한후에, 바로 요청이 온 경우 받아들임
        if (pending == false)
            OnAcceptCompleted(null, args);
        // 아닌 경우, 클라이언트로부터 다른 로직을 수행하며 Connect 요청이 오기를 기다림
        // TODO
    }

    // Accept되는 경우 호출할 함수
    void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
    {
        // Connect를 Accept 하는 과정에서 문제가 없다면 _onAcceptHandler에 연결된 함수를 실행하여 Accept
        if (args.SocketError == SocketError.Success)
        {
            _onAcceptHandler.Invoke(args.AcceptSocket);
        }
        // 문제가 있다면 해당 문제를 출력
        else
            Console.WriteLine(args.SocketError.ToString());

        // 클라이언트로부터 다음에 들어올 Connect 요청을 Accept하기 위해 다시 준비
        RegisterAccept(args);
    }
}

 

2) 기존의 Server

class Program
{
    // Listen 소켓을 생성할 새로운 Listener 인스턴스 생성
    static Listener serverListener = new Listener();

    // Listener의 Accept 이벤트가 성공적으로 발생하면 호출될 콜백 함수
    static void OnAcceptHandler(Socket clientSocket)
    {
        try
        {
            // Client로부터 받은 내용을 기록
            byte[] recvBuffer = new byte[1024];
            int recvBytes = clientSocket.Receive(recvBuffer);
            string recvData = Encoding.UTF8.GetString(recvBuffer, 0, recvBytes);
            Console.WriteLine($"From Client : {recvData}");

            // Client로 전달
            byte[] sendBuffer = Encoding.UTF8.GetBytes("Welcome to Server!!!");
            clientSocket.Send(sendBuffer);

            // Client와의 연결 중지 - 우선 Client의 ServerSocket 연결 중지
            clientSocket.Shutdown(SocketShutdown.Both);
            clientSocket.Close();
        }
        catch(Exception e)
        { 
            Console.WriteLine(e.ToString()); 
        }    
    }

    static void Main(string[] args)
    {
        // DNS를 사용
        string host = Dns.GetHostName();
        IPHostEntry ipHost = Dns.GetHostEntry(host);
        IPAddress ipAddr = ipHost.AddressList[0];
        IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);

        // Listener의 Init 함수에 인자인 DNS의 IPEndPoint와 Accept 이벤트 발생시 호출할 콜백함수를 넘겨줌
        serverListener.Init(endPoint, OnAcceptHandler);
        Console.WriteLine("Listening...");

        while (true)
        {

        }
    }
}

 

3) Client

class Program
{
    static void Main(string[] args)
    {
        while (true)
        {
            // DNS를 사용
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);

            // Server Socket 설정
            Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

            try
            {
                // Server에게 Connect 요청
                socket.Connect(endPoint);
                Console.WriteLine($"Connected to {socket.RemoteEndPoint.ToString()}");

                // 서버로 전송
                byte[] sendBuffer = Encoding.UTF8.GetBytes("Hello From Client");
                int sendBytes = socket.Send(sendBuffer);

                // 서버로부터 받음
                byte[] recvBuffer = new byte[1024];
                int recvBytes = socket.Receive(recvBuffer);
                string recvData = Encoding.UTF8.GetString(recvBuffer, 0, recvBytes);
                Console.WriteLine($"From Server : {recvData}");

                // Server 소켓 종료
                socket.Shutdown(SocketShutdown.Both);
                socket.Close();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }

            Thread.Sleep(100);
        }
    }
}

 

Client의 경우 기존의 코드에서 무한 반복문으로 실행하게끔 수정하여

무한히 서버에 메시지를 전송하게 됩니다.

이제 실제 실행결과를 확인해보겠습니다.

 

 

위와 같이 문제없이 서버 - 클라이언트가

소켓 프로그래밍 방식 그중에서도 비동기 방식으로 돌아가는 것을 확인할 수 있습니다.

728x90
반응형

'C#' 카테고리의 다른 글

Session_2  (0) 2024.12.24
Session_1  (0) 2024.12.24
소켓 프로그래밍 기초  (0) 2024.12.16
네트워크 기초 + 통신 모델  (0) 2024.12.11
Thread Local Storage  (0) 2024.12.09