서버/서버 이론

Session 구현

잼잼재미 2025. 4. 13. 15:52

Session 구현


Listener class를 따로 만들어서 보완했던 것 처럼, Receive, Send 함수 부분도 블로킹 방식에서 비동기 방식으로 보완을 해야한다. Receive는 Listener과 비슷하지만, Send는 조금 더 복잡하다. 먼저, ServerCore에서 Session class를 추가하자.

 

 

코드 작성 및 수정


Session

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

namespace ServerCore
{
    abstract class Session
    {
        Socket _socket;
        int _disconnect = 0;

        object _lock = new object(); 
        Queue<byte[]> _sendQueue = new Queue<byte[]>();
        List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
        SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
        SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();

        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);

        public void Start(Socket socket)
        {
            _socket = socket;
            _recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
            _recvArgs.SetBuffer(new byte[1024], 0, 1024);

            _sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
           
            RegisterRecv();
        }

        public void Send(byte[] sendBuff) 
        {
            // 동시다발적으로 Send하는 경우가 있기 때문에, lock 사용
            lock (_lock)
            {
                // 따로 따로 Send하지 않고, 한번에 모아서 전송하기 위해
                _sendQueue.Enqueue(sendBuff);
                if (_pendingList.Count == 0)
                    RegisterSend();
            }
        }

        void RegisterSend()
        {
            while (_sendQueue.Count > 0)
            {
                byte[] buff = _sendQueue.Dequeue();
                _pendingList.Add(new ArraySegment<byte>(buff, 0, buff.Length)); 
            }

            _sendArgs.BufferList = _pendingList; 

            bool pending = _socket.SendAsync(_sendArgs);
            if (pending == false)
                OnSendCompleted(null, _sendArgs); 
        }

        void OnSendCompleted(object sender, SocketAsyncEventArgs args)
        {
            lock (_lock)
            {
                if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
                {
                    try
                    {
                        _sendArgs.BufferList = null; // 더이상 굳이 pendingList를 갖고 있을 필요 없으니까
                        _pendingList.Clear(); // bool 역할을 얘가 대신 해주는 거. 
                        
                        OnSend(_sendArgs.BytesTransferred); 

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

        public void Disconnect()
        {
            // Disconnect가 동시에 실행되는 것을 막기 위해
            if (Interlocked.Exchange(ref _disconnect, 1) == 1)
                return;

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

        #region 네트워크 통신

        void RegisterRecv()
        {
            bool pending = _socket.ReceiveAsync(_recvArgs);
            if (pending == false)  
                OnRecvCompleted(null, _recvArgs);
        }

        void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
        {
            if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
            {
                // TODO
                try
                {
                    //string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
                    //Console.WriteLine($"[FromClient] {recvData}");
                    OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred)); 
                    RegisterRecv(); 
                }
                catch(Exception e)
                {
                    Console.WriteLine($"OnRecvCompletedFailed {e}"); 
                }
            }
            else
            {
                Disconnect(); 
            }                
        }
        #endregion
    }
}

 

 

Program (ServerCore)

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

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

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

        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($"[FromClient] {recvData}");
        }

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

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

        //static void OnAcceptHandler(Socket clientSocket)
        //{
        //    try
        //    {
        //        //GameSession session = new GameSession();
        //        //session.Start(clientSocket);

        //        byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !!");
        //        session.Send(sendBuff);

        //        Thread.Sleep(1000);

        //        session.Disconnect();
        //        session.Disconnect(); 
        //    }
        //    catch (Exception e)
        //    {
        //        Console.WriteLine(e);
        //    }
        //}
        static void Main(string[] args)
        {
            // DNS ( Domain Name System )
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
            // ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호

            //_listener.Init(endPoint, OnAcceptHandler);
            _listener.Init(endPoint, ()=> { return new GameSession(); });

            Console.WriteLine("Listening...");

            while (true)
            {
                ;
            }
        }
    }
}

 

 

DummyClient

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace DummyClient
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // DNS ( Domain Name System )
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
            // ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호        }
            // 식당 주소 찾는 부분은 똑같을 거야. 

            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 World! {i}");
                        int sendBytes = 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);
            }
        }

 

 

Listener

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace ServerCore
{
    internal 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); // TCP로 할 때 설정
            //_OnAcceptHandler += onAcceptHandler;
            _sessionFactory += sessionFactory; 

            // 문지기 교육
            _listenSocket.Bind(endPoint); // 식당 주소와 후문인지 정문인지 기입을 해준 것

            // 영업 시작
            // backlog : 최대 대기수  
            _listenSocket.Listen(10);

            for (int i = 0; i < 10; i++)
            {
                SocketAsyncEventArgs args = new SocketAsyncEventArgs();
                args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
                RegisterAccept(args);
            }
        }

        void RegisterAccept(SocketAsyncEventArgs args)
        {
            args.AcceptSocket = null;

            bool pending = _listenSocket.AcceptAsync(args);
            if (pending == false)    // 운 좋게 바로 클라이언트가 접속했을 경우
                OnAcceptCompleted(null, args);
        }

        void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
        {
            if (args.SocketError == SocketError.Success) // 모든 게 잘 처리 됐다는 뜻
            {
                // 유저가 커넥트 요청 해서 Accept 했으면 여기서 뭔가를 해주고 
                // TODO
                // _OnAcceptHandler.Invoke(args.AcceptSocket);
                // GameSession session = new GameSession();
                Session session = _sessionFactory.Invoke();
                session.Start(args.AcceptSocket);
                session.OnConnected(args.AcceptSocket.RemoteEndPoint);
            }
            else
                Console.WriteLine(args.SocketError.ToString());

            RegisterAccept(args); // 다음 아이를 위해서 또 한번 등록을 해주는 거
        }

        public Socket Accept()
        {
            return _listenSocket.Accept();
        }

    }
}

'서버 > 서버 이론' 카테고리의 다른 글

Connector 구현  (0) 2025.04.20
Listener 구현  (0) 2025.04.12
소켓 프로그래밍 기초  (0) 2025.04.09
네트워크 기초 이론  (0) 2025.04.07
Thread Local Storage (TLS)  (0) 2025.04.06