[server 프로그램 c#] ServerCore Session (소켓 비동기 구현)

코찔찔이·2023년 10월 21일
0
  1. 기능을 ServerCore 라이브러리로 생성 및Accept, Connection, Receive, Send등 블로킹 함수를 비동기 함수로 변환.
  2. Server Session생성은 Listener, client Session은 Connector클래스에서 생성.

1. ServerCore

1. Session.cs

서버에 접속하는 클라이언트가 수백 ~ 수천이 될 경우 각각의 연결을 session으로 관리 하면 편하기 때문에 session을 만들어 관리.

1.Send

👻 Send()

  1. Send 메소드에서 전송할 데이터를 byte[] 형태로 받음.
  2. _sendQue , _pendingList는 공유 자원으로 main Thread와
    SocketAsyncEventArgs에 의해 생성된 Thread가 동시에 접근하면 안되어 lock 키워드로 한번에 한개의 쓰레드에서 접근하도록 함.
  3. _pendingList.Count > 0 이상이라면 현재 이전에 전송명령이 실행중으로 바로 전송하지 않고 que에 전송할 데이터만 넣어두고 종료.

👻 RegisterSend()

  1. que에 데이터가 여러건 있을경우 한번에 한건씩 전송이 아닌 _sendArgs.BufferList에 데이터를 ArraySegment형태로 리스트로 담아 한번에 전송.
  2. _socket.SendAsync(_sendArgs); 현재 전송이 가능 할경우 false를 반환하며 전송을 할 수 없는 상태일경우 true를 반환하며 전송을 진행 하지 않고 종료 후 전송이 가능한 시점에 SocketAsyncEventArgs에 의해
    데이터 전송.

👻 OnSendCompleted()

  1. 소켓 에러 확인, args.BytesTransferred 전송 바이트 확인 후 이상 없을 경우 컨텐츠단에 전송 완료를 보내고 que에 전송할 데이터가 남아있다면 다시 RegisterSend()실행 하여 남은 데이터 전송.

👻 Disconneted()

  1. if ( Interlocked.CompareExchange(ref _disconneted, 1, 0) != 0)
    return;
  2. 동기화 함수로 Interlocked.CompareExchange(ref ulong location1, ulong value, ulong comparand)
    반환 값으로 기존값을 반환하며, 현재 값고 비교 값이 같을경우 location1의 값을 value으로 변경함.
    반환값이 0이라면 이미 해당 메소드가 실행됬다는 의미로 더이상 밑에 코드를 실행하지 않음.
    msdn참조
    location1 : comparand와 비교하여 바뀔 수 있는 값을 가진 대상입니다.
    value : 비교 결과가 같은 경우 대상 값을 바꿀 값입니다.
    comparand : location1의 값과 비교할 값입니다.
    반환값 : location1의 원래 값입니다.

👻Session.cs 코드

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

namespace ServerCore
{
    public abstract class Session
    {
        Socket _socket;
        int _disconneted = 0; //커넥션 확인 하는 플레그

        List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
        Queue<byte[]> _sendQue = new Queue<byte[]>();
        SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
        SocketAsyncEventArgs _receiveArgs = new SocketAsyncEventArgs();
        object _sendLock = new object();

        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 clientSocket)
        {
            _socket = clientSocket;

            //데이터 수신 완료 후 실행할 메소드 설정.
            _receiveArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted); 
            _receiveArgs.SetBuffer(new byte[1024], 0, 1024); //버퍼 사이즈 설정.
            RegisterRecv();// receive등록

            //데이터 송신 완료 후 실행할 메소드 설정.
            _sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
        }

        public void Disconneted()
        {
            if ( Interlocked.CompareExchange(ref _disconneted, 1, 0) != 0)
                return;

            OnDisconnected(_socket.RemoteEndPoint);

            //예고 
            _socket.Shutdown(SocketShutdown.Both);
            //연결끊기
            _socket.Close();
        }

        public void Send(byte[] sendBuffe)
        {
            /* SocketAsyncEventArgs 에서 실행되는 Thread와 Send를 호출하는 컨턴츠단 Thread가 동시에 같은 자원에 접근시
             * 문제가 발생하여 동기화 lock 적용
             * 데이터를 바로 전송할 수 있는 상태가 아닐경우 que에 넣고 넘김.
             */
            lock (_sendLock)
            {
                _sendQue.Enqueue(sendBuffe);
                if (_pendingList.Count == 0)
                    RegisterSend();
            }
            //_socket.Send(sendBuffe);
        }

        #region 네트워크 통신

        void RegisterSend()
        {
            /*
             * _sendArgs.BufferList : 데이터 전송을 한건씩 하는게 아닌 보낼 데이터를 묶어서 한번에 보냄.
             * 
             */
            while (_sendQue.Count >0)
            {
                byte[] buff = _sendQue.Dequeue();
                _pendingList.Add(new ArraySegment<byte>(buff, 0, buff.Length));
            }
            _sendArgs.BufferList = _pendingList;

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

        void OnSendCompleted(object sender, SocketAsyncEventArgs args)
        {
            lock(_sendLock)
            {
                if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
                {
                    try
                    {
                        _pendingList.Clear();
                        _sendArgs.BufferList = null;

                        OnSend(args.BytesTransferred);

                        // 보낼데이터가 남아있는지 확인 후 남아있다면 전송 로직을 다시 탐.
                        if (_sendQue.Count > 0)
                            RegisterSend();
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine($"OnSendCompleted Failed {e}");
                    }
                }
                else
                {
                    Disconneted();
                }
            }
        }

        void RegisterRecv()
        {
            /* _socket.ReceiveAsync 데이터가 수신되면 false 반환. 없을 경우 true를 반환 하며 이후 데이터가 수신되면
             * SocketAsyncEventArgs에 의해 OnRecvCompleted() 실행.
            */
            bool pending = _socket.ReceiveAsync(_receiveArgs);
            if (!pending)
                OnRecvCompleted(null, _receiveArgs);
        }

        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 Failed {e}");
                }
            }
            else
            {
                Disconneted();
            }

        }
        #endregion
    }
}

