C#

Session_1

monstro 2024. 12. 24. 16:29
728x90
반응형

이번 포스트에서는 기존의 Listener 예제에 Session을 추가하는 방식으로 멀티 네트워크를 구현하겠습니다.

그리고 기존의 Listener의 역할을 조금 더 확대하여

Listener에서 Accept를 수행하도록 하겠습니다.

이런 방식으로 수정하는 경우, 클라이언트가 아닌 서버에서 안전하게 멀티 네트워크를 개시할 수 있습니다.

 

1) Session

일단 현재 만들어진 세션은 서버에서만 동작할 예정입니다.

세션은 크게 2가지의 역할을 수행합니다.

 

  • 서버에서 클라이언트로 데이터를 전송 = Send
  • 클라이언트에서 전송된 데이터를 서버에서 받음 = Recv

 

따라서 위의 2가지 역할을 수행하기 위한 코드를 만들어보겠습니다.

abstract class Session
{
    Socket _socket;
    // 만약, 클라이언트 세션이 Disconnect 되었다면 Flag를 통해 이를 판단
    int _disconnnected = 0;

    // 락을 위한 오브젝트, 락을 걸어 자원에 대한 원자성을 보장
    object _lock = new object();

    // Send할 데이터를 저장하는 큐, Send가 들어올 때마다 데이터를 보내지 않고 대신 Head를 우선 보냄
    Queue<byte[]> _sendQueue = new Queue<byte[]>();
    // Send 이벤트에서 사용할 BufferList를 설정하기 위한 리스트 = 대기중인 목록
    List<ArraySegment<byte>> _pendinglist = new List<ArraySegment<byte>>();
    // Send 이벤트마다 새로 생성되는 문제를 막고자 다음과 같이 프로퍼티로 설정하여 캐싱해놓음
    SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();

    // Recv 이벤트마다 새로 생성되는 문제를 막고자 다음과 같이 프로퍼티로 설정하여 캐싱해놓음
    SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();

    // Connect 이벤트 발생 시 호출
    public abstract void OnConnected(EndPoint endPoint);

    // Recv 이벤트 발생 시 호출
    public abstract void OnRecv(ArraySegment<byte> buffer);

    // Send 이벤트 발생 시 호출
    public abstract void OnSend(int numOfBytes);

    // Disconnect 이벤트 발생 시 호출
    public abstract void OnDisconnected(EndPoint endPoint); 

    // ClietnSession 네트워크 통신 준비
    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)
    {
        lock (_lock)
        {
            _sendQueue.Enqueue(sendBuff);
            if (_pendinglist.Count == 0)
                RegisterSend();
        }
    }

    // Client Session 연결 종료
    public void Disconnect()
    {
        // Flag 변환을 멀티 스레드를 고려하여 Interlocked를 사용하여 수행함
        if (Interlocked.Exchange(ref _disconnnected, 1) == 1)
            return;

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

    // Client Session이 클라로부터 데이터를 받음
    void RegisterRecv()
    {
        bool pending = _socket.ReceiveAsync(_recvArgs);
        
        // Recieve 이후에 서버가 데이터를 바로 받았다면, OnRecvCompleted를 즉시 호출
        if (pending == false)
            OnRecvCompleted(null, _recvArgs);
    }

    // RegisterRecv 함수로부터 호출되어, 클라이언트로부터 받은 데이터를 처리
    void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
    {
        // 클라이언트로부터 받은 데이터의 양이 0보다 크고 동시에 Socket 연결이 성공했다면
        if (_recvArgs.BytesTransferred > 0 && _recvArgs.SocketError == SocketError.Success)
        {
            try
            {
                OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));
                RegisterRecv();
            }
            catch (Exception e)
            {
                Console.WriteLine($"From Client Recv Error : {e.ToString()}");
            }
        }
        // 그렇지 않다면 연결 해제
        else
        { 
            Disconnect();
        }
    }

    // Client Socket이 서버로부터 데이터를 전송
    void RegisterSend()
    {
        // BufferList를 설정
        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);
        // Send 이후에 서버가 데이터를 바로 보냈다면, OnSendCompleted를 즉시 호출
        if (pending == false)
            OnSendCompleted(null, _sendArgs);
    }

    // RegisterSend 함수로부터 호출되어, 서버로부터 보낼 데이터를 처리
    void OnSendCompleted(object handler, SocketAsyncEventArgs args)
    {
        lock (_lock)
        {
            // 서버로부터 보낸 데이터의 양이 0보다 크고 동시에 Socket 연결이 성공했다면
            if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
            {
                try
                {
                    _sendArgs.BufferList = null;
                    _pendinglist.Clear();

                    OnSend(_sendArgs.BytesTransferred);

                    if (_sendQueue.Count > 0)
                        RegisterSend();
                }
                catch (Exception e)
                {
                    Console.WriteLine($"From Server Send Error : {e.ToString()}");
                }
            }
            // 그렇지 않다면 연결 해제
            else
            {
                Disconnect();
            }
        }
    }
}

 

