📄 Async Session

지난 글과 작동은 같지만, 버퍼를 성공적으로 송신하거나, 수신할 때를 다룹니다. 더욱 효율적인 송신 방법과 각 이벤트가 성공적으로 수행되면 Event를 통해 확인할 수 있도록 학습하였습니다.

 

 

📑 Session

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

namespace ServerCore
{
    public abstract class Session
    {
        Socket mSocket;
        int mIsDisconnected = 0; //현재 Disconnected 상태인가?

        object mSendingLock = new object(); //Send Lock
        Queue<byte[]> mSendQueue = new Queue<byte[]>(); //Send 버퍼를 담는 큐
        List<ArraySegment<byte>> mSendPendingList;

        /// <summary>
        /// 비동기 통신 시 특정 행동을 수행하면 호출할 이벤트를 선언
        /// </summary>
        SocketAsyncEventArgs mSendArgs, mReceiveArgs;

        public Session()
        {
            //Send를 성공적으로 수행했다면, OnSendCompleted를 실행하도록 설정
            this.mSendArgs = new SocketAsyncEventArgs();
            mSendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);
            mSendPendingList = new List<ArraySegment<byte>>();

            //Receive를 성공적으로 수행했다면 OnReceiveCompleted를 실행하도록 설정
            this.mReceiveArgs = new SocketAsyncEventArgs();
            mReceiveArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnReceiveCompleted);
            mReceiveArgs.SetBuffer(new byte[1024], 0, 1024); //1024배열크기를 0부터시작하여 1024개의 사이즈로 생성
        }

        /// <summary>
        /// 소켓을 등록시키고, 활성화
        /// </summary>
        /// <param name="socket">사용할 소켓</param>
        public void Start(Socket socket)
        {
            this.mSocket = socket;

            //최초 1회 등록
            RegisterReceive();
        }

        public abstract void OnConnected(EndPoint endPoint);
        public abstract void OnDisconnected(EndPoint endPoint);
        public abstract void OnReceive(ArraySegment<byte> buffer);
        public abstract void OnSend(int numOfBytes);

        /// <summary>
        /// 목적지로 버퍼를 송신한다.
        /// </summary>
        /// <param name="sendBuff"></param>
        public void Send(byte[] sendBuff)
        {
            lock (mSendingLock)
            {
                mSendQueue.Enqueue(sendBuff); //Send를 할 버퍼들을 SendQueue에 담는다.
                if (mSendPendingList.Count == 0) { RegisterSend(); } //현재 대기중인 리스트가 비어있으면, Send를 한다.
            }
        }

        /// <summary>
        /// 목적지와의 연결을 해제한다.
        /// </summary>
        public void Disconnect()
        {
            //인터락을 사용하여 멀티스레드 환경에서 현재 disconnected인지 확인
            //예상치 못한 환경에서 disconnect가 두번이상 호출될경우 리턴하도록 함
            if (Interlocked.Exchange(ref mIsDisconnected, 1) == 1) { return; }

            this.OnDisconnected(mSocket.RemoteEndPoint); //연결 해제 이벤트 호출
            
            mSocket.Shutdown(SocketShutdown.Both); //연결 해제
            mSocket.Close();
        }

#region [은닉]네트워크 통신
        /// <summary>
        /// Send를 비동기로 등록하고 대기하도록 한다.
        /// </summary>
        private void RegisterSend()
        {
            while (mSendQueue.Count > 0) //송신 바이트큐가 빌때까지 송신대기열리스트에 모두 집어넣는다.
            {
                byte[] buffer = mSendQueue.Dequeue(); //큐에 삽입된 순서대로 차례대로 뽑는다(FIFO)
                mSendPendingList.Add(new ArraySegment<byte>(buffer, 0, buffer.Length)); //대기열에 형식에 맞게(ArraySegment) 가공하여 삽입
            }

            mSendArgs.BufferList = mSendPendingList; //BufferList에 replace

            bool isPending = mSocket.SendAsync(mSendArgs); //비동기 Send
            if (isPending == false) //대기중이 아닌경우?
            {
                OnSendCompleted(null, mSendArgs);
            }
        }

        /// <summary>
        /// 송신이 성공적으로 수행되었다면 호출되는 이벤트
        /// </summary>
        private void OnSendCompleted(object sender, SocketAsyncEventArgs args)
        {
            lock (mSendingLock)
            {
                //전송된 바이트가 1이상이고, Success 상태라면? > 정상 통신
                if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
                {
                    try
                    {
                        mSendArgs.BufferList = null; //Send가 정상적으로 이루어졌기에 BufferList를 비워준다.
                        mSendPendingList.Clear(); //Clear을 통하여 대기열 큐를 비워준다.

                        this.OnSend(mSendArgs.BytesTransferred); //성공적으로 보내기를 했으니 이벤트 호출

                        if (mSendQueue.Count > 0) //송신을 완료한 시점에서 SendQueue에 새로운 버퍼가 대기중이라면, 송신하도록 한다.
                        {
                            RegisterSend();
                        }
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine($"OnSendCompleted Failed {e.Message}");
                    }
                }
                else
                {
                    Disconnect();
                }
            }

        }

        /// <summary>
        /// Receive를 위한 비동기 등록 및 대기
        /// </summary>
        private void RegisterReceive()
        {
            bool isPending = mSocket.ReceiveAsync(mReceiveArgs);

            if (isPending == false) //대기중이 아닌경우?
            {
                OnReceiveCompleted(null, mReceiveArgs);
            }
        }

        /// <summary>
        /// 수신을 성공적으로 수행하면 호출되는 이벤트
        /// </summary>
        private void OnReceiveCompleted(object sender, SocketAsyncEventArgs args)
        {
            //전송된 바이트가 1이상이고, Success 상태라면? > 정상 통신
            if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
            {
                try
                {
                    OnReceive(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred)); //성공적으로 수신을 하였다면, 이벤트 호출

                    RegisterReceive();
                }
                catch (Exception e)
                {
                    Console.WriteLine($"OnReceiveCompleted Failed {e.Message}");
                }
            }
            else
            {
                Disconnect();
            }
        }
