Coding Memo

에코 서버 (TcpClient, async/await 이용) 본문

Language/C#

에코 서버 (TcpClient, async/await 이용)

minttea25 2023. 11. 9. 17:29

TcpClient 및 TcpListener는 Socket을 직접 컨트롤 하지 않고 간단하고 빠르게 서버를 오픈하고 클라이언트로 서버에 연결할 수 있도록 구성되어 있는 기본 클래스이다.

https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.tcpclient?view=net-7.0

 

TcpClient Class (System.Net.Sockets)

Provides client connections for TCP network services.

learn.microsoft.com

 

특징으로는 동기 및 비동기 메서드를 포함하고 있다는 것과 Send와 Receive시에 NetworkStream으로 데이터를 읽고 쓸 수 있다는 점이다. 추가적으로 Client 프로퍼티를 통해 연결되어 있는 Socket을 직접 컨트롤 할 수도 있다.

 

단순히 좀 더 높은 레벨에서 간단하게 서버와 클라이언트의 TCP 연결을 구현할 수 있구나~ 정도로 알아도 될 것 같다. async/await를 이용한 비동기 서버와 클라이언트에서 유용하게 사용될 수 있을 것 같다. (SocketAsyncEventArgs 사용 X)


해당 코드는 서버와 클라이언트 1:1 연결을 고려해서 간단하게 작성되었다.

 

 

Server

class Program
{
    async static Task Main()
    {
        string host = Dns.GetHostName(); // local host name of my pc
        IPHostEntry ipHost = Dns.GetHostEntry(host);
        IPAddress ipAddr = ipHost.AddressList[0];
        IPEndPoint endPoint = new(ipAddr, port: 7777);

        TcpListener listener = new TcpListener(endPoint);
        listener.Start();

        var client = await listener.AcceptTcpClientAsync();

        Console.WriteLine($"Connected: {client.Client.RemoteEndPoint}");
        client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
        
        HandleClient(client);

        Console.ReadLine();
        client.Close();
    }

    static async void HandleClient(TcpClient client)
    {
        try
        {
            using (NetworkStream stream = client.GetStream())
            {
                byte[] buffer = new byte[1024];

                while (true)
                {
                    int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
                    string receivedMessage = Encoding.Unicode.GetString(buffer, 0, bytesRead);
                    Console.WriteLine($"recv: {receivedMessage}");

                    await stream.WriteAsync(buffer.AsMemory(0, bytesRead));
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
        finally
        {
            client.Close();
        }
    }
}

 

 

Client

class Program
{
    async static Task Main()
    {
        await Task.Delay(1000); // Thread.Sleep(1000);

        string host = Dns.GetHostName(); // local host name of my pc
        IPHostEntry ipHost = Dns.GetHostEntry(host);
        IPAddress ipAddr = ipHost.AddressList[0];
        IPEndPoint endPoint = new(ipAddr, port: 7777);

        TcpClient client = new TcpClient();
        client.Connect(endPoint); // 동기 호출!

        Console.WriteLine($"Connected to {client.Client.RemoteEndPoint}");

        int i = 0;
        using (NetworkStream stream = client.GetStream())
        {
            byte[] receiveBuffer = new byte[1024];
            while (true)
            {
                string msg = $"Hello: {i}";
                byte[] buffer = Encoding.Unicode.GetBytes(msg);
                await stream.WriteAsync(buffer.AsMemory(0, buffer.Length));

                int bytesRead = await stream.ReadAsync(receiveBuffer.AsMemory(0, receiveBuffer.Length));
                string receivedMessage = Encoding.Unicode.GetString(receiveBuffer, 0, bytesRead);
                Console.WriteLine($"recv: {receivedMessage}");
                
                i++;

                await Task.Delay(1000);
            }
        }
    }
}

 

socket에 대해 직접 read/write 한 것이 아니라 GetStream()을 통해 NetworkStream 객체를 가져오면서 send/recv를 했다는 것에 주목해보자.

 

send시에는 스트림에 Write를 하였고, recv 시에는 Read를 하였다.


NetworkStream 객체가 SocketAsyncEventArgs와 비슷한 역할을 한다고 볼 수도 있겠다.


async/await를 이용해서 Event처리 기반이 아닌 비동기 기반으로 서버를 만들면 코드 읽기는 편할 것같다. 하지만 많은 클라이언트를 처리하기 위해서는 이벤트 처리 기반이 더 낫다는 것 같다. 

 

클라이언트가 많을 때, async/await의 경우 처리하는 이후 코드를 실행하는 스레드가 너무 자주 바뀌어서 그런가..?