2.Receive

👻 RegisterRecv()

  1. bool pending = _socket.ReceiveAsync(_receiveArgs); 데이터가 수신되면 false, 아니라면 true를 반환하며 데이터가 수신되면 SocketAsyncEventArgs의해 OnRecvCompleted() 실행.

👻 OnRecvCompleted()

  1. 커넥션되거나 종료될때 가끔 args.BytesTransferred == 0 으로 들어오는 경우가 있어 전송 바이트 체크를함.

2.Listener.cs

  1. Func _seesionFactory로 Session을 받는 이유는 Session은 추상클래스로 컨텐츠단에서 Session을 상속 받아 구현한 class를 사용하여 ServerCore에서는 컨텐츠단에서 구현한 class를 전달 받기 위해 사용.

👻 init()

  1. 매게변수로 endPoint와 콘텐츠단에서 생성한 Session을 생성할 함수를 받음.
  2. RegisterAccept()를 호출해 client를 받을 준비함.

👻 RegisterAccept()

  1. bool pending = _listenSocket.AcceptAsync(args); 접속을 한 client가 있을경우 false, 아니라면 true를 반환 하며 이후 접속이 되면
    SocketAsyncEventArgs에 의해 Onacceptcompleted() 실행.

👻 Onacceptcompleted()

  1. Session session = _seesionFactory.Invoke(); 콘텐츠에서 받은 함수를 실행 하여 Session 객체 인스턴스.
  2. session.Start(args.AcceptSocket);를 실행해 socket 전달 및 데이를 수신받을수 있는 상태로 만듬.
  3. session.OnConnected(args.AcceptSocket.RemoteEndPoint); 컨텐츠단에 소켓 전달 하여 데이터 전송 및 수신 할 수 있도록 함.
    이후 RegisterAccept(args);를 실행 해 다음 client를 받을 수 있도록 셋팅.

👻Listener.cs 코드

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

namespace ServerCore
{
    public class Listener
    {
        Socket _listenSocket;
        Func<Session> _seesionFactory; //클라이언트 접속 되면 접속 유무 콜백


        public void init(IPEndPoint endPoint, Func<Session> seesionFactory)
        {
            //문지기
            _listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            _seesionFactory += seesionFactory;

            //문지기 교육
            _listenSocket.Bind(endPoint);

            //최대 대기수
            _listenSocket.Listen(10);

            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            //클라 접속 되면 연결 해준 함수로 콜백 해줌
            args.Completed += new EventHandler<SocketAsyncEventArgs>(Onacceptcompleted);
            RegisterAccept(args);

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

        }

        void RegisterAccept(SocketAsyncEventArgs args)
        {
            args.AcceptSocket = null;
            bool pending = _listenSocket.AcceptAsync(args);
            if (!pending) // pending true면 아직 접속된 클라가 없다는 뜻.
                Onacceptcompleted(null, args);
        }

        void Onacceptcompleted(object sender, SocketAsyncEventArgs args)
        {
            if (args.SocketError == SocketError.Success)
            {
                Session session = _seesionFactory.Invoke();
                session.Start(args.AcceptSocket);
                session.OnConnected(args.AcceptSocket.RemoteEndPoint);
            }
            else
                Console.WriteLine(SocketError.SocketError);

            RegisterAccept(args);
        }

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

3.Connertor.cs

  1. Func _seesionFactory로 Session을 받는 이유는 Session은 추상클래스로 컨텐츠단에서 Session을 상속 받아 구현한 class를 사용하여 ServerCore에서는 컨텐츠단에서 구현한 class를 전달 받기 위해 사용.

👻 Connect()

  1. 매게변수로 endPoint와 콘텐츠단에서 생성한 Session을 생성할 함수를 받음.
  2. RegisterConnect()를 호출해 Server에 Connection 시도.

👻 RegisterConnect()

