동기화 코드의 문제점


데이터를 보내는 작업과 받는 작업이 블록되는 문제 상황을 강제로 발생시켜보자.

네트워크 프로그램은 많은 부분에서 비동기 코드가 필요하다.

 

Read( ) 함수의 동작은 C언어의 Scanf( ) 동작과 유사하다.

Read( ) 함수는 stream에 전달받은 데이터가 있다면, 데이터를 읽어들인다.

하지만 읽을 데이터가 없다면, 데이터가 들어올 때까지 대기하는데, 이때 실행 흐름은 블록된다.

 

만약 이 상태에서 데이터를 전송시키고 싶다면, Send( ) 가 실행 제어권을 가지지 못하기에 전송 작업 실행은 불가능하다.

TCPClient_Protocol.zip
0.18MB
TCPServer.zip
0.18MB

위의 예제 프로젝트의 소스코드는, 이전 포스트와 동일하다.

TCPServer, TCPClient_Protocol 을 실행시면, 한 번의 전송 이후 각자 Read( ) 에서 서로 멈춘상태로 영원히 대기한다.  

 

 

TCPListener (Server)

  • While 문의 Stream.Read( ) 는 무한히 반복된다.
    하지만, Read( ) 함수는 스트림에서 읽어들일 데이터가 없기에 실행 흐름이 블록된다.
  • 클라이언트에서 소켓 연결을 종료하지 않아 항상 nbytes 는 0보다 큰 상태다.
  • 네트워크 통신 측면에서, 소켓이 끊어지지 않았다면,  Stream.Read( ) 는 대기상태에 있는 것이 맞다.
    즉, 비동기로 다른 실행 흐름에 영향을 주지 않도록 동작해야 한다.
    // NetworkStream 에서 버퍼로 Read된 크기
    int nbytes;
    //TCP Connection을 종료되면 Read() 메서드가 0 을 리턴한다.
    while ((nbytes = stream.Read(receiverBuff, 0, receiverBuff.Length)) > 0)
    {
        mem.Write(receiverBuff, 0, nbytes);
    }

    // 요소를 새 바이트 배열에 복사한다.
    byte[] outbytes = mem.ToArray();

 

 

TCPClient_Protocol

  • 클라이언트도 서버와 동일한 상황이다.
    While 문의 Stream.Read( ) 는 무한히 반복된다. 
    하지만, Read( ) 함수는 스트림에서 읽어들일 데이터가 없기에 실행 흐름이 블록된다.
  • 서버에서 소켓 연결을 종료하지 않아 항상 nbytes 는 0보다 큰 상태다.
  • 네트워크 통신 측면에서, 소켓이 끊어지지 않았다면,  Stream.Read( ) 는 대기상태에 있는 것이 맞다.
    즉, 비동기로 다른 실행 흐름에 영향을 주지 않도록 동작해야 한다.
    // NetworkStream 에서 버퍼로 Read된 크기
    int nbytes;
    //TCP Connection을 종료되면 Read() 메서드가 0 을 리턴한다.
    while ((nbytes = stream.Read(receiverBuff, 0, receiverBuff.Length)) > 0)
    {
        mem.Write(receiverBuff, 0, nbytes);
    }

    // 요소를 새 바이트 배열에 복사한다.
    byte[] outbytes = mem.ToArray();

 

 

비동기 네트워크 프로그래밍


APM (Asynchronous Programming Model) 

고전적인 APM 방식은 BeginAcceptTcpClient() / EndAcceptTcpClient() 와 같이 Begin* / End* 2개의 메서드를 쌍으로 사용하는 방식으로 Backward Compatibility를 위해 사용된다.

 

 

TAP (Task-based Asynchronous Pattern)

 AcceptTcpClientAsync() 와 같이 끝에 Async 가 붙는 메서드를 C# await 와 함께 사용하는 방식으로 비동기 처리를 단순화한 현대적 방식이다.

 

 

비동기1

  • 네트워크 메인 스레드
  • 프로그램에서 네트워크 기능은 별도의 실행 흐름(스레드)를 가져야 한다.

 

 

