TCP 개요

중요

고급 사용자에게는 TcpClientTcpListener 대신 Socket 클래스가 매우 권장됩니다.

TCP(Transmission Control Protocol)를 사용하려면 두 가지 옵션이 있습니다. 하나는 제어 및 성능을 극대화하기 위해 Socket을 사용하는 것이고, 다른 하나는 TcpClientTcpListener 도우미 클래스를 사용하는 것입니다. TcpClientTcpListenerSystem.Net.Sockets.Socket 클래스 위에 빌드되며, 사용 편의를 위해 데이터 전송 관련 세부 정보를 처리합니다.

프로토콜 클래스는 기본 Socket 클래스를 사용하여 상태 정보를 유지 관리하거나 프로토콜 관련 소켓 설정의 세부 정보를 알아야 하는 오버헤드 없이 네트워크 서비스에 대한 쉬운 액세스를 제공합니다. 비동기 Socket 메서드를 사용하려면 NetworkStream 클래스에서 제공하는 비동기 메서드를 사용할 수 있습니다. 프로토콜 클래스에 의해 노출되지 않는 Socket 클래스의 기능에 액세스하려면 Socket 클래스를 사용해야 합니다.

TcpClientTcpListenerNetworkStream 클래스를 사용하여 네트워크를 나타냅니다. GetStream 메서드를 사용하여 네트워크 스트림을 반환한 다음 스트림의 NetworkStream.ReadAsyncNetworkStream.WriteAsync 메서드를 호출합니다. NetworkStream은 프로토콜 클래스의 기본 소켓을 소유하지 않으므로 닫아도 소켓에 영향을 주지 않습니다.

TcpClientTcpListener 사용

TcpClient 클래스는 TCP를 사용하여 인터넷 리소스의 데이터를 요청합니다. TcpClient의 메서드 및 속성은 TCP를 사용하여 데이터를 요청 및 수신하는 Socket을 만들기 위한 세부 정보를 추상화합니다. 원격 디바이스에 대한 연결은 스트림으로 표현되므로 .NET Framework 스트림 처리 기법을 사용하여 데이터를 읽고 쓸 수 있습니다.

TCP 프로토콜은 원격 엔드포인트에 연결한 후 해당 연결을 사용하여 데이터 패킷을 주고받습니다. TCP는 데이터 패킷이 엔드포인트로 전송되고 도착 시 올바른 순서로 어셈블되도록 합니다.

IP 엔드포인트 만들기

System.Net.Sockets로 작업할 때 네트워크 엔드포인트를 IPEndPoint 개체로 나타냅니다. IPEndPointIPAddress 및 해당 포트 번호로 생성됩니다. Socket을 통해 대화를 시작하려면 먼저 앱과 원격 대상 간에 데이터 파이프를 만듭니다.

TCP/IP는 네트워크 주소와 서비스 포트 번호를 사용하여 서비스를 고유하게 식별합니다. 네트워크 주소는 특정 네트워크 대상을 식별하고, 포트 번호는 연결할 해당 디바이스의 특정 서비스를 식별합니다. 네트워크 주소와 서비스 포트의 조합을 엔드포인트가라고 하며, .NET에서는 EndPoint 클래스로 표현됩니다. EndPoint의 하위 항목이 지원되는 각 주소 패밀리에 대해 정의되고, IP 주소 패밀리에 대한 클래스는 IPEndPoint입니다.

Dns 클래스는 TCP/IP 인터넷 서비스를 사용하는 앱에 도메인 이름 서비스를 제공합니다. GetHostEntryAsync 메서드는 DNS 서버를 쿼리하여 친숙한 도메인 이름(예: “host.contoso.com”)을 숫자 인터넷 주소(예: 192.168.1.1)에 매핑합니다. GetHostEntryAsync는 대기 시 요청된 이름에 대한 주소 및 별칭 목록이 들어 있는 Task<IPHostEntry>를 반환합니다. 대부분의 경우 AddressList 배열에 반환된 첫 번째 주소를 사용할 수 있습니다. 다음 코드에서는 host.contoso.com 서버의 IP 주소가 포함된 IPAddress를 가져옵니다.

