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();
}
}
}