2014년 2월 5일 수요일

[C# IO]C#에서의 Non-Blocking 통신. Server/Client 시도.

조사를 하던 중 C#에서 Non-Blocking을 하기 위한 방법이 단 한 가지로 한정되지 않는다는 사실을 알았다. 그러나 여기서는 대표적으로 쓰이는 방법 한 가지를 언급하고자 한다.

이곳에 조사한 지식을 공유하고자 지식을 쓰고 있음에 작성한 내용에 오타나 그릇된 개념이 들어가 있다면 정정사항을 언급하여 잘못된 지식이 전달되지 않도록 바로잡아 주셨으면 한다.

NIO를 기반으로 하여 작성하여 본 클라이언트 측 코드는 다음과 같다.



using System;                             //가장 기본적인 기능을 위해 추가하는 이름공간, 그 중요도는 java.lang 혹은 stdio.h와 필적
using System.Collections.Generic;//컬렉션을 사용하기 위해 추가하는 이름공간
using System.Linq;                      //SQL 비슷한 구문을 통해 데이터를 쿼리할 때 쓰인다고 파악
using System.Text;                      //문자열 처리를 위한 기능 제공, Format 설정 등
using System.Threading.Tasks;    //Task 기능을 사용하기 위해 추가하는 이름공간

namespace NonBlockingTest01Client //사용자 정의 이름공간
{
    using System.Net;                   // 네트워크 관련한 클래스들을 이용하기 위한 대표적 이름공간
    using System.Net.Sockets;       // 네트워크 관련한 소켓 클래스들을 이용하기 위한 대표적 이름공간

    

/** 본 클래스에서는 소켓을 규칙에 따라 생성하여(별도자료 1 참고) IP주소를 문자열부터 파싱하여 단말기 정보(IEndPoint)를 생성하고(서버) 연결시도하는 내용을 담고 있다. 소켓을 비차단 모드로 설정하고 내부 반복문에서는 이를 테스트하기 위한 수신 및 예외처리 흐름을 넣어 놓았다. 
**/
    class NonBlockingSocketClient   //사용자가 정의한 클래스명
    {
       //정적 메서드, 메인에서 호출하며 인자1:IP주소, 인자2:포트번호
        public static void Run(string addr, int port)  
        {
            Socket client = null;    //소켓 클래스, 생성과 동시에 값 없음
            try
            {     
                client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

                IPAddress ipAddr = IPAddress.Parse(addr);
                IPEndPoint serverEndPoint = new IPEndPoint(ipAddr, port);

                client.Connect(serverEndPoint);
                Console.WriteLine("Connected to server...");
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                Environment.Exit(-1);
            }       
            //별도자료 3 참고
            client.Blocking = false// 비차단, 비봉쇄로 Socket옵션을 설정한다.
          
            string inputMsg = Console.ReadLine();  //표준 입력으로부터 문자열 하나를 입력받는다.
             
            byte[] bytesBuff = Encoding.UTF8.GetBytes(inputMsg);  //유니코드 기반으로 문자열을 바이트로 변환한다.

            int bytesSent = 0;
            while (bytesSent < bytesBuff.Length)
            {
                try
                {
                    bytesSent += client.Send(bytesBuff, 0, bytesBuff.Length, SocketFlags.None);
                }
                catch (SocketException e)
                {
                    Console.Error.WriteLine(e.Message);

                    if (e.ErrorCode == 10035)
                    {
                        //별도자료 2 참고
                    }
                    else
                    {
                        Console.WriteLine("{0} : {1}", e.ErrorCode, e.Message);
                        client.Close();
                        Environment.Exit(-1);
                    }
                }
            }
            int totalBytesRecvd = 0;
            while (totalBytesRecvd < bytesBuff.Length)
            {
                try
                {
                    int byteBytesRecvd = client.Receive(bytesBuff, 0, bytesBuff.Length , SocketFlags.None);

//4
Console.WriteLine("Immediately return?");
                    if (byteBytesRecvd == 0)
                    {
                        Console.WriteLine("Connection closed");
                        break;
                    }
                    string msg = Encoding.UTF8.GetString(bytesBuff, 0, byteBytesRecvd);
                    Console.WriteLine("We've got [{0}]", msg);

                    totalBytesRecvd += byteBytesRecvd;
                }
                catch (SocketException e)
                {
                    if (e.ErrorCode == 10035)
                    {
                        //5
                        Console.WriteLine("10035 error ya?");
                       
                    }
                    else
                    {
                        Console.WriteLine("{0} : {1}", e.ErrorCode, e.Message);
                        client.Close();
                        Environment.Exit(-1);
                    }
                }
            }
        }
        static void Main(string[] args)
        {
            
            Run("127.0.0.1", 10001); //localhost를 주소로 넣고 포트를 만일로 지정한다.
        }
    }
}

================================================================================================
별도자료
1. INET : IPv4 인터넷 프로토콜 체계
   INET6 : IPv6 인터넷 프로토콜 체계
   LOCAL : 로컬 통신을 위한 UNIX 프로토콜 체계
   PACKET : Low Level 소켓을 위한 프로토콜 체계
   IPX : IPX 노벨 프로토콜 체계

1.소켓의 생성, Blocking IO에서 다루었으므로 간략히 설명
  첫번째 인자, 소켓이 사용할 프로토콜 체계, 하단 별도자료1
  두번째 인자, 소켓의 타입, Stream/Datagram
  세번째 인자, 두 컴퓨터간 통신에 사용되는 프로토콜 정보 전달

2. WSAWORLDBLOCK: 리소스가 일시적으로 사용이 불가능하다 . 이 에러가 Non-Blocking에서 자주 발생하는데 원인은 당시 소켓이 전송을 시도할 때 버퍼가 꽉 차있다거나 하는 등의 이유로 전송을 실패했을 때 발생한다. 따라서 이 에러가 발생하면 후에 전송을 다시 시도해야 한다.

3. 이 Blocking 프로퍼티는 비봉쇄 메서드에서는 무시된다.

4. 흐름상 Non-Blocking이라 즉시 리턴하여 이 아래의 코드가 실행될 것이라 예상해 볼 수 있다.
  그러나 Receive 메서드는 Non-Blocking 모드에서 읽을 데이터가 없을 시 에러를 리턴한다.
  위에서 client.Blocking = false;하는 부분이 있다. 여기서 Non-Blocking 모드가 설정되었다.

5.코드를 실행하면 이 에러문구가 분출하듯이 출력됨을 알 수 있다.
   즉 흐름 분기가 바로 이루어지므로 이곳에서 다른 기타 필요한 처리를 시도할 수 있다.
  You can use the Available property to determine if data is available for reading. When Available is non-zero, retry the receive operation.  굳이 해석하자면, 읽을 데이터가 있는지 학인하려면 Available 프로퍼티를 사용해라.
================================================================================================





이제 서버다(키보드를 바꾸던지 이거 원). 중복되는 설명은 최대한 배제하겠다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace NonBlockingTest01Server
{
    using System.Net;
    using System.Net.Sockets;


   class Client //새로이 클래스를 정의하는 부분이다. 이 부분이 왜 필요한지에 대해서는 추후 설명하겠다.
    {
        private const int BUFSIZE = 100;   // 버퍼 사이즈를 100으로 지정한다.
        private byte [] recvbuf;               // 수신버퍼를 생성해 놓는다.
        private Socket clientSock;           // 클라이언트와 연결할 소켓을 담을 변수를 선언한다.

        public Client (Socket clientSock) //생성자이다. 기본 초기화 코드가 들어가 있다.
        {
            this.clientSock = clientSock;
            recvbuf = new byte[ BUFSIZE];
        }

        public byte [] RecvBuffer  //이하 7줄은 프로퍼티 설정 부분이다.
        {
            get { return recvbuf; }
        }

        public Socket Socket
        {
            get { return clientSock; }
        }
    }



   /** 본 서버 코드에서는 소켓을 동일하게 생성하되 콘솔에 스레드 번호와 스레드 상태를 출력하는 구문을 중간 중간에 삽입하였다. 콜백 시스템에 데이터 교환을 위해 사용되는 인터페이스인 IAsynResult를 주의 깊게 분석할 필요가 있다. 초기 무한루프에서 수신을 시작으로 하여 콜백 델리게이트의 등록 및 호출에 대한 처리를 반복하여 메아리 서버를 구현하였다.
   **/
    class AsyncEchoServer  //
    {
        private const int BACKLOG = 128;
        void Run( int port )
        {
            Socket servSock = new Socket(AddressFamily .InterNetwork, SocketType .Stream, ProtocolType.Tcp );
            servSock.Bind (new IPEndPoint(IPAddress .Any, port));
            servSock.Listen (BACKLOG);
            for (; ; )
            {
                Console.WriteLine ("call BeginAccept - {0},{1}"Thread.CurrentThread .GetHashCode(), Thread.CurrentThread .ThreadState);
                //별도자료 2-1 참조
                IAsyncResult result = servSock. BeginAccept(new AsyncCallback (AcceptCallback), servSock);
                Console.WriteLine("Server beginAccept is called...");
                
                Thread.Sleep (1000); //일부러 1초간 잠을 잤다.
                Console.WriteLine("after 1 second...");

                //2-2 
                result.AsyncWaitHandle .WaitOne();

                /*WaitOne의 호출로 클라이언트가 접속하자마자 하단의 출력문이 출력되었다. */
                Console.WriteLine("AsyncWaitHandle.WaitOne() returned.."); 
            }
        }

        //2-3
        public void AcceptCallback(IAsyncResult asyncResult)
        {
  //별도 자료 2-4 참조
            Socket servSock = (Socket) asyncResult.AsyncState ;
            Socket clientSock = null;
            try
            {
               //2-5
                clientSock = servSock .EndAccept( asyncResult);


                Console.WriteLine ("thread {0},{1} - AcceptCallback - endpooint:{2}",
                    Thread.CurrentThread .GetHashCode(),
                    Thread.CurrentThread .ThreadState,
                    clientSock.RemoteEndPoint );

                /*사용자 정의 객체인 Client를 선언하는 부분이다. 생성자를 통해 내부에 소켓정보를 담는다.*/
                Client client = new Client(clientSock );

                //2-6 
                clientSock.BeginReceive (client. RecvBuffer, 0, client.RecvBuffer .Length, SocketFlags.None ,
                    new AsyncCallback (ReceiveCallback), client);

                //2-7
                Console.WriteLine("Server Write returned immedately?");
            }
            catch (SocketException e)
            {
                Console.WriteLine (e. ErrorCode + ":" + e. Message);
                clientSock.Close ();
            }
        }
        //2-8
        public void ReceiveCallback(IAsyncResult result)
        {
            //2-9
            Client client = (Client) result.AsyncState ;
            try
            {
                //2-10
                int recvSize = client. Socket.EndReceive (result);
                if (recvSize > 0)
                {
                    Console.WriteLine ("thread {0},{1} - ReceiveCallback - recvsize:{2}",
                     Thread.CurrentThread .GetHashCode(),
                     Thread.CurrentThread .ThreadState,
                     recvSize);

//2-11
                    Thread.Sleep(5000);
                    //2-12
                    client.Socket .BeginSend( client.RecvBuffer , 0, recvSize, SocketFlags .None,
                        new AsyncCallback (SendCallback), client);
                }
                else
                {
                    client.Socket .Close();
                }
            }
            catch (SocketException e)
            {
                Console.WriteLine (e. ErrorCode + ":" + e. Message);
                client.Socket .Close();
            }
        }

       
        public void SendCallback(IAsyncResult result)  //전송 콜백
        {
            Client client = (Client) result.AsyncState ;
            try
            {
                //2-13
                int bytesSent = client. Socket.EndSend (result);

                Console.WriteLine ("thread {0},{1} - SendCallback - recvsize:{2}",
                  Thread.CurrentThread .GetHashCode(),
                  Thread.CurrentThread .ThreadState,
                  bytesSent);

                //2-14
                client.Socket .BeginReceive( client.RecvBuffer , 0, client.RecvBuffer .Length, SocketFlags.None , newAsyncCallback(ReceiveCallback ), client);
            }
            catch (SocketException e)
            {
                Console.WriteLine (e. ErrorCode + ":" + e. Message);
                client.Socket .Close();
            }
        }

        public static void Main()
        {
            //서버 시작한다.
            new AsyncEchoServer().Run(10001);
        }
    }
}


=============================================================================================
별도 자료 2
 Client라는 새로운 사용자 정의 클래스를 선언한 이유.  콜백의 특성상 유효범위가 전혀 다른 새 메서드가 호출되게 된다. 그렇기에 일반적인 Blocking의 흐름에서라면 가능했던 여러 주변 변수들의 값 참조를 통한 코드 알고리즘 설계가 불가능해지게 된다(접근이 안 되므로). 이에 논리 전개에 필요한 필수적인 변수들과 기타 데이터들을 메서드 내로 가져가 참조하기 위해 이를 선언했다고 설명하도록 하겠다. 단, 중요한 것은 반드시 BeginXXX 계열의 메서드를 호출한 소켓의 정보를 반드시 내부에 가져야 한다는 것이다.

별도 자료 2-1
 IAsynResult 클래스에 주의를 들여 주길 바란다.
 BeginAccept를 하고 있음을 인식하라. 인자로 콜백 델리게이트를 받으며, 2번째 인자로 소켓 정보를 담는 사용자 정의 객체를 전달하게 된다.
별도 자료 2-2
Non-Blocking 기반으로 구현한 부분임을 알 것이다. 보통이라면 Accept에서 차단, 봉쇄가 되었겠지만 여기선 그렇지 않다. 따라서 본 스레드를 기다리는 역할을 한다. Java에서 thread.join()이라 하면 이해가 될 것이다.
스레드 이야기가 나와서 당혹스러웠을 수 있으나 공식 문서에서 이를 설명하였다. 즉, 별도 스레드를 돌린다.

별도 자료 2-3
사용자 정의 콜백 델리게이트에 맞는 프로토타입으로 선언되었다. 인자에는 사용자가 콜백을 등록할 때 호출할 메서드(이 경우 BeginAccept)가 리턴한 값이 들어오게 된다.

별도 자료 2-4
 특히 인자의 AsyncState에는 콜백을 등록할 때 쓰였던 마지막 인자가 Object 타입으로 들어 있다. 이를 받아오는 이유는 차후에 반드시 설명한다.

별도 자료 2-5
 별도자료 2-4에 대한 이유 해명 부분이다. 콜백에 사용자가 소켓 정보를 넣어서 받아온 이유는 다음과 같다.
비동기 처리에서 작업의 종료는 데이터가 모두 수신되거나 예외가 발생하거나 EndAccept 메서드가 호출되었을 때로 판단된다. 이에 공식 문서에서는 반드시 BeginXXX메서드를 호출하면 콜백 메서드 호출 이후 반드시 EndXXX메서드를 이와 같은 이유로 호출하도록 권고하고 있다. 그 인자에는 콜백 메서드의 파라미터값을 그대로 넘긴다. 이로써 작업이 완료되었음을 보장한다. 연결된 소켓정보를 이곳에서 리턴받을 수 있다.

별도 자료 2-6
  소켓의 BeginReceive를 호출함을 인지하라. 또한 역시 콜백메서드를 호출함을 확인하되, 맨 마지막 인자로 소켓정보를 담는 사용자 정의 객체가 들어감을 거듭 확인하라.

별도 자료 2-7
  Non-Blocking 결과 위 BeginReceive 메서드는 즉시 리턴하며 이곳으로 바로 코드의 흐름이 이어진다. 그 결과 클라이언트측에서 데이터를 보내지 않았어도(수신할 데이터가 없다) 다음 코드의 흐름을 진행할 수 있다.

별도 자료 2-8
  수신 콜백 델리게이트의 원형을 따르는 메서드이다. 인자는 역시 콜백 메서드를 등록할 때 사용한 BeginReceive의 리턴값이 들어가게 된다. 여기선 그 객체가 Client였다. 그 변환 타입이 Object임을 잊지 않길 바란다.

별도 자료 2-9
  사용자 정의 객체를 받아오는 부분이 있다. 이 역시 EndReceive를 호출하기 위함이다.

별도 자료 2-10
  EndReceive를 호출함으로써 수신작업이 완료되었음을 보장한다. 이에 수신한 데이터 크기를 받아올 수 있다.

별도 자료 2-11
  메아리(Echo)를 치기 전에 5초간 대기하였다. 이 목적은 클라이언트의 비차단 흐름을 확인하고자 하였기 때문이다. 즉 서버에서 데이터를 보내지 않고 있을 때 클라이언트측에서 Receive하는 부분의서의 Blocking 처리가 어떻게 되는지  확인하고자 하였다.

별도 자료 2-12
  전송을 완료함으로 보낸 바이트 수를 확인할 수 있다.

별도 자료 2-13
  BeginSend이다. 이외의 다른 인자들의 목적은 타 BeginXXX 계열의 것과 비슷하다.
=============================================================================================

별도 자료 2-14
  여기서 수신 콜백을 재등록 하는데 이는 서버의 목적이 메아리치기이기 때문이다.

위 두 프로그램의 수행 결과는 다음과 같다.

구글 이넘들이 이미지를 지웠나봐요. 아래 이미지로 대체합니다.


[그림 1] 서버

  서버에서 확인할 부분은 아래에서 5번줄 'Server Write returned immediately?'([그림 1]에오타가 있다)이다. 이 문자열은 BeginReceive 메서드 직후에 호출된 것으로 해당 메서드가 Blocking 되지 않았음을 증명하는 부분이다.

구글 이넘들이 이미지를 지웠나봐요. 아래 이미지로 대체합니다.

[그림 2] 클라이언트 시작

  시작한 클라이언트에서는 서버에 연결되었다라고 출력해 주었다. 이에 전송할 문자열을 입력하였더니 다음과 같은 화면을 접할 수 있었다.

구글 문제로 이미지가 txt 로 변환되었네요...

[그림 3] 클라이언트 화면

  여기서 확인할 수 있는 부분은 클라이언트의 차단, 봉쇄 기반의 메서드인 Receive에서 소켓의 Blocking 프로퍼티의 false 설정으로 읽을 데이터가 없어 예외를 던졌다는 것이다. 서버에서는 데이터를 보내기 전 5초를 대기하며 이 기간동안 클라이언트는 Receive를 호출하되 블럭킹되지 않았다는 것이 증명되는 부분이다.

 이렇게 정리를 하여 글을 작성해 보았다.

 이상으로 정리를 끝낸다.

댓글 없음:

댓글 쓰기