서버 공부

[서버 공부]12 Session 1,2

myjeongjun 2025. 1. 12. 03:04
        static void OnAcceptHandler(Socket clientSocket) 
        {
            try 
            {
                //받기
                byte[] recvbuffer = new byte[1024];
                int recvByte = clientSocket.Receive(recvbuffer);
                string recvData = Encoding.UTF8.GetString(recvbuffer, 0, recvByte);
                Console.WriteLine($"[From Client] {recvData}");

                //보내기
                byte[] senbuffer = Encoding.UTF8.GetBytes("Welcome to MMORPG Server!");
                clientSocket.Send(senbuffer);

                //쫓아낸다.
                clientSocket.Shutdown(SocketShutdown.Both);
                clientSocket.Close();
            }
            catch(Exception ex)  
            {
                Console.WriteLine(ex.ToString());
            }

        }

이제 위의 부분또한 비동기 작업을 시켜줘야한다.

 

저 작업은 따로 Session 클래스에 분리해서 정의해주도록 하자

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

namespace ServerCore
{
    internal class Session
    {
        Socket _socket;
        int _disconnected = 0;

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

            RegisterRecv(recvArgs);
        }

        public void Send(byte[] sendBuffer) 
        {
            _socket.Send(sendBuffer);
        }

        public void Disconnect() 
        {
            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }

        void RegisterRecv(SocketAsyncEventArgs args) 
        {
            bool pending = _socket.ReceiveAsync(args);
            if (!pending) 
            {
                OnRecvCompleted(null, args);
            }
        }

        void OnRecvCompleted(object sender, SocketAsyncEventArgs args) 
        {
            if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success) 
            {
                try
                {
                    string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred); ;
                    Console.WriteLine($"[From Client] {recvData}");
                    RegisterRecv(args);

                }
                catch (Exception e) 
                {
                    Console.WriteLine($"OnRecvCompleted Faild {e}");
                }
            }
            else 
            {

            }
        }

    }
}

 

분리해서 분석해보자

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

            RegisterRecv(recvArgs);
        }

이 부분은  이전글

 

#12. Listener

#11. 소켓프로그래밍에서 block 계열의 함수안 Aceept ,Receive, Send 를 사용해서 구현해보았다. 하지만 Aceept같은 경우에는 아주 드물게 block계열로 구현할순 있으나 Receive,Send같은 함수의 경우 데이터

myjeongjun.tistory.com

과 비교했을때 구현면에서 크게 차이점은없다 SocketAsyncEventArgs 객체를만들어주고 이벤트 등록을해준다.

 

Send는 Receive를 구현하고나서 아래에 서술하겠다.

        public void Send(byte[] sendBuffer) 
        {
            _socket.Send(sendBuffer);
        }

 

기존 방식과 동일하게 함수로 만들어서 사용한다.

        public void Disconnect() 
        {
            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }

 

현재 작업중인지를 나타내고 완료되면 OnRecvCompleted이 자동으로 SocketAsyncEventArgs에 의해 불러질것이다.

아래에는 비동기 작업이 즉시 완료되는 경우에는 이벤트가 발생하지 않아서 직접 호출을 해줘야 한다.

어째서 그런지는 추가적으로 조사해보겠습니다.

        void RegisterRecv(SocketAsyncEventArgs args) 
        {
            bool pending = _socket.ReceiveAsync(args);
            if (!pending) 
            {
                OnRecvCompleted(null, args);
            }
        }

 

마지막으로 이벤트에 등록된 함수이다. 조건에 맞으면 Encoding 작업으로 데이터를 가져오고 출력한다.

   void OnRecvCompleted(object sender, SocketAsyncEventArgs args) 
   {
       if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success) 
       {
           try
           {
               string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred); ;
               Console.WriteLine($"[From Client] {recvData}");
               RegisterRecv(args);

           }
           catch (Exception e) 
           {
               Console.WriteLine($"OnRecvCompleted Faild {e}");
           }
       }
       else 
       {

       }
   }

 

이제 OnAcceptHandler 함수를 아래와 같이 고치고 실행해보면

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

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

         Thread.Sleep(1000);

         session.Disconnect();
     }
     catch(Exception ex)  
     {
         Console.WriteLine(ex.ToString());
     }

 }

 

 

성공적으로 받아오는것을 볼 수있다.

 

 

이때 개선해야할 사항으로 Disconnect 함수를 볼 수있는데, 갑자기 여러개의 Disconnect의 요청이오면 멀티스레드 환경에서 오류를 발생시키기 충분해보인다.

 

이걸 막기위해 단순히 flag를 둬서 0을 1로 바꾸는 식으로 구현하면 멀티스레드 환경에서 당연히 오류가 발생할게 뻔하다(원자적으로 바꿔야함) 그러므로 이전에 배운 Interlock을 이용해서

 

        public void Disconnect() 
        {
            if (Interlocked.Exchange(ref _disconnected, 1) == 0)
                return;

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

 

이런식으로 구현하면 Disconnect가 여러개와도 잘 처리할수있게된다.

 

 

Send같은 경우에는 조금 사정이 다르다.

만약 Receive와 동일하게 구현했다고 치자

       public void Send(byte[] sendBuffer) 
       {
           SocketAsyncEventArgs sendArgs = new SocketAsyncEventArgs();
           sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
           sendArgs.SetBuffer(sendBuffer,0, sendBuffer.Length);

           RegsiterSend(sendArgs);
       }

       void RegsiterSend(SocketAsyncEventArgs args) 
       {
           bool pending = _socket.SendAsync(args);
           if (!pending) 
           {
               OnSendCompleted(null, args);
           }

       }

       void OnSendCompleted(object send, SocketAsyncEventArgs args) 
       {
           if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success) 
           {
               try
               {
                    RegsiterSend(args);

               }
               catch (Exception e)
               {
                   Console.WriteLine($"OnRecvCompleted Faild {e}");
               }
           }
           else 
           {
               Disconnect();
           }
       }