IPHostEntry ipHostInfo = await Dns.GetHostEntryAsync("host.contoso.com");
IPAddress ipAddress = ipHostInfo.AddressList[0];

수동 테스트 및 디버깅 목적으로 일반적으로 Dns.GetHostName() 값의 결과 호스트 이름과 함께 GetHostEntryAsync 메서드를 사용하여 로컬 호스트 이름을 IP 주소로 확인할 수 있습니다. 다음 코드 조각을 살펴봅니다.

var hostName = Dns.GetHostName();
IPHostEntry localhost = await Dns.GetHostEntryAsync(hostName);
// This is the IP address of the local machine
IPAddress localIpAddress = localhost.AddressList[0];

IANA(Internet Assigned Numbers Authority)는 공통 서비스의 포트 번호를 정의합니다. 자세한 내용은 IANA: 서비스 이름 및 전송 프로토콜 포트 번호 레지스트리를 참조하세요. 다른 서비스에는 1,024 ~ 65,535 범위의 등록된 포트 번호가 있을 수 있습니다. 다음 코드에서는 host.contoso.com의 IP 주소를 포트 번호와 결합하여 연결에 대한 원격 엔드포인트를 만듭니다.

IPEndPoint ipEndPoint = new(ipAddress, 11_000);

원격 디바이스의 주소를 결정하고 연결에 사용할 포트를 선택하면 앱이 원격 디바이스에 대한 연결을 설정할 수 있습니다.

인증 요청을 처리하는 데 사용하는 TcpClient

TcpClient 클래스는 Socket 클래스보다 높은 추상화 수준에서 TCP 서비스를 제공합니다. TcpClient는 원격 호스트에 대한 클라이언트 연결을 만드는 데 사용됩니다. IPEndPoint를 가져오는 방법을 알고 있으므로 원하는 포트 번호와 페어링할 IPAddress가 있다고 가정해 보겠습니다. 다음 예제에서는 TCP 포트 13에서 시간 서버에 연결하도록 TcpClient를 설정하는 방법을 보여 줍니다.

var ipEndPoint = new IPEndPoint(ipAddress, 13);

using TcpClient client = new();
await client.ConnectAsync(ipEndPoint);
await using NetworkStream stream = client.GetStream();

var buffer = new byte[1_024];
int received = await stream.ReadAsync(buffer);

var message = Encoding.UTF8.GetString(buffer, 0, received);
Console.WriteLine($"Message received: \"{message}\"");
// Sample output:
//     Message received: "📅 8/22/2022 9:07:17 AM 🕛"

위의 C# 코드에서:

  • 알려진 IPAddress 및 포트에서 IPEndPoint를 만듭니다.
  • TcpClient 개체를 인스턴스화합니다.
  • TcpClient.ConnectAsync를 사용하여 포트 13의 원격 TCP 시간 서버에 client를 연결합니다.
  • NetworkStream을 사용하여 원격 호스트에서 데이터를 읽습니다.
  • 1_024 바이트의 읽기 버퍼를 선언합니다.
  • stream에서 읽기 버퍼로 데이터를 읽습니다.
  • 결과를 콘솔에 문자열로 씁니다.

클라이언트는 메시지가 작다는 것을 알고 있으므로 하나의 작업으로 전체 메시지를 읽기 버퍼로 읽을 수 있습니다. 메시지 크기가 크거나 길이가 확정되지 않은 메시지의 경우 클라이언트는 버퍼를 더 적절하게 사용하고 while 루프에서 읽어야 합니다.

중요

메시지를 보내고 받을 때 Encoding은 서버와 클라이언트 모두에 미리 알려야 합니다. 예를 들어 서버가 ASCIIEncoding을 사용하여 통신하지만 클라이언트가 UTF8Encoding을 사용하려고 하면 메시지 형식이 잘못됩니다.

인증 요청을 처리하는 데 사용하는 TcpListener