  1. bool pending = socket.ConnectAsync(args); server에 연결을 성공하면 false, 아니라면 true를 반환 하며 이후 접속이 되면
    SocketAsyncEventArgs에 의해 Completeconnect() 실행.

👻 Completeconnect()

  1. Session session = _seesionFactory.Invoke(); 콘텐츠에서 받은 함수를 실행 하여 Session 객체 인스턴스.
  2. session.Start(args.AcceptSocket);를 실행해 socket 전달 및 데이를 수신받을수 있는 상태로 만듬.
  3. session.OnConnected(args.AcceptSocket.RemoteEndPoint); 컨텐츠단에 소켓 전달 하여 데이터 전송 및 수신 할 수 있도록 함.
    이후 RegisterAccept(args);를 실행 해 다음 client를 받을 수 있도록 셋팅.

👻Connector.cs 코드

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

namespace ServerCore
{
    public class Connector
    {
        Func<Session> _seesionFactory; //클라이언트 접속 되면 접속 유무 콜백

        public void Connect(IPEndPoint endPoint, Func<Session> seesionFactory)
        {
            //휴대폰 설정(소켓)
            Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            _seesionFactory = seesionFactory; //콜백 함수 연결

            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.Completed += new EventHandler<SocketAsyncEventArgs>(Completeconnect);
            args.RemoteEndPoint = endPoint;
            args.UserToken = socket;
            RegisterConnect(args);
        }

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

            bool pending = socket.ConnectAsync(args);
            if (!pending)
                Completeconnect(null, args);
        }

        void Completeconnect(object send, SocketAsyncEventArgs args)
        {
            if(args.SocketError == SocketError.Success)
            {
                Session session = _seesionFactory.Invoke();
                session.Start(args.ConnectSocket);
                session.OnConnected(args.RemoteEndPoint);
            }
            else
            {
                Console.WriteLine($"Completeconnect Faile : {args.SocketError}");
            }
        }
    }
}

4.Server.Prgram.cs

  1. ServerCore에서 생성한 Session을 상속받은 GameSession 클래스 생성.
  2. connection,Receive,Send,DisConnected 구현.
  3. 접속이 완료되면 client에게 "welcome to MMORPG Server !..." 메시지 전송.
  4. Program.cs 에서
    _listener.init(endPoint, () => { return new GameSession(); }); 무명 메소드로 Func()로 GameSession을 생성하는 메소드 전달.
    인스턴스해서 전달이 아닌 Func로 전달하는 이유는 Connection이 되었을때 객체를 인스턴스 하기 위함.

👻Connector.cs 코드

using System;
using System.Net;
using System.Text;
using System.Threading;
using ServerCore;

namespace Server
{

    class GameSession : Session
    {
        public override void OnConnected(EndPoint endPoint)
        {
            byte[] sendBuffe = Encoding.UTF8.GetBytes("welcome to MMORPG Server !...");
            Send(sendBuffe);

            Thread.Sleep(1000);

            Disconneted();
            Disconneted();
        }

        public override void OnDisconnected(EndPoint endPoint)
        {
        }

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

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


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


        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipaddr = ipHost.AddressList[0]; //아이피가 여러개 있을수 있으며 배열로 ip를 반환함
            IPEndPoint endPoint = new IPEndPoint(ipaddr, 7777);

            _listener.init(endPoint, () => { return new GameSession(); });

            while (true)
            {

            }
        }
    }
}

5.DummyClient.Prgram.cs

  1. ServerCore에서 생성한 Session을 상속받은 GameSession 클래스 생성.
  2. connection,Receive,Send,DisConnected 구현.
  3. 접속이 완료되면 Server에게 "Hellow world!" 메시지 다서번 전송 후 연결 종료.
  4. Program.cs 에서
    _listener.init(endPoint, () => { return new GameSession(); }); 무명 메소드로 Func()로 GameSession을 생성하는 메소드 전달.
    인스턴스해서 전달이 아닌 Func로 전달하는 이유는 Connection이 되었을때 객체를 인스턴스 하기 위함.

👻Connector.cs 코드

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

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

            for (int i = 0; i < 5; i++)
            {
                //보낸다.(블로킹 함수)
                byte[] sendbuff = Encoding.UTF8.GetBytes($"Hellow world! {i}");
                Send(sendbuff);
            }

            Thread.Sleep(1000);

            Disconneted();
            Disconneted();
        }

        public override void OnDisconnected(EndPoint endPoint)
        {
        }

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

        public override void OnSend(int numOfBytes)
        {

            Console.WriteLine($"SendData Byte : {numOfBytes }");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipaddr = ipHost.AddressList[0]; //아이피가 여러개 있을수 있으며 배열로 ip를 반환함
            IPEndPoint endPoint = new IPEndPoint(ipaddr, 7777);

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

            while (true)
            {
                try
                {
                    
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.Message);
                }

                Thread.Sleep(100);
            }
        }
    }
}

6.실행 화면

0개의 댓글