5. 비동기 TCPListener 예제

동기화 코드의 문제점
데이터를 보내는 작업과 받는 작업이 블록되는 문제 상황을 강제로 발생시켜보자.
네트워크 프로그램은 많은 부분에서 비동기 코드가 필요하다.
Read( ) 함수의 동작은 C언어의 Scanf( ) 동작과 유사하다.
Read( ) 함수는 stream에 전달받은 데이터가 있다면, 데이터를 읽어들인다.
하지만 읽을 데이터가 없다면, 데이터가 들어올 때까지 대기하는데, 이때 실행 흐름은 블록된다.
만약 이 상태에서 데이터를 전송시키고 싶다면, Send( ) 가 실행 제어권을 가지지 못하기에 전송 작업 실행은 불가능하다.
위의 예제 프로젝트의 소스코드는, 이전 포스트와 동일하다.
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 을 사용한다.
- 서버만 비동기 처리한 소스코드를 이용한다.
- 클라인언트도 동일한 방법으로 비동기 처리하여 구현하면 된다.
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(); } } }
댓글을 사용할 수 없습니다.