Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Archives
Today
Total
관리 메뉴

nomad-programmer

[Programming/C#] TcpListener & TcpClient 본문

Programming/C#

[Programming/C#] TcpListener & TcpClient

scii 2020. 10. 4. 02:45

TcpListener와 TcpClient는 .NET 프레임워크가 TCP/IP 통신을 위해 제공하는 클래스이다. 이들 클래스가 속해 있는 System.Net.Sockets 네임스페이스에는 보다 다양한 옵션과 메소드를 제공하는 Socket 클래스도 있지만, 사용이 복잡하다는 단점이 있기 때문에 Socket 클래스 대신 TcpListener와 TcpClient 클래스를 이용한 TCP/IP 프로그래밍을 설명한다.

TcpListener 클래스는 서버 애플리케이션에서 사용되며, 클라이언트의 연결 요청을 기다리는 역할을 한다.
TcpClient는 서버 애플리케이션과 클라이언트 애플리케이션 양쪽에서 사용된다.

클라이언트에서는 TcpClient가 서버에 연결 요청을 하는 역할을 수행하며, 서버에서는 클라이언트의 요청을 수락하면 클라이언트와의 통신에 사용할 수 있는 TcpClient의 인스턴스가 반환된다.

서버와 클라이언트가 각각 갖고 있는 TcpClient는 GetStream()이라는 메소드를 갖고 있어서, 양쪽의 응용 프로그램은 이 메소드가 반환하는 NetworkStream 객체를 통해 데이터를 주고 받는다.

  • NetworkStream.Write() : 데이터를 보낸다.
  • NetworkStream.Read() : 데이터를 읽는다.

데이터를 주고받는 일을 마치고 나서 서버와 클라이언트의 연결을 종료할 때는 NetworkStream 객체와 TcpClient 객체 모두의 Close() 메소드를 호출한다. 
다음 그림은 서버와 클라이언트에서 TCP/IP 통신을 수행하기 위해 호출하는 TcpListener와 TcpClient, 그리고 NetworkStream 클래스 메소드들의 흐름을 나태낸것이다.

다음 표는 TcpListener와 TcpClient 클래스의 주요 메소드이다.

클래스 메소드 설명
TcpListener Start() 연결 요청 수신 대기를 시작한다.
AcceptTcpClient() 클라이언트의 연결 요청을 수락한다. 이 메소드는 TcpClient 객체를 반환한다.
Stop() 연결 요청 수신 대기를 종료한다.
TcpClient Connect() 서버에 연결을 요청한다.
GetStream() 데이터를 주고받는데 사용하는 매개체인 NetworkStream을 가져온다.
Close() 연결을 닫는다.

TcpListener 코드 예제

// IPEndPoint는 IP 통신에 필요한 IP 주소와 출입구(포트)를 나타낸다.
IPEndPoint localAddress = new IPEndPoint(IPAddress.Parse("192.168.0.1"), 5555);

TcpListener server = new TcpListener(localAddress);

// server 객체는 클라이언트가 TcpClient.Connect()를 호출하여 연결 요청해오기를 기다리기 시작한다.
server.Start();

TcpListener의 인스턴스인 server가 연결 요청 수신을 받을 준비가 되었다. 이번엔 클라이언트에서 TcpClient 객체를 생성하고 서버에 연결을 요청하는 코드이다.

// 포트를 0으로 지정하면 OS에서 임의의 번호로 포트를 할당해준다.
IPEndPoint clientAddress = new IPEndPoint(IPAddress.Parse("192.168.0.1"), 0);

TcpClient client = new TcpClient(clientAddress);

IPEndPoint serverAddress = new IPEndPoint(IPAddress.Parse("192.168.0.2"), 5555);

// 서버가 수신대기하고 있는 IP주소와 포트 번호를 향해 연결 요청을 수행한다.
client.Connect(serverAddress);

서버에서 다음과 같이 AcceptTcpClient()를 호출하면 코드는 블록되어 그 자리에서 이 메소드가 반환할 때까지 진행하지 않는다. AcceptTcpClient() 메소드는 클라이언트의 연결 요청이 있기 전까지는 반환되지 않는다. 기다리던 요청이 오면 이 메소드는 클라이언트와 통신을 수행할 수 있도록 TcpClient 형식의 객체를 반환한다.

TcpClient client = server.AcceptTcpClient();

서버와 클라이언트에 있는 TcpClient 형식의 객체로부터 NetworkStream 형식의 객체를 가져와서 데이터를 읽고 쓸 수 있다. 다음의 코드는 TcpClient 객체가 NetworkStream 객체를 반환하고, NetworkStream 객체를 이용하여 데이터를 읽고 쓰는 예제이다.

// TcpClient를 통해 NetworkStream 객체를 얻는다.
NetworkStream stream = client.GetStream();

int length;
// string은 참조형식이기때문에 null을 그대로 넣을 수 있다.
string data = null;
byte[] bytes = new byte[256];

// NetworkStream.Read() 메소드는 상대방이 보내온 데이터를 읽어들인다.
// 만약 상대와의 연결이 끊어지면 이 메소드는 0을 반환한다. 
// 즉, 이 루프는 연결이 끊어지기 전까지 계속된다.
while ((length = stream.REad(bytes, 0, bytes.Length)) != 0)
{
    data = Encoding.Default.GetString(bytes, 0, length);
    Console.WriteLine(String.Format("수신: {0}", data));
    
    byte[] msg = Encoding.Default.GetBytes(data);
    
    // NetworkStream.Write() 메소드를 통해 상대방에게 메시지를 전송한다.
    stream.Write(msg, 0, msg.Length);
    Console.WriteLine(String.Format("송신: {0}", data));
}

다음은 Echo 서버 & 클라이언트 예제 코드다. 클라이언트가 보내오는 메시지를 서버가 그대로 '메아리'쳐 돌려보내는 간단한 프로그램이다.

Echo 서버

using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Diagnostics;

namespace EchoServer
{
    class MainApp
    {
        static void Main(string[] args)
        {
            if (args.Length < 1)
            {
                Console.WriteLine("usage: {0} <Bind IP>",
                    Process.GetCurrentProcess().ProcessName);
                return;
            }

            string bindIP = args[0];
            const int bindPort = 5555;
            TcpListener server = null;
            try
            {
                // IPEndPoint는 IP 통신에 필요한 IP주소와 출입구(포트)를 나타낸다.
                IPEndPoint localAddress = new IPEndPoint(IPAddress.Parse(bindIP), bindPort);

                server = new TcpListener(localAddress);

                server.Start();

                Console.WriteLine("에코 서버 시작...");

                while (true)
                {
                    TcpClient client = server.AcceptTcpClient();
                    Console.WriteLine("클라이언트 접속 : {0}",
                        ((IPEndPoint)client.Client.RemoteEndPoint).ToString());

                    NetworkStream stream = client.GetStream();

                    int length;
                    string data = null;
                    byte[] bytes = new byte[256];

                    while((length = stream.Read(bytes, 0, bytes.Length)) != 0)
                    {
                        data = Encoding.Default.GetString(bytes, 0, length);
                        Console.WriteLine(String.Format("수신: {0}", data));

                        byte[] msg = Encoding.Default.GetBytes(data);

                        stream.Write(msg, 0, msg.Length);
                        Console.WriteLine(String.Format("송신: {0}", data));
                    }
                    stream.Close();
                    client.Close();
                }
            }
            catch (SocketException err)
            {
                Console.WriteLine(err);
            }
            finally
            {
                server.Stop();
            }
            Console.WriteLine("서버를 종료합니다.");
        }
    }
}

Echo 클라이언트

using System;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace EchoClient
{
    class MainApp
    {
        static void Main(string[] args)
        {
            if (args.Length < 4)
            {
                Console.WriteLine("usage: {0} <Bind IP> <Bind Port> <Server IP> <Message>",
                    Process.GetCurrentProcess().ProcessName);
                return;
            }

            string bindIP = args[0];
            int bindPort = Convert.ToInt32(args[1]);
            string serverIP = args[2];
            const int serverPort = 5555;
            string message = args[3];

            try
            {
                IPEndPoint clientAddress = new IPEndPoint(IPAddress.Parse(bindIP), bindPort);
                IPEndPoint serverAddress = new IPEndPoint(IPAddress.Parse(serverIP), serverPort);

                Console.WriteLine("클라이언트: {0}, 서버: {1}",
                    clientAddress.ToString(), serverAddress.ToString());

                TcpClient client = new TcpClient(clientAddress);

                client.Connect(serverAddress);

                byte[] data = Encoding.Default.GetBytes(message);

                NetworkStream stream = client.GetStream();

                stream.Write(data, 0, data.Length);

                Console.WriteLine("송신: {0}", message);

                data = new byte[256];

                string responseData = "";
                int bytes = stream.Read(data, 0, data.Length);
                responseData = Encoding.Default.GetString(data, 0, bytes);
                Console.WriteLine("수신: {0}", responseData);

                stream.Close();
                client.Close();
            }
            catch (SocketException err)
            {
                Console.WriteLine(err);
            }
            Console.WriteLine("클라이언트를 종료합니다.");
        }
    }
}

실행 결과

127.0.0.1
127.0.0.1 IP주소는 컴퓨터의 네트워크 입출력 기능을 시험하기 위해 가상으로 할당한 주소이다. 네트워크 출력에 데이터를 기록하면 실제로 패킷이 링크 계층을 거쳐 네트워크 바깥으로 가야하지만 127.0.0.1을 향해 데이터를 기록하면 링크 계층을 거치지 않고 다시 자기 자신에게로 패킷을 보내게 된다.
자신에게 다시 네트워크 입력이 들어온다는 것이다. 이렇게 되돌아오는 입출력 기능 때문에 "루프백(Loopback) 주소"라고 부르기도 한다.
Comments