[C#] 게임서버에서 Listener란?

Arthur·2023년 8월 30일
0
post-thumbnail
post-custom-banner

Listener란?


Client의 접속을 받아들이는 Listner 객체를 얘기합니다.

TCP 소켓 게임 서버를 구축할 때 Listner 객체로 정리할 수 있습니다.

Listener의 역할을 클래스로 따로 빼서 구현하지 않고 구현해도 아래와 같이 가능합니다.

static void Main(string[] args)
{
	// 1. Host의 EndPoint 값을 세팅합니다.
    string host = Dns.GetHostName();
    IPHostEntry ipHost = Dns.GetHostEntry(host);
    IPAddress ipAddr = ipHost.AddressList[0];
    IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);

	// 2. 클라이언트의 접속을 받아들이는 Listen 소켓을 생성합니다.
    Socket listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

	// 3. Host의 EndPoint를 Listen 소켓에 바인딩합니다.
	listenSocket.Bind(endPoint);
    
    // 4. 클라이언트의 접속을 허용 할 갯수를 미리 정의합니다.
    listenSocket.Listen(10);
    
    Console.WriteLine("Listening...");

	// 5. 클라이언트의 접속(Accept) 처리 하기 위한 반복문을 실행합니다.
    while (true)
    {
    	// 6. 클라이언트의 접속을 Accept 처리를 동기 방식으로 처리합니다.
        Socket clientSocket = listenSocket.Accept();
        
        // 7. Accept 처리가 완료되면 클라이언트로부터 온 데이터(메시지)를 받아서 출력합니다.
        byte[] recvBuff = new byte[1024];
        int recvBytes = clientSocket.Receive(recvBuff);
        string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
        Console.WriteLine($"[From Client] {recvData}");
        
       	// 8. 클라이언트에게 서버에 접속한 것을 환영한다는 데이터(메시지)를 보냅니다.
        byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !");
        clientSocket.Send(sendBuff);
        
        // 9. 클라이언트의 접속을 종료합니다.
        clientSocket.Shutdown(SocketShutdown.Both);
        clientSocket.Close();
    }
}

위 코드를 보면 소켓 프로그래밍의 대략적인 순서(흐름)는 아래와 같습니다.

여기서 흐름에 해당하는 로직을 한 클래스나 함수에서 관리해도 작동하는데 문제는 없습니다.
하지만 게임의 규모가 커지고 코드길이가 길어지면 유지보수에 문제가 생기게 됩니다.

그래서 흐름에 맞게 역할을 분담하는 객체를 생성합니다.
그것 중 하나가 바로 Listner입니다.



Listner 예제 코드


public class Listener
{
    Socket _listenSocket;
    Func<Session> _sessionFactory;

    public void Init(IPEndPoint endPoint, Func<Session> sessionFactory, int register = 10, int backlog = 100)
    {
    	// 1. 클라이언트의 접속을 받아들이기 위한 객체 생성 및 초기화
        _listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

		// 2. 서버 정보를 소켓에 bind
        _listenSocket.Bind(endPoint);

		// 3. 소켓을 수신 상태로 둡니다.
        // 보류 중인 연결 큐의 최대 길이를 매개변수로 입력합니다.
        _listenSocket.Listen(backlog);

		// 4. register의 수 만큼 소켓 Accept를 비동기로 처리한다.
        for (int i = 0; i < register; i++)
        {
            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            
            // 이벤트 핸들러 작업이 완료되면 이 곳에 추가된 함수가 실행된다.
            // 추가된 함수 => OnAcceptCompleted
            args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
            RegisterAccept(args);
        }
    }

	// AcceptAsync를 사용해 비동기로 처리
    // SocketAsyncEventArgs를 사용해 AcceptAsync 비동기 성공과 실패에 대한 처리를 합니다.
    private void RegisterAccept(SocketAsyncEventArgs args)
    {
    	// Event를 재사용하기 위해 기존에 있던 것을 null 처리
        args.AcceptSocket = null;
		
        // 작업이 보류 되었는지 완료되었는지를 bool 값(pending)으로 return 해줍니다.
        // 바로 처리를 했는지 아니면 못했는지를 확인합니다.
        bool pending = _listenSocket.AcceptAsync(args);
        
        // 바로 처리가 되었을 경우(false)
        if (pending == false)
            OnAcceptCompleted(null, args);
    }