그럼 이런식으로  Recv를 Send로 고쳐서 나올것이다.

 

 

 

Receive는

void OnRecvCompleted(object sender, SocketAsyncEventArgs args) 
{
    if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success) 
    {
        try
        {
            string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred); ;
            Console.WriteLine($"[From Client] {recvData}");
            RegisterRecv(args);

        }
        catch (Exception e) 
        {
            Console.WriteLine($"OnRecvCompleted Faild {e}");
        }
    }
    else 
    {
        Disconnect();
    }
}

여러명한테서 무언갈 받은경우가 존재하므로 수신이 끝났으면 다시 RegisterRecv를 해주는게 마땅하고,

Send같은 경우 보내는 사람은 나 한 명 밖에없으므로 위에있는 Recv와 동일하게 구현한다는것은

 

보냈던걸 다시 보내는 꼴이된다. 애초애 둘이 같은식으로 동작하는게 아니라는 것이다. (아래에는 Recv와 똑같이 구현했을때,잘못된 구현)

        void OnSendCompleted(object send, SocketAsyncEventArgs args) 
        {
            if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success) 
            {
                try
                {

                    RegsiterSend(args);


                }
                catch (Exception e)
                {
                    Console.WriteLine($"OnRecvCompleted Faild {e}");
                }
            }
            else 
            {
                Disconnect();
            }
        }

 

그렇다는말은 굳이 이런식으로 보낼때마다 새롭게 sendArgs를 만든다는건 비효율적이라는 소리로 연결된다. 비유를 하자면 같은공간에 유저가 100명 존재한다했을때 1명이 걸을때마다 나머지한테 전부 움직였다는걸 Send 해줘야하는데,매번 sendArgs를 만들면서 Send한다고 생각하면 당연히 비효율적이고, 쓰던 버퍼를 다쓰고 교체하는것이 효율적으로 보인다.

        public void Send(byte[] sendBuffer) 
        {
            SocketAsyncEventArgs sendArgs = new SocketAsyncEventArgs();
            sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
            sendArgs.SetBuffer(sendBuffer,0, sendBuffer.Length);

            RegsiterSend(sendArgs);
        }

 

 

개선 시킨 버전

    internal class Session
    {
        Socket _socket;
        int _disconnected = 0;

        object _lock = new object();
        Queue<byte[]> _sendQueue = new Queue<byte[]>();
        bool _pending = false;
        SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();

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


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

            RegisterRecv(recvArgs);
        }

        public void Send(byte[] sendBuffer) 
        {
            lock (_lock) 
            {
                _sendQueue.Enqueue(sendBuffer);
                if (!_pending) 
                {
                    RegsiterSend();
                }
            }
            
            _sendArgs.SetBuffer(sendBuffer,0, sendBuffer.Length);

            
        }

        void RegsiterSend() 
        {
            _pending = true;
            byte[] buff = _sendQueue.Dequeue();
            _sendArgs.SetBuffer(buff,0,buff.Length);

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

        }

        void OnSendCompleted(object send, SocketAsyncEventArgs args) 
        {
            if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success) 
            {
                try
                {
                    if (_sendQueue.Count > 0)
                    {
                        RegsiterSend();
                    }
                     _pending = false;
                }
                catch (Exception e)
                {
                    Console.WriteLine($"OnRecvCompleted Faild {e}");
                }
            }
            else 
            {
                Disconnect();
            }
        }

 

 

달라진점은 _sendArgs을 멤버변수로 만들어 주고 송신할 녀석들의 순서를 보장해주기위해 queue를 사용했다.

        public void Send(byte[] sendBuffer) 
        {
            lock (_lock) 
            {
                _sendQueue.Enqueue(sendBuffer);
                if (!_pending) 
                {
                    RegsiterSend();
                }
            }
            
            _sendArgs.SetBuffer(sendBuffer,0, sendBuffer.Length);

            
        }

이것은 멀티스레드 환경을 고려하여 send를 동시에 실행시키면 안되기때문에 lock을 이용해서 구현해주었고 lock을 잡았을때 일단 큐에 넣어주고 누군가 작업중이라면 lock을 풀고 작업중이지 않으면 바로 Register작업으로 들어가준다.

  void RegsiterSend() 
  {
      _pending = true;
      byte[] buff = _sendQueue.Dequeue();
      _sendArgs.SetBuffer(buff,0,buff.Length);

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

  }

이 작업에서는 순서대로 Dequeu해줘서 SendAsync작업을 수행해준다.

SendAsync작업이 완료되면 자동으로 OnSendCompleted함수를 호출하도록 이벤틀르 등록하였으므로

        void OnSendCompleted(object send, SocketAsyncEventArgs args) 
        {
            lock (_lock) 
            {
                if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
                {
                    try
                    {
                        if (_sendQueue.Count > 0)
                        {
                            RegsiterSend();
                        }
                        _pending = false;
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine($"OnRecvCompleted Faild {e}");
                    }
                }
                else
                {
                    Disconnect();
                }
            }

        }

OnSendCompleted가 호출되었을때 이걸 처리하기 위해 여러 스레드가 경쟁하는것을 방지하기 위해 이부분도 lock으로 감싸준다.내부에는 큐를 비울때까지 RegisterSend를 호출하는것을 볼 수있다.

 

이후에 유저가 추가적인 동작으로 Send를 시도해도

비동기적으로 Send를 처리하고있으므로 새로 Send를 보내도 큐에 집어넣고 자신의 처리 순서를 기다리는 형태가 될것이다.