#endregion
    }
}

 

object mSendingLock = new object(); //Send Lock
Queue<byte[]> mSendQueue = new Queue<byte[]>(); //Send 버퍼를 담는 큐
List<ArraySegment<byte>> mSendPendingList;
  • Send와 관련된 멤버변수들입니다.
  • Lock을 사용하고, Send를 모아서 보내기 위한 큐와 현재 Send대기상태인 리스트가 있습니다.

 

SocketAsyncEventArgs mSendArgs, mReceiveArgs;
  • 비동기 스레드에서 작업을 완료하면 호출하기위한 기능들을 선언하기위한 이벤트입니다.

 

public Session()
  • 기본 생성자를 오버라이드하여 멤버변수들을 설정해줍니다.

 

public void Start(Socket socket)
  • 외부에서 호출하여 소켓정보를 등록하고 활성화합니다.

 

public abstract void OnConnected(EndPoint endPoint);
public abstract void OnDisconnected(EndPoint endPoint);
public abstract void OnReceive(ArraySegment<byte> buffer);
public abstract void OnSend(int numOfBytes);
  • 각각 연결, 연결해제, 패킷수신, 패킷전송시 호출될 이벤트입니다.
  • 오버라이드를 하여 상속하는 여러 디자인에서도 동일한 호출을 하도록 합니다.

 

public void Send(byte[] sendBuff)
  • 버퍼를 소켓정보의 목적지로 송신합니다.

 

private void RegisterSend()
  • 목적지로 송신하기위한 비동기스레드를 활성화합니다.
  • 큐에 있는 모든 바이트를 한번에 보내기위헤 BufferList를 사용합니다.

 

private void OnSendCompleted(object sender, SocketAsyncEventArgs args)
  • Send가 성공적으로 수행되면, 호출되는 이벤트입니다.
  • this.OnSend(mSendArgs.BytesTransferred);를 통하여 오버라이드한 이벤트를 호출합니다.

 

private void RegisterReceive()
  • 수신을 위한 비동기 스레드를 활성화합니다.

 

private void OnReceiveCompleted(object sender, SocketAsyncEventArgs args)
  • Receive가 성공적으로 수행되면, 호출하는 이벤트입니다.
  • OnReceive(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred));를 통하여 오버라이드한 이벤트를 호출합니다.

 

📑 GameSession

public class GameSession : Session
{
    public override void OnConnected(EndPoint endPoint)
    {
        Console.WriteLine($"{endPoint}연결 성공");

        //클라이언트에게 송신
        byte[] sendBuffer = Encoding.UTF8.GetBytes("Hello World From Server");
        Send(sendBuffer);
        Thread.Sleep(1000);
        Disconnect();
    }

    public override void OnDisconnected(EndPoint endPoint)
    {
        Console.WriteLine($"{endPoint} 연결 해제");
    }

    public override void OnReceive(ArraySegment<byte> buffer)
    {
        string receiveData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count); //문자열로 획득
        Console.WriteLine($"클라이언트로부터 받음: {receiveData}");
    }

    public override void OnSend(int numOfBytes)
    {
        Console.WriteLine($"{numOfBytes}바이트 전송 성공");
    }
}
  • Session을 상속하여 오버라이드할 함수들을 재정의하여 의도에 맞는 동작을 수행하도록 합니다.
bonnate