공부/인프런 - Rookiss

Part 4-3-6. 네트워크 프로그래밍 : Session #2 (Send 분리)

셩잇님 2023. 9. 22. 17:25
반응형

 

 

네트워크 프로그래밍

 

 지난 시간에서 우리는 새로운 클래스인 Session을 만들어 소켓 프로그래밍의 Receive 부분을 분리하여 작업을 진행했다. 오늘은 이어서 Send 부분을 분리하여 비동기로 처리할 것이다. 

 

 


 

 

Send 비동기 처리 구현

 Send는 Receive 와는 다르게 언제, 어디서, 얼마의 버퍼(Buffer)의 크기를 사용하여 보낼지 모르기 때문에 기존 구조와는 다르게 비동기 처리 구현이 필요하다. Send의 동작 구조는 기존의 Receive의 동작 구조와 유사하다. 하지만 Start 메서드에서 SocketAsyncEventArgs 클래스를 이용해 소켓을 생성했던 기존의 Receive와는 달리 Send에서는 이벤트만을 연결해 줄 것이다.

 

 왜 이렇게 처리해야 할까? 기존의 Receive 동작 구조는 메시지를 받고 난 이후 SocketAsyncEventArgs 클래스를 통해 만들어진 recvArgs를 널로 변환하여 재사용할 수 있었지만, Send 에서는 얼마의 버퍼 크기를 사용할 지 모르기 때문에 해당 변수를 생성하고 재 사용하는 방식이 불가능하기 때문이다. 따라서 _sendArgs는 세션 클래스 내 전역변수로 처리하여 진행한다.

 

        public void Start(Socket socket)
        {
            _socket = socket;

            SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
            recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
            // recvArgs.UserToken = this;
            recvArgs.SetBuffer(new byte[1024], 0, 1024);

            // 이벤트만 붙여 처리를 진행한다.
            _sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);

            RegisterRecv(recvArgs);
        }
    class Session
    {
        Socket _socket;
        int _disconnected = 0;

        // 전역 변수로 처리하여 사용하는 _sendArgs 소켓
        SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
        
        ...
    }

 

 


 

 

큐(Queue)를 이용한 Send 처리

 왜 Send를 큐(Queue)를 이용해 처리해야할까? 이는 간단하다. Send를 비동기 함수로 변경해서 처리를 진행했을 때의 생기는 문제점을 생각해보자. 예를 들어 메이플스토리 헤네시스에 천명의 유저들이 모여있을 경우 A 유저가 움직였다는 정보를 나머지 999명한테 루프를 이용해 보내야 한다. 그러나 A 유저만 움직이는 것이 아니라 B, C, D, E, F 유저 모두가 움직이고.. 움직임 뿐만 아니라 스킬을 이용해 난리를 치고 있을 것이다.

 

 이렇게 될 경우 Send를 호출하는 횟수가 굉장히 많아지고 결국 부하가 걸릴 것이다. 따라서 Send를 무차별적으로 매 프레임 처리하는 것이 아닌 큐(Queue)를 이용해 Send를 한번에 모아서 처리하는 형태로 사용하는 것이다.

 

    class Session
    {
        Socket _socket;
        int _disconnected = 0;

        // 새롭게 전역변수로 추가되는 변수
        object _lock = new object();
        Queue<byte[]> _sendQueue = new Queue<byte[]>();
        bool _pending = false;
        
        SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
    }

 

 먼저 세션 클래스에서 큐를 사용하기 위해 전역 변수로 큐를 생성해준다. 또한 멀티쓰레드 환경에서 동작하는 것이 목표이기 때문에 기존에 학습했던 lock 이용할 것이므로 lock을 만들어준다. 마지막으로 모든 처리가 한번에 실행되지 않게하기 위해 pending을 bool 변수를 추가해준다.

 

전송(Send)

 

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

 

 이후 Send 메서드에서 락을 이용해 스크립트를 감싸고, 큐에 정보를 넣어준다. RegisterSend 메서드에 한 번에 하나씩 처리하며 동작시키기 위해 락을 이용해준다. 마지막으로 _pending을 이용하여 이미 Send가 실행 중일 경우 동작 실행을 대기하며 큐에 들어간다.

 

등록(Register)

 

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

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

 

 이 후 Send에서 보내준 일감을 Dequeue()를 이용해 뽑아온다. 뽑아온 정보를 SetBuffer를 통해 크기 등의 정보를 저장하여 소켓에 담아준 뒤 다음 작업으로 처리를 진행한다. pending이 한번 더 동작하여 검사하는데 이 부분은 Receive와 동일하게 작동하므로 설명은 생략한다.

 

완료 이후(OnSendCompleted)

 

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

 

 완료 이후 동작하는 메서드 또한 Receive와 동일하게 콜백으로 들어올 수 있다. 당연하겠지만 멀티 쓰레드 환경에서 독립적으로 동작하기 위해 lock을 이용해 구역을 나뉘어준다. 이후 큐의 카운트를 이용해 개수를 체크하여 남아 있을 경우 RegisterSend() 메서드를 실행하여 Buffer 설정 및 업무 처리를 다시 진행시킨다. 남아있지 않을 경우 _pending의 값을 false로 변경하여 대기 상태로 전환시킨다.

 

 

 

반응형