공부/인프런 - Rookiss

Part 4-3-9. 네트워크 프로그래밍 : Connector

셩잇님 2023. 10. 30. 19:27
반응형

 

네트워크 프로그래밍

 

 지난 시간에는 Session을 상속하는 과정과 총 4가지의 Event Handler(OnConnected, OnRecv, OnSend, OnDisconnected )를 이용하여 구현하는 시간을 가졌다. 이번 시간에는 Connector를 만드는 시간을 가진다. 👍

 

 


 

 

Connector를 만들기 전에, 왜 만들어야 할까?

 

 클라이언트와 서버간의 통신은 당연히 필요하기 때문에 구현하는 것이 맞지만, 서버만 이용하고 있는 곳에 왜 Connecotr가 필요할까? 그 이유는 다음과 같다.

 

1. 현재 서버 코어 같은 경우에는 서버를 메인 용도로 만들고 있지만 커넥트, 샌드, 리시브 하는 부분은 사실 공용으로 사용하면 좋다.

2. 서버 또한 분산처리를 할 수 있기 때문에 서버끼리의 연결과 통신이 필요하다. 예를 들어 MMO 같은 경우 A 서버는 AI, B 서버는 NPC, C 서버는 몬스터와 같이 분할해서 만들 수 있다. 이 때 메인 서버로 작동하는 서버는 하나가 있긴 하지만, 메인 서버 또한 A, B, C 서버와 연결이 필요하기 때문에 Connector가 필요하다.

 

Connector 제작

 

 Connector 역시 기존에 DummyClient 솔루션에서 작업한 부분의 문제점을 수정하고, 이를 라이브러리화 시키기 위한 작업을 진행한다.

 

 

 기존에 작성된 코드를 보면, 소켓을 생성하고 Connect() 메서드를 이용해 사용하는 것을 볼 수 있다. 하지만 이 또한 블로킹 함수이기 때문에 추가적으로 구분 작업이 필요하다. 먼저 ServerCore 솔루션에서 Connector 스크립트를 새롭게 만든다.

 

Connector.cs - Connect

    public class Connector
    {
        Func<Session> _sessionFactory;

        public void Connect(IPEndPoint endPoint, Func<Session> sessionFactory)
        {
            // 휴대폰 설정(=손님 소켓 생성)
            Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            _sessionFactory = sessionFactory;

            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.Completed += OnConnectCompleted;
            args.RemoteEndPoint = endPoint;
            args.UserToken = socket;

            RegisterConnect(args);
        }
        
        ...
        
    }

 

  Connect 함수의 구현 방법은 기존의 Session 스크립트의 Start와, Listner의 Init과 유사한 방식을 이용하여 처리한다. Socket과 이벤트를 연동하는 방식으로 동작하는 SocketAsyncEventArgs를 생성하고 해당 이벤트가 동작할 함수와 주소를 연결하여 준다.

 

Connector.cs - RegisterConnect

        void RegisterConnect(SocketAsyncEventArgs args)
        {
            Socket socket = args.UserToken as Socket;
            if (socket == null)
            {
                return;
            }

            bool pending = socket.ConnectAsync(args);
            if (pending == false)
            {
                OnConnectCompleted(null, args);
            }
        }

 

 RegisterConnect() 메서드는 기존에 사용하지 않고 넘어갔던 UserToken을 이용하여 소켓 값을 넘겨받아 사용한다. 이 방법 외에도 매개변수나, 멤버 변수로도 사용하여 처리할 수 있지만 한 번에 여러개의 소켓이 활용 될 수 있으므로 위와 같은 방법으로 처리를 진행한다. 다음으로는 비동기로 동작하는 ConnectAsyn 메서드에 args를 담아주고 성공, 실패 여부를 처리하여 진행한다. 

 

