[server 프로그램 c#] socket.AcceptAsync를 사용 해서 client 연결을 비동기 방식으로 실행.

래또나·2023년 10월 18일
  1. 블로킹 함수를 이용해 소켓을 Accept를 하면 코드는 직관적 이지만 접속을 기다리는 동안 그 후 로직이 진행되지 않아 실제 프로젝트에서는 적합하지 않음.
  2. 기존 블로킹 함수인 Accept 함수를 AcceptAsync 함수를 사용해 비동기 방식으로 리팩토링.
  3. Listener.cs를 추가하여 비동기 구현.

Listner.cs 생성

🕵️ init 메소드

  1. 소켓의 기본 정보 등록
  2. client가 접속 되면 연결 해준 함수로 소켓을 콜백
    SocketAsyncEventArgs args = new SocketAsyncEventArgs();
    args.Completed += new EventHandler<SocketAsyncEventArgs>(Onacceptcompleted);
    3.소켓을 오픈하기 위해 호출
    RegisterAccept(args);

🕵️ RegisterAccept

  1. 접속을 대기할 수 있도록 소켓 오픈.(비동기 방식)
  2. AcceptAsync메소드는 client가 접속 되면 false 반환 client가 접속 되지 않아도 블로킹을 하지않고 true 반환 이후에 client가 접속 되면 SocketAsyncEventArgs로 소캣을 콜백
    bool pending = _listenSocket.AcceptAsync(args);
  3. init에서 호출할때는 args가 null이지만 한번 accept하고 Onacceptcompleted에서 호출 됬을때는 args가 비어있지 않음으로 기존 소켓 정보를 초기화. 초기화 하지 않으면 에러가 발생.
    args.AcceptSocket = null;

🕵️ Onacceptcompleted

  1. 접속이 완료되면 소켓 에러유무 확인 후 에러가 없으면 Program.cs에서 전달받은 콜백 함수로
    접속된 소켓 전송.
  2. 이후 다음 client 접속을 받을 수 있도록 RegisterAccept 메소드를 다시 호출.

서버 소스

  • Listener.cs
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace ServerCore
{
    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);

            //최대 대기수
            _listenSocket.Listen(10);

            //클라 접속 되면 연결 해준 함수로 콜백 해줌(콜백 함수 등록)
            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.Completed += new EventHandler<SocketAsyncEventArgs>(Onacceptcompleted);

            //소켓 오픈
            RegisterAccept(args);

        }

        void RegisterAccept(SocketAsyncEventArgs args)
        {
            //Onacceptcompleted메소드에서 accept후 다시 들어오면 args에 기존 값이 남아있어 해당 부분을 초기화 시켜줌. 초기화 하지 않으면 에러 발생.
            args.AcceptSocket = null;

            //AcceptAsync client가 접속 되면 false 반환, 접속되지 않으면 true 반환(true 반환 후 client가 접속 되면 SocketAsyncEventArgs로 소캣을 콜백 해줌.
            bool pending = _listenSocket.AcceptAsync(args);
            if (!pending) // pending true면 아직 접속된 클라가 없다는 뜻.
                Onacceptcompleted(null,args);
        }

        void Onacceptcompleted(object sender , SocketAsyncEventArgs args)
        {
        //소켓 에러유무 확인
            if (args.SocketError == SocketError.Success)
            {
            	//접속이 완료되어 호출자에게 접속 소켓 전달.
                _onacceptHandler.Invoke(args.AcceptSocket);
            }
            else
                Console.WriteLine(SocketError.SocketError);

            //다음 접속을 기다릴 수 있도록 AcceptAsync를 재 실행.
            RegisterAccept(args);
        }

        public Socket Accept()
        {
            return _listenSocket.Accept();
        }
    }
}
  • program.cs
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Program
    {
        static Listener _listener = new Listener();
        static void OnacceptHandler(Socket socket)
        {
            try
            {
                Console.WriteLine("Listening...");
                //손님입장
                Socket clientSocket = socket;

                // 받는다.(블로킹 함수)
                byte[] recvBuff = new byte[1024];
                int recvBytes = clientSocket.Receive(recvBuff);
                string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                Console.WriteLine(recvData);

                //전송 한다.(블로킹 함수)
                byte[] sendBuffe = Encoding.UTF8.GetBytes("welcome to MMORPG Server !...");
                clientSocket.Send(sendBuffe);

                //예고 
                clientSocket.Shutdown(SocketShutdown.Both);
                //연결끊기
                clientSocket.Close();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
        }

        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipaddr = ipHost.AddressList[0]; //아이피가 여러개 있을수 있으며 배열로 ip를 반환함
            IPEndPoint endPoint = new IPEndPoint(ipaddr, 7777);

            _listener.init(endPoint, OnacceptHandler);

            while (true)
            {

            }
        }
    }
}

클라이언트 더미 소스

  • progam.cs
 using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace DummyClient
{
    class Program
    {


        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipaddr = ipHost.AddressList[0]; //아이피가 여러개 있을수 있으며 배열로 ip를 반환함
            IPEndPoint endPoint = new IPEndPoint(ipaddr, 7777);

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

                    //문지기한테 입장 문의
                    socket.Connect(endPoint);
                    Console.WriteLine($"Conneted to {socket.RemoteEndPoint.ToString()}");

                    //보낸다.(블로킹 함수)
                    byte[] sendbuff = Encoding.UTF8.GetBytes("Hellow world!");
                    int sendByte = 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.Message);
                }

                Thread.Sleep(1500);
            }
        }
    }
}

실행 화면

dummyclient는 server에서 계속해서 접속을 받는지 테스트를 위해
wihle문으로 접속 socket 정료후 재좁속 하도록 수정.

0개의 댓글