Übersicht über TCP

Wichtig

Die Socket-Klasse wird dringend für fortgeschrittene Benutzer anstelle von TcpClient und TcpListenerempfohlen.

Um mit TCP (Transmission Control Protocol) zu arbeiten, haben Sie zwei Optionen: Entweder verwenden Sie Socket für maximale Kontrolle und Leistung, oder Sie die verwenden die Hilfsklassen TcpClient und TcpListener. TcpClient und TcpListener bauen auf der System.Net.Sockets.Socket-Klasse auf und kümmern sich um die Details der Datenübertragung, um die Benutzung zu erleichtern.

Die Protokollklassen verwenden die zugrunde liegende Socket-Klasse, um einfachen Zugriff auf Netzwerkdienste bereitzustellen, ohne Mehraufwand für die Verwaltung von Statusinformationen oder Kenntnis der Details zum Einrichten der protokollspezifischen Sockets. Für die Verwendung der asynchronen Socket-Methoden können Sie die asynchronen Methoden nutzen, die von der NetworkStream-Klasse bereitgestellt werden. Zum Zugriff auf Funktionen der Socket-Klasse, die nicht von den Protokollklassen verfügbar gemacht werden, müsse Sie die Socket-Klasse verwenden.

TcpClient und TcpListener stellen das Netzwerk mithilfe der NetworkStream-Klasse dar. Verwenden Sie die GetStream-Methode zum Zurückgeben des Netzwerkstreams, und rufen Sie anschließend die NetworkStream.ReadAsync- und NetworkStream.WriteAsync-Methoden des Streams auf. Der NetworkStream besitzt den den Protokollklassen zugrunde liegenden Socket nicht, sodass der Abschluss den Socket nicht betrifft.

Verwenden von TcpClient und TcpListener

Die TcpClient-Klasse fordert über TCP Daten von einer Internetressource an. Die Eigenschaften und Methoden von TcpClient abstrahieren die Details zum Erstellen einer Socket zum Anfordern und Empfangen von Daten mithilfe von TCP. Da die Verbindung mit dem Remotegerät als Datenstrom dargestellt wird, können Daten mit Techniken zur Datenstromverarbeitung von .NET Framework gelesen und geschrieben werden.

Das TCP-Protokoll stellt eine Verbindung mit einem Remoteendpunkt her, und verwendet dann diese Verbindung zum Senden und Empfangen von Datenpaketen. TCP ist dafür verantwortlich, dass die Datenpakete an den Endpunkt gesendet und in der richtigen Reihenfolge zusammengestellt werden, wenn sie ankommen.

Erstellen eines IP-Endpunkts

Wenn Sie mit System.Net.Sockets arbeiten, stellen Sie einen Netzwerkendpunkt als ein IPEndPoint-Objekt dar. Der IPEndPoint wird mit einer IPAddress und der entsprechenden Portnummer erstellt. Bevor Sie über einen Socket eine Konversation initiieren können, müssen Sie eine Datenpipeline zwischen Ihrer App und dem Remoteziel erstellen.

TCP/IP verwendet eine Netzwerkadresse und eine Dienstportnummer zur eindeutigen Identifizierung eines Diensts. Die Netzwerkadresse identifiziert ein bestimmtes Netzwerkziel und die Portnummer den Dienst auf diesem Gerät, mit dem eine Verbindung hergestellt werden soll. Die Kombination von Netzwerkadresse und Dienstport wird Endpunkt genannt. Dieser wird in .NET durch die EndPoint-Klasse dargestellt. Ein Nachfolger des EndPoint wird für jede unterstützte Adressfamilie definiert. Die Klasse für die IP-Adressfamilie ist IPEndPoint.

Die Dns-Klasse stellt Domain Name Services für Apps bereit, die TCP/IP-Internetdienste verwenden. Die GetHostEntryAsync-Methode fragt einen DNS-Server ab, um einer numerischen Internetadresse (z. B. 192.168.1.1) einen benutzerfreundlichen Domänennamen (z. B. „host.contoso.com“) zuzuordnen. GetHostEntryAsync gibt einen Task<IPHostEntry> zurück. Wenn dieser erwartet wird, enthält er eine Liste der Adressen und Aliasse für den angeforderten Namen. In den meisten Fällen können Sie die erste Adresse verwenden, die im AddressList-Array zurückgegeben wurde. Der folgende Code erhält eine IPAddress mit der IP-Adresse für den Server host.contoso.com.

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

Tipp

Zu manuellen Test- und Debugzwecken können Sie in der Regel die GetHostEntryAsync-Methode mit dem resultierenden Hostnamen aus dem Dns.GetHostName()-Wert verwenden, um den Localhostnamen in eine IP-Adresse aufzulösen. Betrachten Sie den folgenden Codeausschnitt:

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

