Erweiterte Programmierthemen: Netzwerkzugriffe
Um Netzwerkzugriffe zu programmieren, benötigen wir als erstes die Namensräume System.Net und System.Net.Sockets. Als Basis (für verschiedene Netzwerkzugriffe) wollen wir uns mit den Klassen IPAddress, IPEndPoint und Dns vertraut machen. IPAddress speichert eine IP-Adresse in einem Objekt. Dem Konstruktor kann sowohl eine 64bit-Zahl oder auch ein byte-Array übergeben werden. Die Eigenschaft AddressFamily ruft die Adressen-Familie (Enumeration AddressFamily: InterNetwork für IPv4 und InterNetworkV6 für IPv6) ab. Zumeist nutzen wir jedoch die Funktion TryParse(), um eine Zeichenkette in ein IP-Adressen-Objekt umzuwandeln. Die IPEndPoint-Klasse stellt im Gegensatz zur IPAddress-Klasse einen Netzwerk-Endpunkt zur Verfügung, welcher eine IP-Adresse und ein Port enthalten. Dem Konstruktor der IPEndPoint-Klasse werden ein IPAddress-Objekt und ein Port (als Int-Wert) übergeben. Die Dns-Klasse ist statisch und stellt uns Funktionen zur DNS-Auflösung zur Verfügung. Die Funktion GetHostAddresses() erwartet als Parameter eine Zeichenkette (der aufzulösende DNS-Name) und gibt eine Array von IP-Adressen zurück. Diese Funktion arbeitet synchron, d. h. das Programm kann erst weitere Statements ausführen, sobald die Funktion vollständig abgearbeitet wurde (also nach erfolgreicher Namensauflösung oder einem Timeout). Bitte bedenken Sie, dass ggf. keine Namensauflösung erfolgt, wenn die DNS-Datenbank des Computers bereits einen passenden Eintrag enthält. Für eine asynchrone Ausführung stehen uns die Funktionen BeginGetHostAddresses() und EndGetHostAddresses() zur Verfügung.
Für UDP-Verbindungen steht uns die Klasse UdpClient zur Verfügung. Bei UDP gibt es programmiertechnisch gesehen keine Unterscheidung zwischen Client und Server, da UDP verbindungslos ist, weshalb auch für Server-Zwecke die Klasse UdpClient verwendet wird. Dem Konstruktor wird zumeist kein Parameter übergeben, da sich die Parameter immer auf die lokale Schnittstelle beziehen, wir jedoch (in diesem Fall) wollen, dass Windows sich die lokale Netzwerk-Schnittstelle automatisch heraussucht. Die Eigenschaft Available gibt die Datenmenge in Bytes an, welche am Socket (also der „Anschlussdose“) eingegangen sind. Die Funktion Connect() stellt eine Verbindung zu einem Netzwerk-Endpunkt her. Dieser Funktionsaufruf baut nicht wirklich eine Verbindung auf (da UDP verbindungslos ist), sondern übernimmt vielmehr den Endpunkt in die Klasse. Die Funktion Send() sendet ein Daten-Paket an den Ziel-Endpunkt (welcher mit Connect() übergeben wurde). Hierzu werden der Funktion ein byte-Array und eine Größe des Arrays übergeben. Über die Funktion Receive() können wir ein Daten-Paket empfangen. Als Parameter müssen wir einen Verweis auf eine Endpunkt-Variable übergeben. Darüber können wir prüfen, von wem das gesendete Paket stammt. Da es sich bei einem Socket um einen Stream handelt, müssen wir diesen am Ende wieder schließen (Funktion Close()). Das Beispiel zeigt einen (S)NTP-Client, mit welchem wir die Uhrzeit und das Datum von einem Zeitserver abrufen können. Auf das Protokoll selbst wollen wir jedoch hier nicht genauer eingehen. Nähere Informationen zum Protokoll im RFC 958.
Für TCP-Verbindungen stehen uns die Klassen TcpClient und TcpListener (für Server-Zwecke) zur Verfügung. Ein paar Eigenschaften und Funktionen sowie der Grundaufbau sind ähnlich zur UdpClient-Klasse. Bei TCP-Client-Verbindungen werden Daten über einen Stream versendet und empfangen (GetStream()-Funktion). Bei einem TcpListener können wir über die Funktion AcceptTcpClient() ein TcpClient-Objekt abrufen, um eine einkommende Nachricht zu bearbeiten. Mit Hilfe dieses Objekts können wir dann die Nachricht des Clients abrufen, als auch eine Nachricht an den Client versenden. Zu beachten ist, dass AcceptTcpClient() eine blockierende Funktion ist und somit auf die Anfrage eines Clients wartet. Dieses „Problem“ lässt sich über die asynchronen Funktionen BeginAcceptTcpClient() und EndAcceptTcpClient() lösen.
Um einen Ping (ICMP-Paket) zu senden, benötigen wir ein Objekt der Klasse Ping. Über die Funktion Send() kann ein ICMP-Paket synchron gesendet werden. Hierfür müssen wir der Funktion eine Zeichenkette, welche eine IP-Adresse oder einen Hostnamen enthält, übergeben. Die Funktion ist mehrfach überladen, wodurch wir zusätzlich den Timeout, die zu versendenden Daten und Ping-Optionen festlegen können. Die Funktion liefert ein Objekt der Klasse PingReply zurück. Über die Eigenschaft Status können wir einen Wert der Enumeration IPStatus abrufen (Success = Erfolg, TimedOut = Timeout, …). Über die Funktion SendAsync() kann ein asynchroner ICMP-Paket-Versand gestartet werden. Anders als bei anderen asynchronen Funktionen müssen wir hier das Ereignis PingCompleted des Ping-Objekts registrieren. Die Ping-Funktionalität befindet sich im Namensraum System.Net.NetworkInformation.
Program.cs
using System; using System.Net; using System.Net.Sockets; namespace CSV20.Netzwerkzugriffe { class Program { private static string sTitle = "SNTP-Client"; private static string sSntpServer = "ptbtime1.ptb.de"; static void Main(string[] args) { IPAddress oIpAddress; IPEndPoint oIpEndPoint; UdpClient oSocket = null; DateTime oDateTime; byte[] aPacket = new byte[48]; uint uiTimestamp; Console.Title = sTitle; Console.WriteLine(sTitle); Console.WriteLine(); try { // Umwandlung von sSntpServer in eine IP-Adressen ausprobieren if (!IPAddress.TryParse(sSntpServer, out oIpAddress)) { // sSntpServer ist ein DNS-Name, starte Auflösung oIpAddress = Dns.GetHostAddresses(sSntpServer)[0]; Console.WriteLine("DNS-Name {0} in {1} aufgelöst!", sSntpServer, oIpAddress.ToString()); } // IP-Endpunkt erstellen oIpEndPoint = new IPEndPoint(oIpAddress, 123); // Socket öffnen oSocket = new UdpClient(); oSocket.Connect(oIpEndPoint); // leeres Packet mit Kennezeichnung an Server senden aPacket[0] = 0x1B; // NTP-Version 3, Client-Modus (siehe RFC 5905) oSocket.Send(aPacket, 48); // Daten vom Server abholen aPacket = oSocket.Receive(ref oIpEndPoint); // UTC-Sekunden aus NTP-Packet laden uiTimestamp = BitConverter.ToUInt32(aPacket, 40); uiTimestamp = (((uiTimestamp & 0x000000ff) << 24) + ((uiTimestamp & 0x0000ff00) << 8) + ((uiTimestamp & 0x00ff0000) >> 8) + ((uiTimestamp & 0xff000000) >> 24)); // in Datum- / Uhrzeit-Objekt umwandeln und ausgeben oDateTime = new DateTime(1900, 1, 1).AddSeconds(uiTimestamp); Console.WriteLine("UTC-Zeit: " + oDateTime.ToString()); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } finally { // Socket schließen if (oSocket != null) oSocket.Close(); } Console.ReadKey(); } } }