C#

비동기 TCP 서버

비동기 TCP 서버

이전 아티클에서 예를 든 TCP 서버는 동기적 TCP 서버로서 이는 동시에 여러 클라이언트들이 접속했을 때, 순차적으로 하나씩 처리해야 하는 단점이 있다. 따라서, 다중 클라이언트 접속을 핸들링하기 위해서는 TCP 서버를 비동기로 만들 필요가 있다. TCP 서버를 비동기로 만드는 방법은 크게 두가지로 분류할 수 있는데, 고전적인 APM (Asynchronous Programming Model) 방식과 요즘 많이 사용되는 TAP (Task-based Asynchronous Pattern) 방식이 있다. APM 방식은 BeginAcceptTcpClient() / EndAcceptTcpClient() 와 같이 Begin* / End* 2개의 메서드를 쌍으로 사용하는 방식으로 Backward Compatibility를 위해 사용된다. TAP 방식은 AcceptTcpClientAsync() 와 같이 끝에 Async 가 붙는 메서드를 C# await 와 함께 사용하는 방식으로 비동기 처리를 단순화한 현대적 방식이다.

TAP 비동기 TCP 서버 사용예제

TAP (Task-based Asynchronous Pattern) 방식을 사용하여 간단한 비동기 TCP 서버를 작성한 예제를 살펴보자. 먼저 동시에 여러 클라이언트들이 접속할 때 각 클라이언트별로 독립적인 쓰레드를 사용하여 동작하도록 하기 위해 TcpListener 객체의 AcceptTcpClientAsync() 메서드를 호출한다. 이 메서드를 C# await로 사용하여 클라이언트 Connection을 비동기로 받아들이도록 한다. 새로 연결된 클라이언트는 새로운 작업 쓰레드에서 처리를 담당한다. 또한, 데이타 송수신시 NetworkStream의 ReadAsync(), WriteAsync() 메서드를 사용하여 비동기로 네트워크 스트림에 데이타를 읽고 쓰게 할 수 있다.

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

namespace TcpSrvAsync
{
    class Program
    {
        static void Main(string[] args)
        {
            AysncEchoServer().Wait();
        }

        async static Task AysncEchoServer()
        {           
            TcpListener listener = new TcpListener(IPAddress.Any, 7000);
            listener.Start();            
            while (true)
            {
                // 비동기 Accept                
                TcpClient tc = await listener.AcceptTcpClientAsync().ConfigureAwait(false);                
                
                // 새 쓰레드에서 처리
                Task.Factory.StartNew(AsyncTcpProcess, tc);                
            }
        }

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

            int MAX_SIZE = 1024;  // 가정
            NetworkStream stream = tc.GetStream();

            // 비동기 수신            
            var buff = new byte[MAX_SIZE];
            var nbytes = await stream.ReadAsync(buff, 0, buff.Length).ConfigureAwait(false);
            if (nbytes > 0)
            {                
                string msg = Encoding.ASCII.GetString(buff, 0, nbytes);
                Console.WriteLine($"{msg} at {DateTime.Now}");                
                                
                // 비동기 송신
                await stream.WriteAsync(buff, 0, nbytes).ConfigureAwait(false);
            }

            stream.Close();
            tc.Close();
        }
    }
}

수신시 타임아웃 기능 추가 예제

데이타를 수신할 때 일정 시간 동안 데이타를 수신하지 못하면 에러 발생하는 타임아웃 기능을 추가하고 싶을 때가 있다. 이를 위해서는 아래와 같이 2개의 Task 즉, 수신 Task와 타임아웃 Task를 생성하고, Task.WhenAny()를 사용하여 2개 중 하나가 완료되면 리턴하도록 하면 된다. 리턴된 Task가 타임아웃 Task이면 이는 타임아웃 시간이 경과한 것이므로 그에 따른 메시지를 전달하면 된다.

// 생략...

while (true)
{
    TcpClient tc = await listener.AcceptTcpClientAsync().ConfigureAwait(false);

    NetworkStream stream = tc.GetStream();

    var buff = new byte[MAX_SIZE];

    var readTask = stream.ReadAsync(buff, 0, buff.Length);
    var timeoutTask = Task.Delay(10 * 1000);  // 10 secs
    var doneTask = await Task.WhenAny(timeoutTask, readTask).ConfigureAwait(false);

    if (doneTask == timeoutTask) // 타임아웃이면
    {
        var bytes = Encoding.ASCII.GetBytes("Read Timeout Error");
        await stream.WriteAsync(bytes, 0, bytes.Length);
    }
    else  // 수신 성공이면
    {
        int nbytes = readTask.Result;
        if (nbytes > 0)
        {
            await stream.WriteAsync(buff, 0, nbytes).ConfigureAwait(false);
        }
    }

    stream.Close();
    tc.Close();
}

본 웹사이트는 광고를 포함하고 있습니다. 광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.

Previous Next Print