	// SocketAsyncEventArgs를 사용해 성공 실패 여부 체크 후 세션 시작 및 연결
    private void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
    {
        if (args.SocketError == SocketError.Success)
        {
            Session session = _sessionFactory.Invoke();
            session.Start(args.AcceptSocket);
            session.OnConnected(args.AcceptSocket.RemoteEndPoint);
        }
        else
            Console.WriteLine(args.SocketError.ToString());

        RegisterAccept(args);
    }
}
  1. 클라이언트의 접속을 받아들이기 위한 객체 생성 및 초기화
  2. 서버 정보를 소켓에 bind
  3. 소켓을 수신(Listen) 상태로 둡니다.
  4. register의 수 만큼 소켓 Accept를 비동기로 처리한다.
  5. SocketAsyncEventArgs를 활용해 성공 실패 여부를 확인 합니다.
  6. 성공하면 세션을 시작합니다.
  7. 실패하면 에러를 콘솔에 출력합니다.

메시지를 주고 받기 전까지의 단계를 Listner에서 담당을 하는 것입니다.

클라이언트의 연결을 처리하는 과정을 효율적으로 처리하기 위해 위와 같이 코드가 작성되어 있습니다.

코드에서 좀 더 알아야 할 부분이 AcceptAsync와 SocketAsyncEventArgs 라고 생각해서
한 번 정리해봤습니다.


예제코드에 나오는 단어의 개념

  • IPEndPoint : 네트워크 엔드포인트를 IP 주소와 포트 번호로 나타냅니다.
  • Session : 일정 시간동안 같은 사용자로부터 들어오는 일련의 요구를 하나의 상태로 보고 그 상태를 일정하게 유지시키는 기술
  • Bind : 소켓에 주소를 할당하는 함수입니다.
  • listen : 클라이언트가 해당 소켓에 연결할 수 있도록 그 요청을 대기하는 상태로 만들어주는 것을 담당하는 함수를 지칭한다.
  • backlog : 연결 요청을 대기하는 큐의 크기


Accept와 AcceptAsync의 차이


C#에서 Accept 함수는 블로킹 방식으로 동작합니다.
Accept에 대한 함수의 동작이 처리 되지 않으면 다음으로 넘어가질 못합니다.

그래서 AcceptAsync를 사용해 성공여부를 기다리지 않도록 합니다.
Accept에 대한 동작에 대해서 결과에 상관 없이 바로 다음 클라이언트의 연결 처리를 할 수 있습니다.

여기서 비동기로 처리한다는 것은 해당 작업 완료 여부를 따지지 않고 다음 작업을 한다는 것입니다.

결국 구현되어 있는 Listner는 흐름을 막지 않도록 논 블로킹으로 처리하고,
다른 클라이언트의 연결도 처리하기 위해 비동기 처리를 합니다.


  • Accept
    • 클라이언트의 연결을 기다려 서버 소켓이 연결을 수락합니다.
    • 블로킹 메서드로, 연결이 수락되기 전까지 다음 코드로 진행되지 않는 동기식입니다.
  • AcceptAsync
    • 비동기적으로 연결을 수락합니다.
    • 연결이 수락될 때까지 블로킹되지 않고 다른 작업을 계속 수행할 수 있습니다.
    • 비동기 패턴을 사용하여 연결을 수락하고 연결이 수락되면 콜백이 호출됩니다.

(블로킹과 논 블로킹, 동기와 비동기관련 잘 정리되어 있는 블로그 => 링크)



작성하면서 느낀 점


Listener의 코드는 상당히 짧은데 이해하는데 시간이 꽤 걸렸습니다.
코드를 보면
블로킹, 논 블로킹, 동기, 비동기, 소켓, 이벤트 처리와 콜백 함수 키워드들이 있습니다.

코드에서 왜 비동기 처리를 하는지 부터 이벤트로 처리하는 이유도 이유를 잘 몰랐습니다.
그런데 정리를 하면서 확실히 이해를 하는데 도움이 되었습니다.

이런 코드를 보면서 어떻게 성능적으로 이점을 주고 더 많은 트래픽을 해결하는데 도움을 주는지 알아가는 계기가 되었습니다.



참고 자료


  • [C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 => 링크
  • c#으로 게임 서버 만들기 - 1. 네트워크 기반 코드 작성 => 링크
  • [네트워크] 서버의 시스템 콜( bind, listen, accept ) => 링크
  • SESSION이란? => 링크
  • c#으로 게임 서버 만들기 - 2. 접속 처리 및 버퍼 풀링 기법 => 링크
  • 개인공부) 서버실습(20) - 서버 코드 구분, 비동기 승인 처리 => 링크
  • 👩‍💻 완벽히 이해하는 동기/비동기 & 블로킹/논블로킹 => 링크
profile
기술에 대한 고민과 배운 것을 회고하는 게임 서버 개발자의 블로그입니다.
post-custom-banner

0개의 댓글