TcpListener는 TCP 포트에서 들어오는 요청을 모니터링한 다음, 클라이언트에 대한 연결을 관리하는 Socket 또는 TcpClient를 만드는 데 사용됩니다. Start 메서드는 수신 대기를 사용하도록 설정하고, Stop 메서드는 포트에서 수신 대기를 사용하지 않도록 설정합니다. AcceptTcpClientAsync 메서드는 들어오는 연결 요청을 허용하고 TcpClient를 만들어 요청을 처리하며, AcceptSocketAsync 메서드는 들어오는 연결 요청을 허용하고 Socket을 만들어 요청을 처리합니다.

다음 예제에서는 TcpListener를 사용해서 네트워크 시간 서버를 만들어 TCP 포트 13을 모니터링하는 방법을 보여 줍니다. 들어오는 연결 요청이 허용되면 시간 서버가 호스트 서버의 현재 날짜 및 시간을 사용하여 응답합니다.

var ipEndPoint = new IPEndPoint(IPAddress.Any, 13);
TcpListener listener = new(ipEndPoint);

try
{    
    listener.Start();

    using TcpClient handler = await listener.AcceptTcpClientAsync();
    await using NetworkStream stream = handler.GetStream();

    var message = $"📅 {DateTime.Now} 🕛";
    var dateTimeBytes = Encoding.UTF8.GetBytes(message);
    await stream.WriteAsync(dateTimeBytes);

    Console.WriteLine($"Sent message: \"{message}\"");
    // Sample output:
    //     Sent message: "📅 8/22/2022 9:07:17 AM 🕛"
}
finally
{
    listener.Stop();
}

위의 C# 코드에서:

  • IPAddress.Any 및 포트를 사용하여 IPEndPoint를 만듭니다.
  • TcpListener 개체를 인스턴스화합니다.
  • Start 메서드를 호출하여 포트에서 수신 대기를 시작합니다.
  • AcceptTcpClientAsync 메서드의 TcpClient를 사용하여 들어오는 연결 요청을 수락합니다.
  • 현재 날짜 및 시간을 문자열 메시지로 인코딩합니다.
  • NetworkStream을 사용하여 연결된 클라이언트에 데이터를 씁니다.
  • 보낸 메시지를 콘솔에 씁니다.
  • 마지막으로 Stop 메서드를 호출하여 포트의 수신 대기를 중지합니다.

Socket 클래스를 사용하는 유한 TCP 컨트롤

TcpClientTcpListener 둘 다 내부적으로 Socket 클래스에 의존합니다. 즉, 이러한 클래스로 수행할 수 있는 모든 작업은 소켓을 직접 사용하여 수행할 수 있습니다. 이 섹션에서는 여러 TcpClientTcpListener 사용 사례와 기능적으로 동일한 해당 Socket 사용 사례를 보여 줍니다.

클라이언트 소켓 만들기

TcpClient의 기본 생성자는 Socket(SocketType, ProtocolType) 생성자를 통해 이중 스택 소켓을 만들려고 합니다. 이 생성자는 IPv6이 지원되면 이중 스택 소켓을 만들고, 그렇지 않으면 IPv4로 대체됩니다.

다음 TCP 클라이언트 코드를 고려합니다.

using var client = new TcpClient();

이전 TCP 클라이언트 코드는 다음 소켓 코드와 기능적으로 동일합니다.

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);

TcpClient(AddressFamily) 생성자

이 생성자는 세 개의 AddressFamily 값만 허용합니다. 그렇지 않으면 ArgumentException을 throw합니다. 유효한 값은 다음과 같습니다.

다음 TCP 클라이언트 코드를 고려합니다.

using var client = new TcpClient(AddressFamily.InterNetwork);

이전 TCP 클라이언트 코드는 다음 소켓 코드와 기능적으로 동일합니다.

using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

TcpClient(IPEndPoint) 생성자

소켓을 만들 때 이 생성자는 제공된 로컬IPEndPoint에도 바인딩됩니다. IPEndPoint.AddressFamily 속성은 소켓의 주소 패밀리를 결정하는 데 사용됩니다.

다음 TCP 클라이언트 코드를 고려합니다.

var endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5001);
using var client = new TcpClient(endPoint);

이전 TCP 클라이언트 코드는 다음 소켓 코드와 기능적으로 동일합니다.

