[C#/Unity] 게임 서버 #7 - Connector, TCP&UDP

Donghee·2024년 9월 11일
0

본 게시물은 Rookiss님의 '[C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버' 강의를 듣고 정리한 내용임을 미리 알립니다.

Connector

현재 클라이언트가 서버에 연결 시도하는 코드를 보자.

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

            while (true)
            {
                Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
                
                try
                {
                    socket.Connect(endPoint); // 연결 시도. 블로킹 함수.
                    ...
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.ToString());
                }
                Thread.Sleep(1000);
            }
        }
    }
}

try 블록안의 socket.Connect(endPoint)가 서버의 엔드포인트와 연결하는 부분이다. 이는 블로킹 함수로, 이 동작이 끝날 때까지 다른 행동을 못하는 상황이 된다. 이를 비동기적으로 작업하게 하고, 여러번 재활용하기 위해 연결을 비동기적으로 담당하는 Connector 클래스를 만들어보자.

Connect

우선 Connector도 저번에 만들었던 Listener와 동일하게, 연결된 후 통신을 담당하는 Session이 필요하다. 지금 Session은 추상 클래스이므로, 어떤 파생 클래스를 만들지 결정해주는 _sessionFactory Func이 클래스 멤버로 선언되어 있다.
이후 Connector 클래스의 연결을 시작하는 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);
        }

인자로 IPEndPoint를 받아 이와 연결하는 소켓을 만들어준다. 이후 SocketAsyncEventArgs 객체를 통해 비동기 작업에 대한 사전 설정을 하고, RegisterConnect 메소드를 통해 연결 작업을 시작한다.

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에서는 args의 UserToken에 있는 소켓을 빼와 ConnectAsync를 진행한다. 이는 비동기로 처리되기 때문에, 이 작업이 끝나면 콜백 형식으로 OnConnectCompleted가 진행될 것이다. 혹시 동작에 펜딩이 없을 수도 있으니, 이를 확인하는 작업도 꼭 포함하도록 하자.

OnConnectCompleted

연결이 끝났을 때 콜백으로 실행될 OnConnectCompleted 메소드이다.

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

연결에 오류가 없는지 확인 후, 지정한 세션을 만들어 연결된 소켓을 전달해준다. 이를 통해 세션을 시작하고, Connector의 역할은 끝나게 된다.

TCP & UDP

위의 Connect 메소드에 있었던 새 소켓 선언 과정을 보자.

            Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

맨 마지막 인자를 보면, ProtocolType.Tcp라는 인자가 들어간다. 우리는 지금 TCP 프로토콜을 이용해 통신을 하고 있는 것이다. 이는 전에 살펴봤던 TCP/IP 모델에서 트랜스포트(4계층)에 해당하는 프로토콜이다. 트랜스포트 계층에서 대표적인 TCP와 UDP를 비교해보자.

TCP

연결 지향성

TCP의 연결 지향성은 전화 연결에 비유된다.
TCP는 연결형 서비스이다. 즉 전화처럼 연결을 위해 할당되는 경로가 존재한다. 또한 우리가 전화에서 "A, B, C" 라고 전달하면 수신받는 쪽에서도 "A, B, C" 순서대로 들리는 것처럼, 전송 순서가 보장된다는 특징이 있다.

속도와 신뢰성

또한 TCP의 속도/신뢰성은 안전한 트럭에 비유된다. 분실이 일어나면 책임지고 다시 전송하는데, 이런 점이 신뢰성이 좋다는 특징을 갖게 한다. 물건을 주고 받을 상황이 아니면, 일단 보낼 수 있는 일부만 보낸다는 특징이 있다. 또한 이런 여러 상황을 고려하다보니, UDP에 비해서 속도가 다소 느리다.

추가로 TCP는 끊어져서 올 수도 있기 때문에, 언젠가는 완벽한 패킷이 온다는걸 보장하고 걔를 순차적으로 조립해서 사용해야한다.

UDP

연결 지향성

UDP의 연결 지향성은 우편 전송에 비유된다.
UDP는 비연결형 서비스이다. 우편처럼 한번 주소지를 찍고 보내면, 그 다음의 전송 경로는 우리가 알 수 없는 것이다. 때문에 연결이라는 개념이 존재하지 않고, 전송 순서가 따로 보장되지 않는다. 우편을 우리가 전송하면 그 이후는 우체국, 집배원에게 위임되는 것처럼, 일단 보내고 나머지는 큰 생각을 하지 않는다.

속도와 신뢰성

또한 UDP의 속도/신뢰성은 위험한 총알 배송에 비유된다. 일단 단순하기 때문에 속도 자체는 빠르다는 특징이 있다. 하지만, 분실에 대한 책임성이 없기 때문에 신뢰성이 나쁘다.

물론 UDP는 TCP처럼 여러가지를 고려해 따로 따로 전송하거나 하는 일은 없기 때문에, 단순하게 "보내지거나, 중간에 분실되거나"의 상황 밖에는 존재하지 않는다.

게임에서의 적용

멀티플레이 게임에서는 보통 빠르게 실시간 정보를 주고 받아야 하는 FPS게임의 경우에는 UDP, 확실한 신뢰 전송이 필요한 MMORPG게임의 경우에는 TCP를 주로 사용한다고 한다. 물론 UDP 방식을 그냥 사용하기엔 불안정한 측면이 있기 때문에, UDP의 안정성을 개선한 Reliable UDP 등을 사용한다고 한다. 추가로 언리얼 엔진에서 자체 지원하는 Dedicated Server도 UDP 방식으로 구현이 되어있다고 한다.
UDP는 애초에 연결이라는 개념이 없어서 잠깐 끊긴다고 뭐가 큰일나지 않는다. 반면 TCP는 반대로 연결이 끊기면 재연결을 하는 과정이 필요하다. 이런 점을 잘 고려해 어떤 프로토콜을 사용할지 취사선택해야 한다.

profile
마포고개발짱

0개의 댓글