Internet Assigned Numbers Authority (IANA) definiert die Portnummern für verbreitete Dienste. Weitere Informationen finden Sie unter IANA: Portnummerregister für Dienstnamen und Transportprotokolle. Andere Dienste haben registrierte Portnummern im Bereich von 1024 bis 65.535. Der folgende Code kombiniert die IP-Adresse für host.contoso.com mit einer Portnummer zum Erstellen eines Remoteendpunkts für eine Verbindung.

IPEndPoint ipEndPoint = new(ipAddress, 11_000);

Nach der Ermittlung der Adresse des Remotegeräts und der Auswahl eines Ports für die Verbindung, kann die App versuchen, eine Verbindung mit dem Remotegerät herzustellen.

Erstellen der Datei TcpClient

Die TcpClient-Klasse stellt TCP-Dienste auf einer höheren Abstraktionsebene bereit als die Socket-Klasse. TcpClient wird verwendet, um eine Clientverbindung mit einem Remotehost herzustellen. Wenn Sie wissen, wie Sie einen IPEndPoint abrufen, nehmen wir an, dass Sie über eine IPAddress zum Koppeln mit Ihrer gewünschten Portnummer verfügen. Das folgende Beispiel veranschaulicht das Festlegen eines TcpClient zur Verbindung mit einem Zeitserver an TCP-Port 13:

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 🕛"

Für den C#-Code oben gilt:

  • Erstellt einen IPEndPoint aus einer bekannten IPAddress und einem Port.
  • Instanziiert ein neues TcpClient-Objekt.
  • Verbindet client unter Verwendung von TcpClient.ConnectAsync mit dem TCP-Remotezeitserver an Port 13.
  • Verwendet einen NetworkStream, um Daten vom Remotehost zu lesen.
  • Deklariert einen Lesepuffer von 1_024 Bytes.
  • Liest Daten aus dem stream in den Lesepuffer.
  • Schreibt die Ergebnisse als Zeichenfolge in die Konsole.

Da der Client weiß, dass die Nachricht klein ist, kann die gesamte Nachricht in einem Vorgang in den Lesepuffer gelesen werden. Bei größeren Nachrichten oder Nachrichten mit unbestimmter Länge sollte der Client den Puffer effizienter verwenden und in einer while-Schleife lesen.

Wichtig

Beim Senden und Empfangen von Nachrichten sollte sowohl dem Server als auch dem Client das Encoding im Voraus bekannt sein. Wenn der Server beispielsweise mit ASCIIEncoding kommuniziert, aber der Client versucht, UTF8Encoding zu verwenden, werden die Nachrichten falsch formatiert.

Erstellen der Datei TcpListener

Der Typ TcpListener wird verwendet, um einen TCP-Port auf eingehende Anforderungen zu überwachen, und entweder einen Socket oder TcpClient zu erstellen, der die Verbindung mit dem Client verwaltet. Die Start-Methode ermöglicht die Überwachung, und die Stop-Methode deaktiviert die Überwachung des Ports. Die AcceptTcpClientAsync-Methode akzeptiert eingehende Verbindungsanforderungen und erstellt einen TcpClient zur Verarbeitung der Anforderung. Die AcceptSocketAsync-Methode akzeptiert eingehende Verbindungsanforderungen und erstellt einen Socket zur Verarbeitung der Anforderung.

Das folgende Beispiel veranschaulicht das Erstellen eines Netzwerkzeitservers mithilfe eines TcpListener zur Überwachung des TCP-Ports 13. Wenn eine eingehende Verbindungsanforderung akzeptiert wird, antwortet der Zeitserver mit dem aktuellen Datum und der Uhrzeit vom Hostserver.

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

Für den C#-Code oben gilt:

  • Erstellt einen IPEndPoint mit IPAddress.Any und einen Port.
  • Instanziiert ein neues TcpListener-Objekt.
  • Ruft die Start-Methode auf, um mit dem Lauschen am Port zu beginnen.
  • Verwendet einen TcpClient aus der AcceptTcpClientAsync-Methode, um eingehende Verbindungsanforderungen zu akzeptieren.
  • Codiert das aktuelle Datum und die aktuelle Uhrzeit als Zeichenfolgennachricht.
  • Verwendet einen NetworkStream, um Daten in den verbundenen Client zu schreiben.
  • Schreibt die gesendete Nachricht in die Konsole.
  • Ruft schließlich die Stop-Methode auf, um das Lauschen am Port zu beenden.

Endliche TCP-Steuerung mit der Socket-Klasse

