공부/인프런 - Rookiss

Part 4-3-8. 네트워크 프로그래밍 : Session #4 (Event Handler)

셩잇님 2023. 10. 29. 22:23
반응형

 

 

네트워크 프로그래밍

 

 지난 시간에서 우리는 Session을 만들어 소켓 프로그래밍의 Receive 부분과 Send 부분을 분리하여 작업이 진행되도록 처리 했다. 오늘은 Recv 부분의 코드 로직을 서버 단과 컨텐츠 단을 나누어서 작업이 진행될 수 있도록 개선할 것이다. 이 작업은 이벤트 핸들러를 이용하는 방법과, 상속을 이용하는 방법이 있지만 루키스님은 상속이 보다 구현하기 쉽기 때문에 상속 방법을 통해 이를 구현한다. 😎

 

 


 

 

Session 클래스 내 상속 메서드 추가

 

 먼저 우리가 무엇을 사용해야 되는지 다시 한번 생각해보아야 한다. 이를 곰곰이 생각해 나누어 보면 크게 4가지로 나뉘어 질 수 있는 것을 알 수 있다.

 

1. 어떤 클라이언트에서 접속을 했는지 알 수 있는 OnConnected

2. 클라이언트가 보낸 패킷을 내가 받았다는 것을 알려주는 OnReceive

3. 서버(Session)에서 내가 클라이언트에게 무엇인가를 보냈다는 것을 알려주는 OnSend

4. 연결이 끊어졌음을 알 수 있는 OnDisconnected가 있다.

 

 결국 이렇게 상속을 이용해 구현을 진행하면 외부에서 우리가 구현한 세션을 이용하여 다양한 작업을 진행할 경우 내부 로직을 궂이 외부로 알릴 필요없이 사용할 수 있다는 것이다.

 

Session.cs

    abstract class Session
    {
        Socket _socket;
        int _disconnected = 0;

	...

        public abstract void OnConnected(EndPoint endPoint);
        public abstract void OnRecv(ArraySegment<byte> buffer);
        public abstract void OnSend(int numOfBytes);
        public abstract void OnDisconnected(EndPoint endPoint);
        
        ...
        
        void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
        {
		...
        }

 

 따라서 위와 같이 4개의 메소드와 세션 자체에 abstract를 붙여 이를 오버라이딩하여 사용하도록 변경하여 준다. 이를 구현하기 위해 ServerCore 템플릿 내 Program.cs에 새로운 클래스를 아래와 같이 만들어준다.

 

Program.cs 스크립트 내 새로 생성된 GameSession 클래스

    class GameSession : Session
    {
        public override void OnConnected(EndPoint endPoint)
        {
        
        }

        public override void OnDisconnected(EndPoint endPoint)
        {
        
        }

        public override void OnRecv(ArraySegment<byte> buffer)
        {
        
        }

        public override void OnSend(int numOfBytes)
        {
        
        }
    }

 

 위와 같이 코드를 변경한다면 기존에 세션을 생성하여 사용했던 OnAcceptHandler 메서드 부분이 abstract로 변경되어 사용할 수 없으므로 이를 새로 사용한 GameSession 클래스로 변경하여 준다.

 

새로 만든&nbsp; GameSession으로 인해 사용하지 못한다!

 

 


 

상속 관계를 이용한 연동 

 

OnDisconnected 연결

가장 쉬운 OnDisconnect는 기존에 생성한 함수인 Disconnect 메서드 내부에 추가해주면 된다. 이후 해당 함수 (OnDisconnected)로 이동하여 해당 함수 내부에서 추가적으로 하고 싶은 작업이 있을 경우 진행해 주면된다. 지금은 딱히 할 이렇다 할 기능이 없으므로 일단은 로그를 남기도록 하자.

 

Disconnect 메서드

        public void Disconnect()
        {
            if (Interlocked.Exchange(ref _disconnected, 1) == 1)
            {
                return;
            }

            OnDisconnected(_socket.RemoteEndPoint);
            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }

 

GameSession 클래스 내 OnDisconnected 함수 정의

        public override void OnDisconnected(EndPoint endPoint)
        {
            // 로그
            Console.WriteLine($"OnDisconnected : {endPoint}");
        }

 

 


 

 

OnRecv 연결

 다음은 OnRecv를 연결하도록 하자. OnRecv는 OnRecvCompleted 메서드 내부에서 작업을 진행하면 된다. 이전에는 하드코딩하여 메시지를 출력하고 끝이났지만, 지금은 이벤트를 연동하여 이 작업을 진행하면 된다. 이후 마찬가지로 OnRecv 메서드 내부에 기존에 사용하던 주석 코드를 이동시키도록 하자.

 

OnRecvCompleted 메서드

        void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
        {
            if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
            {
                try
                {
                    OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
                    RegisterRecv();
                }
                catch (Exception e)
                {
                    Console.WriteLine($"OnRecvCompleted Fail {e}");
                }
            }
            else
            {
                Disconnect();
            }
        }

 

GameSession 클래스 내 OnRecv 함수 정의

        public override void OnRecv(ArraySegment<byte> buffer)
        {
            string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
            Console.WriteLine($"[From Client] {recvData}");
        }

 

 


 

 

OnSend 연결

 다음은 OnSend를 연결하도록 하자. OnSend 또한 OnRecv와 마찬가지로 OnSendCompleted 메서드 내부에서 작업을 진행하면 된다. 또한 OnRecv 메서드 내부에도 아직 이렇다 할 기능이 필요하지 않으므로 단순하게 로그만 찍는 작업을 진행하도록 하자.

 

OnSendCompleted 메서드

void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
    lock (_lock)
    {
        if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
        {
            try
            {
                _sendArgs.BufferList = null;
                // 성공적으로 데이터를 모두 전송했으므로 리스트를 비워준다.
                _pendinglist.Clear();

                OnSend(_sendArgs.BytesTransferred);

                if (_sendQueue.Count > 0)
                {
                    RegisterSend();
                }
            }
            catch (Exception e)
            {
                Console.WriteLine($"OnSendCompleted Fail {e}");
            }
        }
        else
        {
            Disconnect();
        }
    }
}

 