Session 클래스의 경우, abstract 클래스 즉 추상 클래스로 선언되어 반드시 상속되어야 합니다.

상속하는 경우 이벤트 발생 시에 수행해야 하는 함수들오버라이드해야 합니다.

오버라이드하는 함수들은 다음의 4개입니다.

  • OnConnected : 연결시에 호출
  • OnDisconnected : 연결이 끊길시에 호출
  • OnRecv : 데이터를 받는 경우 호출
  • OnSend : 데이터를 보내는 경우 호출

또한 이번에는 SetBuffer가 아닌,

BufferList를 사용하여 서버가 Send하고자 하는 데이터를 배열 형태로 저장합니다.

이 경우에, Send할 때리스트에 저장된 모든 목록을 한번에 처리할 수 있습니다.

단 SetBuffer나 BufferList 중에서 반드시 하나만을 선택하여 사용해야 합니다.

 

Session의 진행 절차를 간략하게 설명하자면 다음과 같이 설명할 수 있습니다.

1 - 1) 클라이언트에서 보내진 데이터를 Recv 하는 경우

  1. 공통적으로 Start 함수에서 Recv와 Send 시에 사용하기 위한 비동기 소켓인자를 설정합니다.
  2. 이때 Recv의 비동기 소켓인자에는 OnRecvCompleted 함수를 연결합니다.
  3. 이어서 Recv한 데이터를 저장하기 위한 버퍼설정합니다.
  4. Start 함수에서 RegisterRecv를 호출하여 Recv하기 위한 준비를 수행합니다.
  5. RegisterRecv를 수행한 직후 바로, 데이터를 받은 경우 OnRecvCompleted를 호출합니다.
  6. 그렇지 않은 경우, 다른 작업을 수행합니다.
  7. OnRecvCompleted에서는 Recv된 데이터를 확인하여 OnRecv 함수를 수행합니다.
  8. 이후RegisterRecv를 호출하여 다시 Recv하기 위한 준비를 수행합니다.

 

 1 - 2) 서버에서 클라이언트로 데이터를 Send 하는 경우

  1. Start 함수에서 Send 시에 사용하기 위한 비동기 소켓인자를 설정합니다.
  2. 이때 Send의 비동기 소켓인자에는 OnSendCompleted 함수를 연결합니다.
  3. Send 함수에서 Lock을 걸고 _sendQueue에 send할 데이터를 넣습니다.
  4. 만약 Send를 위한 예약목록인 _pendingList가 비어있다면 즉시 RegisterSend를 수행합니다.
  5. 비어있지 않다면 일단 SendQueue에만 넣어놓습니다.
  6. RegisterSend 함수에서는 _sendQuque에 있던 데이터를 하나하나 빼와 _pendingList에 넣습니다.
  7. 채워진 _pendingList_sendArgs의 BufferList로 설정합니다.
  8. 7의 절차로 인해  BufferListSend하기 위한 모든 예약 목록을 갖고 있게 됩니다.
  9. SendAsync를 통해 비동기적으로 Send를 수행합니다.
  10. 이때 바로 Send가 가능하다면 OnSendComplete를 수행하지만, 가능하지 않다면 다른 작업을 수행합니다.
  11. OnSendComplete 함수에서는 예약 목록에 있던 데이터를 Send합니다.
  12.  그리고 _sendQueue가 남아있다면 다시 RegisterSend 함수를 호출Send하기 위한 준비를 합니다.

 

728x90
반응형

'C#' 카테고리의 다른 글

Connect를 위한 Connector 생성  (0) 2024.12.25
Session_2  (0) 2024.12.24
Listener  (0) 2024.12.18
소켓 프로그래밍 기초  (0) 2024.12.16
네트워크 기초 + 통신 모델  (0) 2024.12.11