비동기2

  • 서버는 클라이언트의 연결 요청을 대기한다.
  • 클라이언트에서 접속 요청이 들어올때마다 연결된 소켓을 생성해야한다.
    그렇지 않으면 여러 클라이언트들이 접속할때마다, 순차적으로 하나씩 처리해야 한다.
  • 클라이언트의 연결 호출 대기와 연결 소켓 생성 흐름은 서로 영향을 주지 않도록 비동기로 구현되어야 한다.
  • TcpListener 객체의 AcceptTcpClientAsync() 메서드와 C# await로 사용하여 클라이언트 Connection을 비동기로 받아이고 연결된 소켓을 생성 할 수 있다.

 

 

비동기3

  • 비동기2의 별도의 소켓 통신 흐름 내에서 연결된 소켓의 송신 작업은 비동기로 동작해야 한다.
    그렇지 않으면, 수신 작업 중에 송신이 불가능하다.
  • NetworkStream의 ReadAsync(), WriteAsync() 메서드를 사용한다.
  • 그렇지 않으면, Read( ) 블록 상태에서 다른 실행 제어가 불가능하다.

 

 

비동기4

  • 비동기2의 별도의 소켓 통신 흐름 내에서 연결된 소켓의 수신 작업은 비동기로 동작해야 한다.
    그렇지 않으면,신 작업 중에 송신이 불가능하다
  • NetworkStream의 ReadAsync(), WriteAsync() 메서드를 사용한다.
  • 그렇지 않으면, Read( ) 블록 상태에서 다른 실행 제어가 불가능하다.

 

 

실행 흐름

 

 

비동기 흐름

 

 

예제


동기화 코드의 문제점 에서 사용된 TCPClient_Protocol 을 사용한다.

  • 서버만 비동기 처리한 소스코드를 이용한다.
  • 클라인언트도 동일한 방법으로 비동기 처리하여 구현하면 된다. 

TCPClient_Protocol.zip
0.18MB
TCPServer_Async.zip
0.18MB

Server

using System.Net.Sockets;
using System.Net;
using System;
using System.Threading.Tasks;
using System.Text;

namespace TcpServerAsync
{
    class Program
    {
        // Main 에서 AysncEchoServer 로 구현된 비동기 로직을 실행한다.
        static void Main(string[] args)
        {
            AysncEchoServer().Wait();
        }

        // 비동기 서버 로직을 구현한 함수
        async static Task AysncEchoServer()
        {
            // [1] 서버의 Listen 동작을 바로 실행 할 수 있다.
            TcpListener listener = new TcpListener(IPAddress.Any, 7000);
            listener.Start();

            while (true)
            {
                // 비동기 Accept                
                TcpClient Connected_TCPClient = await listener.AcceptTcpClientAsync().ConfigureAwait(false);

                // 연결된 소켓의 이후 작업은 Task 에서 비동기로 진행되도록 실행 흐름을 분리한다
                Task.Factory.StartNew(AsyncTcpProcess, Connected_TCPClient);
            }
        }

        async static void AsyncTcpProcess(object ConnectedSocket)
        {
            TcpClient tc = (TcpClient)ConnectedSocket;


            // [2] 송수신

            NetworkStream stream = tc.GetStream();

            // 버퍼
            int MAX_SIZE = 2048;
            var buff = new byte[MAX_SIZE];

            // 받는 실행 흐름을 비동기로 동작하게 한다.
            var nbytes = await stream.ReadAsync(buff, 0, buff.Length).ConfigureAwait(false);

            if (nbytes > 0)
            {
                string msg = Encoding.UTF8.GetString(buff, 0, nbytes);
                Console.WriteLine($"{msg} at {DateTime.Now}");

                // 보내는 작없을 비동기로 동작하게 한다. 에코서버라서 받은 데이터를 그대로 보낸다.
                await stream.WriteAsync(buff, 0, nbytes).ConfigureAwait(false);
            }

            // [3] 닫는다.
            // 연결이 끊어지지만, 특정 문자이 왔을때 끊어지도록 프로토콜을 로직을 만들면 된다. 
            stream.Close();
            tc.Close();
        }
    }
}