GameSession 클래스 내 OnSend 함수 정의

        public override void OnSend(int numOfBytes)
        {
            // 로그
            Console.WriteLine($"Transferred bytes: {numOfBytes}");
        }

 

 


 

 

OnConnected 연결

 OnConnected는 조금 까다롭다. 왜냐하면 해당 메서드를 호출하는 부분이 세션 내부에 없기 때문이다. 그렇다면 해당 메서드는 어디서 호출되었을까?  해당 메서드는 클라이언트가 접속을 했다는 것을 알리는 부분의 역할을 할 것인데, 우리는 이 부분을 이전에 리스너 OnAcceptCompleted 내부에서 해주고 있었다.

 

OnAcceptCompleted 메서드

OnAcceptCompleted 내부에서 Invoke를 통해 진행하고 있다.

 

 따라서 위 코드를 아래와 같이 바꾸어준다. 

 

변경된 OnAcceptCompleted

 

 물론 위와 같은 방법을 사용하여 구현하는 것이 문제가 없는 것은 아니다. 애시당초 우리는 컨텐츠 단과 서버 단을 나누어서 구분하기 위해 이 작업을 진행하고 있는데, 위와 같이 OnAcceptCompleted  코드 내부에서 new를 이용해 게임세션 클래스를 만들어준다면 구분하는 작업이 아무런 의미가 없는 것이다. 또한 추가적인 문제로 나중에는 게임 세션 클래스가 아닌 다른 클래스를 사용해야 한다고 가정한다면 결국 해당 코드를 또 변경하여 수정 작업 해야한다.

 

 그렇다면 어떻게 처리해야 할까? 🤔

 정답은 Func를 이용하여 대리자를 통해 우리가 사용할 세션이 무엇인지를 정해주는 방법으로 변경하는 것이다! 즉 기존에 Action 키워드를 이용해 작업하던 것을 Func로 변경하여 어떤 Session을 뱉어서 사용할 것인지 정의하는 것이다.

 

Listener.cs

class Listener
{
    
    Socket _listenSocket;
    // Action<Socket> _onAcceptHandler; ❌
    Func<Session> _sessionFactory; // ⭕
    
    public void Init(IPEndPoint endPoint, Func<Session> sessionFactory)
    {
        _listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
        _sessioFactory += sessioFactory; // ⭕
        
        ...
        
    }

    void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
    {
        // 소켓 에러가 발생하지 않았다면
        if (args.SocketError == SocketError.Success)
        {
            Session session = _sessioFactory.Invoke();
            session.Start(args.AcceptSocket);
            session.OnConnected(args.AcceptSocket.RemoteEndPoint);
        }
        else
        {
            Console.WriteLine(args.SocketError.ToString());
        }

        // 다음 이벤트를 위해 새로이 등록해준다.
        RegisterAccept(args);
    }
}

 

 그 뒤에 Session을 new로 생성하는 방식이 아닌 Func<Session>를 통해 인자 값으로 받아와 값을 넣어주어 Session을 생성한다. 이렇게 작업을 진행 할 경우 게임 세션이 아닌 다른 타입의 Session들 또한 Func이 이를 받아와 가장 상위 클래스인 Session를 받아와 처리를 진행한다.

 

 변경된 Session 클래스에 따라 Program 클래스 또한 리스너를 Init 할 때를 바꿔주어야 한다. 

 

Listener.cs - 계속

    class Program
    {
        static Listener _listener = new Listener();

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

            _listener.Init(endPoint, () => { return new GameSession(); });
            Console.WriteLine("Listening...");
            
            ...
        }
    }

 

 나중에야 매니저를 통해 생성해야하는 세션들을 관리할 수 있지만, 지금은 GameSession 밖에 없기 때문에 람다를 이용해 세션을 생성해 보내준다. 마지막으로 OnConnected 또한 기존에 OnAcceptEventHandler 에서 작동하던 처리를 가져와 작업을 마무리 한다.

 

GameSession 클래스 내 OnConnected 함수 정의

        public override void OnConnected(EndPoint endPoint)
        {
            // 로그
            Console.WriteLine($"OnConnected : {endPoint}");

            byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server!");
            Send(sendBuff);
            Thread.Sleep(1000);
            Disconnect();
        }

 

 


 

 

아직도 남은 문제점.. 😫

위와 같이 4개의 강의를 통해 Session을 개선했지만, 아직도 모든 문제가 해결된 것은 아니다. OnAcceptComplete 메서드 내부에서 session을 생성해주고, Start를 하고, 연결되기 전에 연결을 끊어버릴 경우, OnConnected 메서드가 정상적으로 실행될 수 없기 때문에 또 수정을 해주어야 한다. 😭..

 

🤬

 

 

 

반응형