수업

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Net;
using System.Text;
using System.Threading.Tasks;

namespace ServerCore
{
    // 왜 커넥터를 만들어서 사용할까
    // 컨텐츠를 분할해서 만들 경우
    // 메인은 하나로 작동하고
    // 다른 서버와 통신을 하기 위해서는 커넥터 개념이 필요함
    // 서버들끼리의 통신을 위해서
    // 필수적으로 필요하다.

    public class Connector
    {
        private Func<Session> _sessionFactory; // 세션을 생성하는 함수 (Session Factory)
        public void Connect(IPEndPoint endPoint, Func<Session> sessionFactory)
        {
            // 소켓 생성 (TCP 통신)
            Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            _sessionFactory = sessionFactory; // 세션을 생성할 함수 저장

            // 비동기 연결을 위한 SocketAsyncEventArgs 설정
            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.Completed += OnConnectCompleted; // 연결 완료 시 실행될 콜백 함수 지정
            args.RemoteEndPoint = endPoint; // 서버 주소 설정
            // 원하는 정보를 넘겨 줄 수 있음
            args.UserToken = socket; // 연결할 소켓을 UserToken에 저장

            RegisterConnect(args); // 비동기 연결 요청 실행
        }

        private void RegisterConnect(SocketAsyncEventArgs args)
        {
            // 소켓으로 변환해서 받음
            // 커넥터도 여러명 받을 수 있기에 이벤트로 인자를 넘겨줌
            Socket socket = args.UserToken as Socket; // UserToken에서 Socket 가져오기
            if (socket == null) return;

            // 비동기 연결 요청 실행
            bool pending = socket.ConnectAsync(args);

            // ConnectAsync()가 즉시 완료되면 콜백을 직접 실행
            if (pending == false)
                OnConnectCompleted(null, args);
        }

        private void OnConnectCompleted(object sender, SocketAsyncEventArgs args)
        {
            if (args.SocketError == SocketError.Success)
            {
                // 세션 생성 및 연결 시작
                // 컨텐츠에서 요구한 방향으로 만듬
                Session session = _sessionFactory.Invoke();
                // 인자로 받아준 ConnectSocket통해서 연결
                session.Start(args.ConnectSocket);
                session.OnConnected(args.RemoteEndPoint);
            }
            else
            {
                Console.WriteLine($"OnConnectCompleted Fail: {args.SocketError}");
            }
        }
    }
}
using ServerCore;
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}");

            // 서버에 5번 메시지를 전송
            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)
        {
            // DNS
            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)
            {
                // 휴대폰 설정
                Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);


                try
                {
                    //// 문지기한테 입장 문의
                    //socket.Connect(endPoint);

                    //Console.WriteLine($"Connected to {socket.RemoteEndPoint.ToString()}");

                    //// 보낸다
                    //for (int i = 0; i < 5; i++)
                    //{
                    //    byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello Server! {i}");
                    //    int sendByte = socket.Send(sendBuff);
                    //}
                    //// 받는다
                    //byte[] recvBuff = new byte[1024];
                    //int recvBytes = socket.Receive(recvBuff);
                    //string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                    //Console.WriteLine($"[From Server] {recvData}");

                    //socket.Shutdown(SocketShutdown.Both);
                    //socket.Close();
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.ToString());
                }

                Thread.Sleep(100);
            }
            
            

        }
    }
}

✅ 주제

  • 본 강의는 서버와 클라이언트, 혹은 서버 간의 연결을 비동기로 처리하기 위한 Connector 구조를 학습합니다.
  • MMO 게임 구조와 같이 분산 서버 아키텍처를 구성할 때, 서버가 서버에 접속해야 하는 구조에서 Connector는 필수입니다.
  • 또한 기존의 블로킹 기반 연결 방식인 socket.Connect()를 비동기 방식인 ConnectAsync()로 교체하여,
    네트워크 안정성과 확장성을 확보하는 방식도 다룹니다.

📚 개념

  • ConnectorListener와 반대 역할을 수행하는 비동기 연결 담당자입니다.
  • Listener는 외부 연결을 기다리며, Connector는 외부로 연결을 시도합니다.
  • 서버 간 통신이 필요한 구조에서 한쪽은 Listener, 다른 한쪽은 Connector가 됩니다.
  • Connector는 내부에서 Func<Session>으로 SessionFactory를 받아 사용하므로,
    구체적인 Session 구현에 대해 알 필요가 없습니다.
  • 연결 성공 이후에는 Session.Start()로 비동기 수신을 시작하고, OnConnected()를 호출하여 로직을 트리거합니다.

📘 용어정리

용어설명
Connector외부 서버에 비동기로 연결을 시도하는 구조
Listener외부에서 들어오는 연결 요청을 수락하는 구조
Session연결 후 데이터를 송수신하는 추상 클래스
SessionFactory어떤 Session을 만들지 결정하는 델리게이트 함수 (Func<Session>)
ConnectAsync()비동기 연결을 수행하는 Socket 함수
SocketAsyncEventArgs비동기 소켓 통신에 필요한 모든 정보가 담긴 구조체
UserToken사용자가 원하는 정보를 저장하는 객체 필드 (보통 Socket 객체 저장)

💻 코드 분석


1. 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 객체를 준비합니다.
  • 이 객체에는 다음 정보를 담습니다:
    • RemoteEndPoint: 연결 대상
    • UserToken: 연결할 소켓 객체
    • Completed: 연결 완료 시 호출될 콜백 함수 (OnConnectCompleted)
  • 이후 RegisterConnect()를 호출하여 실제 비동기 연결을 시도합니다.

2. 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);
}

📌 주요 포인트

  • UserToken에 넣어두었던 Socket을 꺼내서 사용합니다.
  • ConnectAsync()는 연결을 비동기적으로 수행합니다.
    • true 반환: 연결 요청 중 → 콜백이 자동 호출됨
    • false 반환: 즉시 연결 완료됨 → 직접 OnConnectCompleted() 호출
  • 이 구조는 비동기와 즉시 완료(동기) 상황 모두를 커버합니다.

3. 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}");
    }
}

📌 주요 포인트

  • 연결이 성공하면 외부에서 전달받은 Func<Session>을 통해 Session 객체를 생성합니다.
  • 생성된 세션으로부터:
    • Start()를 호출하여 비동기 수신을 시작
    • OnConnected()를 호출하여 접속 후 초기 로직 실행
  • 실패하면 SocketError를 출력만 하고 종료합니다.

4. GameSession.cs – 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 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}");
    }

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

📌 분석 포인트

  • 연결되자마자 서버에 "Hello World!" 메시지를 5회 전송
  • 서버로부터 데이터 수신 시 콘솔에 출력
  • 송신 후 바이트 수 출력
  • 연결 끊김 시 로그 출력

🔄 전체 흐름 요약

[DummyClient]
    → Connector.Connect()
        → ConnectAsync()
            → 연결 완료 시 OnConnectCompleted()
                → Session 생성 및 Start()
                → OnConnected()

[GameSession]
    → OnConnected() → 메시지 송신
    → OnRecv() → 서버 응답 수신 처리

✅ 핵심

항목설명
Connector 역할서버에 비동기 연결을 시도하고 세션을 시작
Connect()세션 생성 방식 지정, 연결 설정
RegisterConnect()ConnectAsync() 호출로 비동기 연결 시작
OnConnectCompleted()연결 성공 시 Session 생성 → Start → OnConnected
GameSession실제 통신 로직 구현, 연결 시 메시지 송신 등

profile
李家네_공부방

0개의 댓글