Listener 서론

이번 시간에는 식당의 문지기를 따로 책임분리를 시킬것이다

또한 블로킹함수를 논블로킹함수로 대체하고 콜백함수를 이용해서 지속적인 루프문을 돌릴것.


Why?

블로킹으로 하면 어떤 작업을 할 때마다 해당 작업이 완료 되기 전 까지
다른 작업을 하지못한다

나는 블로킹 함수를 꼭 써야해!

그렇다면 소켓 마다 1개의 스레드가 필요함
만약 10만명의 클라가 접속한다면 10만개의 스레드가 필요하다는 소리
터무니없는 소리다 생각을 잠깐만 해봐도 ContextSwitching이 미친듯이 발생한다는것…

그래서 논 블로킹을 사용해서 흐름을 유지한 채 요청을 미리 해두고
비동기 방식 콜백으로 데이터를 받아와서 처리를해주면 보다 적은비용으로
많은 처리가 가능하다는것


전체 로직

 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);
         //왜 초기화 하는시점에 바로 한번에 처리하냐?
         //이벤트를 한번에 같이 등록시켜줘서
         //args를 재사용 하기위해서 그럼 그뒤에는
         //OnAcceptCompeted 호출됨
         RegisterAccept(args);
     }
 
 
     //당장 완료하는게아니라 등록을 한 것
     private void RegisterAccept(SocketAsyncEventArgs args)
     {
         //이 코드는 이벤트 재사용을 위해 
         //클리어 처리해주는 것
         //클리어처리를 안해주면 기존 AcceptSocket이 남아있게된다
         args.AcceptSocket = null;
 
         bool pending = _listenSocket.AcceptAsync(args);
         //펜딩 여부 (대기가있는지없는지 여부)
         //소켓이 진짜 아예 할게없어서 바로 리턴을 때려주면
         //처리해줄려고 펜딩여부를 둔것
         if (pending == false)
             OnAcceptCompleted(null, args);
 
         //근대왜 pending여부를 두냐? 바로 이벤트연결해서 
         //pending이 false가나오면 바로 콜백이 호출될텐대?
         // -> 답변 ms가 그렇게 만듬
     }
 
     private void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
     {
         //소켓에 간간히 에러가 나와서 처리해줌
         if (args.SocketError == SocketError.Success)
         {
             _onAcceptHandler.Invoke(args.AcceptSocket);
         }
         else
             Console.WriteLine(args.SocketError.ToString());
 
 
         //다음 턴을 위해 args를 등록해주는 것
         //전에 우리 코드는 while문으로 accecpt블로킹함수를썻는데
         //비동기는 계속 호출을 해주기위해서는 따로 
         //우리가 register를 해줘야한다
         //코드의 흐름을 따라가보면 
         //init -> 이벤트 등록 -> RegisterAccept -> OnAcceptCompeted -> RegisterAccept ...n
         //init한번으로 while처럼 반복되게 처리한 것 
         RegisterAccept(args);
     }

동작 순서

유저 코드 ↓ Socket.AcceptAsync(SocketAsyncEventArgs) ↓ Windows 커널 (IOCP 등록) ↓ 클라이언트 접속 발생 ↓ IOCP 완료 큐에 등록 ↓ .NET이 Completed 이벤트 발생 ↓ args.Completed → OnAcceptCompleted 실행


문뜻 생각난 정리

서버코어부분에서
이런식으로 while문을 돌리는 메인 스레드가있는데

  static void Main(string[] args)
  {
      string host = Dns.GetHostName();
      IPHostEntry ipHost = Dns.GetHostEntry(host);
      IPAddress ipAddr = ipHost.AddressList[0];
      IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
 
      _listener.Init(endPoint, OnAcceptHandler);
      Console.WriteLine("Listening...");
 
      while(true)
      {
          ;
      }
 
  }

밑에 로직은
OnAcceptCompleted RegisterAccept 돌리면서 뺑뺑이를 돌고있다
실행시키면 두개가 따로 도는걸 확인 할 수잇는데

      SocketAsyncEventArgs args = new SocketAsyncEventArgs();
         args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
         RegisterAccept(args);
         
     private void RegisterAccept(SocketAsyncEventArgs args)
     {
         args.AcceptSocket = null;
         bool pending = _listenSocket.AcceptAsync(args);
  
         if (pending == false)
             OnAcceptCompleted(null, args);
     }
 
     private void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
     {
         //소켓에 간간히 에러가 나와서 처리해줌
         if (args.SocketError == SocketError.Success)
         {
             _onAcceptHandler.Invoke(args.AcceptSocket);
         }
         else
             Console.WriteLine(args.SocketError.ToString());
         RegisterAccept(args);
     }

찾아보니 스레드풀에서 하나를 꺼내와서 작업을 돌려주는걸 확인할 수 있다

즉!!

두개의 스레드가 돌아간다는 뜻은 메인스레드 와 작업자 스레드가 공용 데이터를 건드릴 수 있는 RaceCondition상태가 되기에 이제는 머리에 염두해두고 작업을 진행해야한다 동기화 문제를 생각하면서 코딩을 하자!