공부/인프런 - Rookiss

Part 4-3-7. 네트워크 프로그래밍 : Session #3 (Send 개선)

셩잇님 2023. 9. 22. 18:58
반응형

 

 

네트워크 프로그래밍

 

 지난 시간에서 우리는 새로운 클래스인 Session을 만들어 소켓 프로그래밍의 Receive 부분과 Send 부분을 분리하여 작업을 진행했다. 오늘은 이어서 Send 부분의 코드 로직을 조금 더 우아하게 사용하기 위해 개선할 것이다.

 

 


 

 

Send 개선

 

    class Session
    {
        Socket _socket;
        int _disconnected = 0;

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

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

 

_sendArgs는 보낼 때 버퍼 사이즈가 정해지기 때문에 Receive 처럼 재사용할 수가 없다. 따라서 세션 클래스 내부에 멤버 변수로 변경해주어 사용한다. 이 때 C++ 서버 구조와 유사하기 맞추기 위해 _recvArgs 또한 같이 멤버 변수로 빼주어 사용한다. 이렇게 처리하게 되면 기존의 RegisterRecv() 메서드에서 매개 변수 사용하지 않고 처리할 수 있으며 내부 로직은 아래와 같이 변경하여 진행한다.

 

        void RegisterRecv()
        {
            // 전역변수로 선언한 _recvArgs의 값을 이용한다.
            bool pending = _socket.ReceiveAsync(_recvArgs);
            if (pending == false)
            {
                OnRecvCompleted(null, _recvArgs);
            }
        }

 

 


 

 

List를 이용한 일괄처리

 

 이전 시간에 작성한 RegisterSend() 내부 코드를 보면 SetBuffer 1개. 즉 버퍼라는 통짜 하나를 사용하는 방식으로 이루어져 있다. 그러나 BufferList() 메서드 기능을 이용하면 리스트에 버퍼 정보를 담아 전달해 줄 수 있다. 즉 Buffer를 한 번에 한 개, 또 다른 Buffer를 한 번에 한 개, 이런 방식으로 동작하던 로직을 리스트를 이용하여 한번에 슈루룽~ 보내는 것이다.

 

 하지만 BufferList의 사용법은 일단 리스트와는 조금 다르다. 아래 이미지만 봐도 알 수 있듯이 배열의 일부분을 사용하는 것을 볼 수 있으며, 내부 인자 값으로는 사용하고자 하는 배열, 시작 인덱스, 길이를 설정해주어 사용자가 원하는 배열의 시작 위치, 길이만큼을 복사해서 사용할 수 있게 해준다.

 

 

 

 즉, 아래와 같이 사용해야 한다.

 

// _sendArgs.BufferList.Add(new ArraySegment<byte>(buff, 0, buff.Length));
// ❌

_pendinglist.Add(new ArraySegment<byte>(buff, 0, buff.Length));
// ⭕

 

 다만 위와같이 리스트에 직접적으로 데이터를 넣을 경우 문제가 생길 수 있으므로 List에 정보를 담고, 리스트를 또 버퍼 리스트에 담아서 사용해야 한다. 조금은 복잡한 편 😩.. 따라서 전체 코드는 아래와 같다.

 

        List<ArraySegment<byte>> _pendinglist = new List<ArraySegment<byte>>();

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

 

 즉, 위에 설명한 것과 같이 리스트를 따로 제작하고... 해당 리스트에 Add를 통해 데이터를 넣어주고... 이 리스트를 또 버퍼리스트에 담아주는 일련의 과정을 거쳐야 한다. 보다 자세한 사용법은 아래 MSDN 링크를 참고한다. 😎

https://learn.microsoft.com/ko-kr/dotnet/api/system.net.sockets.socketasynceventargs.bufferlist?view=net-7.0 

 

SocketAsyncEventArgs.BufferList 속성 (System.Net.Sockets)

비동기 소켓 메서드에 사용할 데이터 버퍼의 배열을 가져오거나 설정합니다.

learn.microsoft.com

 

 


 

 

생성한 _pendinglist를 통한 pending 관리

 

 이전까지는 _pending 이라는 bool 값을 전역 변수를 통해 관리하였다. 하지만 이제 리스트를 이용하여 해당 값을 처리하고 관리한다. 

 

Send

 

        public void Send(byte[] sendBuff)
        {
            lock (_lock)
            {
                _sendQueue.Enqueue(sendBuff);
                if (_pendinglist.Count == 0)
                {
                    RegisterSend();
                }
            }
        }

 

 카운트를 통해 로직을 처리한다.

 

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

 

 일전 로직에서는 _pending의 값을 true 처리하여 작업을 진행하였지만, 이제 큐를 통해 처리를 진행한다. 👍

 

OnSendCompleted

 

        void OnSendCompleted(object sender, SocketAsyncEventArgs args)
        {
            lock (_lock)
            {
                if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
                {
                    try
                    {
                        _sendArgs.BufferList = null;
                        // 성공적으로 데이터를 모두 전송했으므로 리스트를 비워준다.
                        _pendinglist.Clear();

                        // 로그
                        Console.WriteLine($"Transferred bytes: {_sendArgs.BytesTransferred}");

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

 

 마지막으로 성공적으로 데이터를 모두 전송했으면 리스트를 비워주고, 버퍼 리스트도 초기화한다. 끝! 😤.. 인줄 알았지만 또 개선할 부분이 필요하다. 🤯..

 

 


 

 

무엇을 또 개선해야할까?

 

 첫째. 바로 패킷이 비정상적으로 몰리는 상황이다. 현재 RegisterSend 메서드에서 while (_sendQueue.Count > 0) 큐를 모두 비울 때 까지 정보를 보내고 있는데, 이는 Receive할 때와 마찬가지로 동일한 문제를 가지고 있다. 즉. 누군가 악의적으로 의미 없는 정보들을 마치 디도스 공격과 같이 계속 보낼 경우 큐에 있는 정보를 계속 비워낼 떄 까지 while 문이 돌기 때문에 문제가 과부하 문제가 생길 수 있다. 이럴 때는 Receive 할 때 체크하여 Disconnect 해주어야 한다.

 

 둘째. 특정 상황에서는 패킷을 모아보내는 상황이다. 이전 포스팅에서 메이플스토리에 천 명의 유저가 몰려 있고, 움직이거나 스킬을 쓴다고 가정했는데, 만약 이 때 각 사용자들의 패킷을 모두 모아서 보내어 한 개씩 처리하게 되면 모든 작업을 하나, 하나 일련의 과정으로 처리하기 때문에 비 합리적이므로 처리가 된다. 따라서 이 때에는 천 명의 유저가 사용한 움직임, 스킬과 같은 정보를 어마어마하게 큰 1개의 버퍼를 만들어 패킷을 처리하는 것이 더 바람직하다.

 

 

 

반응형