Sowohl TcpClient als auch TcpListener stützen sich intern auf die Socket-Klasse, was bedeutet, dass alles, was Sie mit diesen Klassen erledigen können, auch direkt mit Sockets erreicht werden kann. In diesem Abschnitt werden mehrere Anwendungsfälle für TcpClient und TcpListener sowie deren funktional gleichwertige Socket-Entsprechung gezeigt.

Erstellen eines Clientsockets

Der Standardkonstruktor von TcpClient versucht, einen Dual-Stack-Socket über den Konstruktor Socket(SocketType, ProtocolType) zu erstellen. Dieser Konstruktor erstellt einen Dual-Stack-Socket, wenn IPv6 unterstützt wird. Andernfalls wird auf IPv4 zurückgegriffen.

Betrachten Sie den folgenden TCP-Clientcode:

using var client = new TcpClient();

Der TCP-Clientcode oben entspricht funktionell dem folgenden Socketcode:

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

Der TcpClient(AddressFamily)-Konstruktor

Dieser Konstruktor akzeptiert nur drei AddressFamily-Werte, andernfalls wird eine ArgumentException ausgelöst. Gültige Werte sind:

Betrachten Sie den folgenden TCP-Clientcode:

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

Der TCP-Clientcode oben entspricht funktionell dem folgenden Socketcode:

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

Der TcpClient(IPEndPoint)-Konstruktor

Beim Erstellen des Sockets wird dieser Konstruktor auch an den bereitgestellten lokalenIPEndPointgebunden. Die IPEndPoint.AddressFamily-Eigenschaft wird verwendet, um die Adressfamilie des Sockets zu bestimmen.

Betrachten Sie den folgenden TCP-Clientcode:

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

Der TCP-Clientcode oben entspricht funktionell dem folgenden Socketcode:

// 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);

Der TcpClient(String, Int32)-Konstruktor

Dieser Konstruktor versucht, einen dualen Stapel ähnlich wie der Standardkonstruktor zu erstellen und ihn mit dem DNS-Remoteendpunkt zu verbinden, der durch das Paar hostname und port definiert wird.

Betrachten Sie den folgenden TCP-Clientcode:

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

Der TCP-Clientcode oben entspricht funktionell dem folgenden Socketcode:

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

Mit Server verbinden

Alle Connect-, ConnectAsync-, BeginConnect- und EndConnect-Überladungen in TcpClient sind funktionell mit den entsprechenden Socket-Methoden gleichwertig.

Betrachten Sie den folgenden TCP-Clientcode:

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

Der TcpClient-Code oben entspricht dem folgenden Socketcode:

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

Erstellen eines Serversockets

Ähnlich wie TcpClient-Instanzen funktionale Äquivalenz mit ihren Socket-Raw-Gegenstücken aufweisen, ordnet dieser Abschnitt TcpListener-Konstruktoren ihrem entsprechenden Socketcode zu. Der erste zu berücksichtigende Konstruktor ist TcpListener(IPAddress localaddr, int port).

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

Der TCP-Listener oben entspricht funktionell dem folgenden Socketcode:

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

Starten des Lauschvorgangs auf dem Server

Die Start()-Methode ist ein Wrapper, der die Bind- und Listen()-Funktionalität von Socket kombiniert.

Betrachten Sie den folgenden TCP-Listenercode:

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

Der TCP-Listener oben entspricht funktionell dem folgenden Socketcode:

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

Annehmen einer Serververbindung

Im Hintergrund erstellen eingehende TCP-Verbindungen immer einen neuen Socket, wenn sie akzeptiert werden. TcpListener kann eine Socket-Instanz direkt (über AcceptSocket() oder AcceptSocketAsync()) oder einen TcpClient (über AcceptTcpClient() und AcceptTcpClientAsync()) akzeptieren.

Betrachten Sie folgenden TcpListener-Code:

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

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

Der TCP-Listener oben entspricht funktionell dem folgenden Socketcode:

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

Erstellen eines NetworkStream zum Senden und Empfangen von Daten

Mit TcpClient müssen Sie einen NetworkStream mit der GetStream()-Methode instanziieren, um Daten senden und empfangen zu können. Mit Socket müssen Sie die NetworkStream-Erstellung manuell durchführen.

Betrachten Sie folgenden TcpClient-Code:

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

Er entspricht dem folgenden Socketcode:

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

Tipp

Wenn Ihr Code nicht mit einer Stream-Instanz arbeiten muss, können Sie sich direkt auf die Methoden zum Senden und Empfangen (Send, SendAsync, Receive und ReceiveAsync) von Socket berufen, anstatt einen NetworkStream zu erstellen.

Siehe auch