// Example IPEndPoint object
var endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5001);
using var socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(endPoint);

TcpClient(String, Int32) 생성자

이 생성자는 기본 생성자와 유사한 이중 스택을 만들고 hostnameport 쌍으로 정의된 원격 DNS 엔드포인트에 연결하려고 합니다.

다음 TCP 클라이언트 코드를 고려합니다.

using var client = new TcpClient("www.example.com", 80);

이전 TCP 클라이언트 코드는 다음 소켓 코드와 기능적으로 동일합니다.

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
socket.Connect("www.example.com", 80);

서버에 연결

TcpClientConnect 모든 ConnectAsync, BeginConnectEndConnect 오버로드는 해당 Socket 메서드와 기능적으로 동일합니다.

다음 TCP 클라이언트 코드를 고려합니다.

using var client = new TcpClient();
client.Connect("www.example.com", 80);

위의 TcpClient 코드는 다음 소켓 코드와 동일합니다.

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
socket.Connect("www.example.com", 80);

서버 소켓 만들기

원시 Socket 해당 항목과 기능적으로 동등한 TcpClient 인스턴스처럼 이 섹션에서는 TcpListener 생성자를 해당 소켓 코드에 매핑합니다. 고려해야 할 첫 번째 생성자는 TcpListener(IPAddress localaddr, int port)입니다.

var listener = new TcpListener(IPAddress.Loopback, 5000);

이전 TCP 수신기 코드는 다음 소켓 코드와 기능적으로 동일합니다.

var ep = new IPEndPoint(IPAddress.Loopback, 5000);
using var socket = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

서버에서 수신 대기 시작

Start() 메서드는 SocketBindListen() 기능을 결합하는 래퍼입니다.

다음 TCP 수신기 코드를 고려해 보세요.

var listener = new TcpListener(IPAddress.Loopback, 5000);
listener.Start(10);

이전 TCP 수신기 코드는 다음 소켓 코드와 기능적으로 동일합니다.

var endPoint = new IPEndPoint(IPAddress.Loopback, 5000);
using var socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(endPoint);
try
{
    socket.Listen(10);
}
catch (SocketException)
{
    socket.Dispose();
}

서버 연결 허용

내부적으로, 들어오는 TCP 연결은 수락되면 항상 새 소켓을 만듭니다. TcpListenerSocket 인스턴스를 직접 수락할 수 있습니다(AcceptSocket() 또는 AcceptSocketAsync()를 통해). 또는 TcpClient을 수락할 수 있습니다(AcceptTcpClient()AcceptTcpClientAsync()를 통해).

다음 TcpListener 코드를 생각해 볼 수 있습니다.

var listener = new TcpListener(IPAddress.Loopback, 5000);
using var acceptedSocket = await listener.AcceptSocketAsync();

// Synchronous alternative.
// var acceptedSocket = listener.AcceptSocket();

이전 TCP 수신기 코드는 다음 소켓 코드와 기능적으로 동일합니다.

var endPoint = new IPEndPoint(IPAddress.Loopback, 5000);
using var socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
using var acceptedSocket = await socket.AcceptAsync();

// Synchronous alternative
// var acceptedSocket = socket.Accept();

데이터를 보내고 받을 NetworkStream 만들기

TcpClient를 사용하는 경우 데이터를 보내고 받을 수 있도록 NetworkStreamGetStream() 메서드로 인스턴스화해야 합니다. Socket을 사용하면 NetworkStream 만들기를 수동으로 수행해야 합니다.

다음 TcpClient 코드를 생각해 볼 수 있습니다.

using var client = new TcpClient();
using NetworkStream stream = client.GetStream();

이것은 다음 소켓 코드와 동일합니다.

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);

// Be aware that transferring the ownership means that closing/disposing the stream will also close the underlying socket.
using var stream = new NetworkStream(socket, ownsSocket: true);

코드에 Stream 인스턴스를 사용할 필요가 없는 경우 NetworkStream를 만드는 대신, Socket의 Send/Receive 메서드(Send, SendAsync, ReceiveReceiveAsync)를 직접 사용할 수 있습니다.

추가 정보