Connector.cs - OnConnectCompleted

        void OnConnectCompleted(object sender, SocketAsyncEventArgs args)
        {
            if (args.SocketError == SocketError.Success)
            {
                Session session = _sessionFactory.Invoke();
                session.Start(args.ConnectSocket);
                session.OnConnected(args.ConnectSocket.RemoteEndPoint);
            }
            else
            {
                Console.WriteLine($"OnConnectCompleted Fail : {args.SocketError}");
            }
        }

 

 마지막으로 연결 완료(OnConnectCompleted) 메서드이다. 해당 함수가 실행된다는 것은 연결이 완료되었다는 것을 뜻하는데, 이 때에는 정보를 보내고 받는 기능을 시작하면 된다. 이 때에도 지난 시간과 같이 sessin을 가져와서 사용해야 하는데, 저번 수업 시간에 처리한 것과 동일한 방식으로 Func에 담아 _sessionFactory를 Invoke()하는 방식을 통해 사용한다.

 

 


 

 

라이브러리화 진행하기 📋

 

위 작업까지 모두 마친 뒤 DummyClient와 Server에서 함께 사용하기 위해 ServerCore를 라이브러리 형태로 변경한다.

 

1. Servoer Core 프로젝트 마우스 우클릭 - 속성

 

 

2. 출력 유형을 클래스 라이브러리로 변경한다.

 

 

3. Server Core를 다시 우클릭하여 시작 프로그램으로 설정 후 실행하면 다음과 같은 경고가 뜬다! 이렇게 나오면 정상적으로 되고 있는것이니 걱정하지 말것 🤔

 

 

4. Server / Dummy Client 프로젝트 마우스 우클릭 - 추가 - 프로젝트 참조 클릭

 

 

5. Server Core를 체크한 후 확인을 눌러준다.

 

 

6. 솔루션 마우스 우클릭 - 시작 프로그램 설정 - Dummy, Server만 시작으로 변경

 

 

 


 

 

서버 코드 이전 🚗

 

ServerCore 프로젝트를 라이브러리화 시켜주었기 때문에 더 이상 ServerCore에서 코드 실행이 되지 않으므로 Server의 Program.cs 으로 코드를 옮겨준다. 🙃

 

 

 소스코드를 옮기면 다양한 오류들이 발생하지만 침착하자. 먼저 최상단에 using ServerCore;를 추가하고, ServerCore의 클래스들을 모두 Public 클래스로 변경해주면 된다. 이렇게 처리할 경우 이제 엔진 영역과 콘텐츠 영역이 구분된 것이다. 콘텐츠는 엔진 에서 무엇을 어떤걸 하는지 알 수가 없고, 주어진 함수로만 동작되어 진다. 👍

 

 


 

 

 Blocking 방식 비동기 처리로 변경하기

 

 이제 DummyClient에서 블로킹 함수를 사용하던 부분을 위에서 만들어준 Connector로 교체하는 작업이 필요하다. 우선 Connector를 동작시키기 위해서는 Func(_sessionFactory)에 넣어줄 값, 즉 어떠한 세션인지를 알려줄 필요가 있끼 때문에 만들어 주어야 한다. 따라서 기존에 만들어 두었떤 Game Session을 복사해서 가져온다. 👍

 

받는 작업 처리

 DummyClient 에서는 실행 함수가 연결이 되었을 떄 보내는 작업을 아래와 같이 처리하고 있었는데, 이를 복사해서 가져온 곳으로 이동시켜 준다. 

 

이동시켜 주자!

 

Recv를 삭제하는 이유

기존에 작덩하던 코드는 보낸 뒤에 받는 작업을 하고 있었다. 그러나 이제 SocketAsyncEventArgs와 EventHandler를 이용하여 처리하고 있기 때문에 삭제해준다.

 

지워버렷!

 

마무리

 불필요한 부분을 모두 다 지워주고, 오늘 제작한 Connector를 DummyClient 내 Program 스크립트 내에서 만들어주고 게임 세션을 호출해주면 끝이다!

 

DummyClient - Program.cs

using ServerCore;
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

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

            // 보낸다
            for (int i = 0; i < 5; i++)
            {
                byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello World! {i}");
                Send(sendBuff);
            }
        }

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

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

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

    class Program
    {
        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);

            Connector connector = new Connector();
            connector.Connect(endPoint, () => { return new GameSession(); });

            while (true)
            {
                try
                {

                }
                catch (Exception e)
                {
                    Console.WriteLine(e.ToString());
                }

                Thread.Sleep(1000);
            }
        }
    }
}

 

정상작동! 야호! 👍

 

 

 

반응형