Artikel

Mai 2004 | Artikel

Verbindungstest

(Link zum Artikel: http://www.it-republik.de/dotnet/artikel/0551)

Implementierung des Ping-Befehls

Text: von Sebastian Eschweiler
  • Teilen
  • kommentieren
  • empfehlen
  • Bookmark and Share
Um den Status einer Netzwerkverbindung oder einer Verbindung ins Internet zu überprüfen, führt der sicherste und einfachste Weg über den Ping-Befehl des Betriebssystems. Das .NET Framework stellt bisher leider keine Klasse zur Verfügung, die die Funktionalität dieses kleinen Programms nachbildet. Soll die Erreichbarkeit eines Hosts trotzdem auf diese Weise überprüft werden, bleibt uns nichts anderen übrig als hierzu eine eigene Funktion zu entwickeln. Mit ein wenig Hintergrundwissen über die Möglichkeiten des Netzwerkprotokolls ICMP (Internet Control Message Protocol) ist dieses Problem schnell gelöst.

Jeder, der irgendwann einmal ein Netzwerk in Betrieb genommen hat oder versucht hat, seinen Computer in die unendlichen Weiten des World Wide Web zu befördern, dürfte früher oder später über das Programm Ping gestolpert sein, welches auf einfach Weise die Erreichbarkeit eines Hostrechners überprüft. Hierzu wird per Kommandozeile die IP-Adresse oder der Domainname des Zielhosts übergeben. Um den Verbindungstest vorzunehmen, greift Ping auf das Netzwerkprotokoll ICMP (Internet Control Message Protocol) zurück und nutzt hierbei die Möglichkeit, über ICMP einen Echo Request zu erzeugen. Hierbei passiert nichts anderes, als dass ein ICMP-Packet versendet wird und der Zielhost anhand einer Kennung erkennt, dass dieses Paket ohne Veränderung an den Sender zurückgeschickt werden muss. Der Sender kann nun überprüfen, ob das empfangene Paket dem ursprünglich gesendeten entspricht und aus dieser Tatsache ableiten, ob der Zielhost erreichbar ist. Außerdem kann die Laufzeit des Pakets vom Senden bis zum Eintreffen des Echos ermittelt und somit eine Aussage über die Geschwindigkeit der getesteten Verbindung getroffen werden.

Internet Control Message Protocol
Das Internet Control Message Protocol ist eine Erweiterung des Internet Protocols (IP). Dies bedeutet, dass der Header eines IP-Pakets um die entsprechenden Felder des ICMP-Pakets erweitert wird. Abpictureung 1 zeigt die Felder, die hinzugefügt werden.
Zunächst ist hierbei das Feld mit der Bezeichnung Type von entscheidender Bedeutung. Damit der Zielrechner beim Eintreffen des Pakets weiß, dass eine Echo-Nachricht an den Sender zurückgeschickt werden soll, wird in diesem Feld eine 8 eingetragen. Welche Werte für das Feld Type sonst noch zur Verfügung stehen, lässt sich unter ftp.rfc-edior.org/in-notes/rfc792.txt nachlesen. Um ein weiteres Beispiel anzuführen, lässt sich mit Type=3 eine Packet vom Typ Destination unreachable generieren, um darüber zu informieren, dass ein bestimmter Rechner über das Netzwerk nicht zu erreichen ist.
Über das Feld Code des ICMP-Headers lässt sich der mit Type ausgewählte ICMP-Type noch näher spezifizieren. Für unseren Anwendungsfall (Type=8: Echo request) wird dieses Feld jedoch nicht benötigt und daher auf den Wert 0 gesetzt.Im dritten Feld (16-Bit-Wort) wird die Prüfsumme der Nachricht verwaltet. Die Prüfsumme besteht aus dem Einerkomplement aller Einerkomplemente der 16-Bit-Wörter der Nachricht. Dies hört sich zunächst etwas kompliziert an, eine entsprechende Methode, die diese Arbeit für uns übernimmt, werden wir aber weiter unten kennen lernen.Die beiden nächsten ICMP-Felder, Identifier und Sequence Number, nehmen einen beliebigen Wert auf und erfüllen lediglich den Zweck einer eindeutigen Identifizierung des Pakets. Im letzten Feld nimmt das ICMP-Paket die eigentlichen Daten auf. Dies ist für die Implementierung einer Ping-Methode aber nicht von Bedeutung, weshalb dieser Bereich mit Nullen (8 Byte) gefüllt wird.
Somit hätten wir den Aufbau eines ICMP-Pakets geklärt und können uns nun dem wesentlich einfacheren Teil, der Implementierung unserer Ping-Klasse, widmen.
Ping-Klasse
Für die Realisierung einer ICMP-Message sollten Sie zunächst die Namensräume System.Net und System.Net.Sockets einbinden. Zum Ausführen des Ping implementieren wir eine gleichnamige statische Methode, die einen Rückgabewert vom Typ bool liefert, abhängig davon, ob der Host erreichbar war oder nicht. Die Methode Ping benötigt zwei Parameter. Der Hostname, der auf Erreichbarkeit geprüft werden soll, wird als String übergeben. Die maximale Zeit, die verstreichen darf, bis der Host einen Antwort gesendet hat (Timeout), wird in Millisekunden als int-Wert übergeben. Listing 1 zeigt die fertige Methode Ping.

Listing 1
  1. // Methode zum Ausführen des Ping-Befehls
  2. public static bool Ping(string host, int timeout)
  3. {
  4. // DNS-Abfrage zur Ermittlung der IP-Adresse des Zielhosts
  5. // starten
  6. IPHostEntry ipHostEntry = Dns.Resolve(host);
  7. IPAddress ip = ipHostEntry.AddressList[0];
  8. // Port 7 des Hosts auswählen
  9. IPEndPoint ipEndPoint = new IPEndPoint(ip, 7);
  10. // Einen Socket für das ICMP-Protokoll erzeugen
  11. Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.Icmp);
  12. // ICMP-Packet (ICMP-Type = 8: Echo Request)
  13. byte[] icmpPacket = {8, 0, 0xF7, 0xFF, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
  14. int result = socket.SendTo(icmpPacket, icmpPacket.Length, SocketFlags.None, ipEndPoint);
  15. ArrayList receivedData = new ArrayList();
  16. receivedData.Add(socket);
  17. Socket.Select(receivedData, null, null, timeout*1000);
  18. byte[] receiveICMPPacket = new byte[200];
  19. int bytes = 0;
  20. if (receivedData.Count > 0)
  21. {
  22. // Daten jetzt auslesen
  23. bytes = socket.Receive(receiveICMPPacket, receiveICMPPacket.Length, SocketFlags.None);
  24. if (getICMPChecksum(receiveICMPPacket) == 0)
  25. return true;
  26. else
  27. return false;
  28. }
  29. else
  30. return false;
  31. }
Um die ICMP-Message vom Typ Echo Request zu senden, sind zunächst einige vorbereitende Schritte innerhalb der Methode Ping nötig. Der als String übergebene Zielhost wird zunächst über eine DNS-Abfrage ermittelt. Die zugehörige IP-Adresse kann nun über das IPHostEntry-Objekt ermittelt werden. Die Tatsache, dass ein Host auch mehrere zugeteilte IP-Adressen besitzen kann, berücksichtigen wir nicht und wählen über die Eigenschaft AddressList über den Index 0 die erste IP-Adresse aus. Ob noch weitere IP-Adressen vorhanden sind, kann auf Wunsch an dieser Stelle leicht überprüft werden.
Der End Point wird auf die ermittelte IP-Adresse eingestellt und mit Port 7 verbunden. Die Klasse IPEndPoint des .NET Frameworks übernimmt diese einfache Aufgabe für uns. Da nun die Zieladresse bekannt ist, kann eine Socket-Instanz angelegt werden. Der Konstruktor der gleichnamigen Klasse erwarte von uns drei Parameter, die den Verbindungstyp genauer spezifizieren. Zunächst übergeben wir an erster Stelle den Wert AdressFamily.InterNetwork und signalisieren, dass IPv4-gültige IP-Adressen zum Einsatz kommen. Über den zweiten Parameter stellen Sie den Socket Type ein. Hier verwenden wir SocketType.Raw. Zuletzt wird das Protokoll ausgewählt und für unseren Anwendungsfall natürlich auf ProtocolType.ICMP eingestellt.
Die Socket-Instanz ist nun für das Senden eines ICMP-Pakets vorbereitet. Um diese Aktion nun endlich auszuführen, fehlt uns noch das passende ICMP-Paket. Mit den Grundlagen, die wir bereits im ersten Abschnitt ausführlich geklärt haben, ist dieses Paket schnell erstellt:
  1. byte[] icmpPacket = {8, 0, 0xF7, 0xFF, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
Um ein ICMP-Paket vom Typ Echo Request zu erzeugen, muss die erste Stelle im Array auf den Wert 8 gesetzt werden. Die Felder Checksum und Identifier belegen wir mit den Werten 0xF7 und 0xFF, um dem Paket eine eindeutige Kennung zu verleihen. Die restlichen Stellen des Array (ICMP-Felder Sequence Number und Data) werden mit Nullen aufgefüllt, um das Paket auf die entsprechende Größe zu bringen. Es werden folglich keine Daten im Header übertragen, da dies für unseren Anwendungsfall nicht erforderlich ist.
Ist das ICMP-Paket in Form eines einfachen Byte-Arrays erzeugt, können wir die Daten zum Senden übergeben. Dies übernimmt für uns die Methode SendTo der Socket-Instanz. Als Parameter erwartet diese Methode das soeben erzeuge ICMP-Paket als Byte-Array, die Länge des Arrays und einen Wert vom Typ SocketFlags. Nach dem Senden überprüfen wir mittels der Methode Select aus der Klasse Socket, ob empfangene Daten nach einem angegeben Timeout vorliegen. Erst nachdem dieser Test positiv ausgefallen ist, was mit dem Ausdruck receivedData.Count > 0 überprüft wird, kann das Antwort-Paket ausgelesen werden. Anschließend muss sichergestellt werden, dass das Echo-Paket dem Ausgangspaket entspricht. Nur in diesem Fall kann davon ausgegangen werden, dass der Zielhost das Paket als Antwort auf den Echo Request verschickt hat. Hierzu muss durch die Methode getICMPChecksum die Prüfsumme gepictureet werden. Da die Prüfsumme aber bereits enthalten ist, muss das Ergebnis der Methode 0 sein. Die Implementierung der Checksum-Methode werden wir uns in Kürze ansehen.
Prüfsumme bilden
In Listing 1 verwenden wir die Methode getICMPChecksum um die Prüfsumme eines ICMP-Pakets zu pictureen. Als Parameter erhält diese Methode das komplette ICMP-Paket als Byte-Array. Der Rückgabewert ist vom Typ ushort.
Die Prüfsumme ist das Einerkomplement der Einerkomplementsumme aller 16-Bit-Wörter des ICMP-Pakets. Im Beispiel aus Listing 1 wurde die Prüfsumme mit 0xF7 und 0xFF (zweimal 8 Bit) direkt eingesetzt. Wird zu einem Paket, welches bereits die eigene Prüfnummer gespeichert hat, ein weiteres Mal die Prüfsumme berechnet, erhält man das Ergebnis 0. Diese Eigenschaft setzten wir im Beispiel zur Überprüfung des empfangenen Datenpakets ein. Möchten Sie an dieser Stelle die Prüfsumme nicht, wie im Beispiel geschehen, manuell einsetzen, dann setzen Sie das dritte und vierte Byte der Nachricht auf 0 und übergeben das Array an die Methode getICMPChecksum. Den Rückgabewert können Sie anschließend an der betreffenden Stelle im Paket einsetzen. Dieses Verfahren müssen Sie anwenden, wenn die Prüfsumme des Pakets sich von Mal zu Mal unterscheiden kann. Dies kann zum Beispiel dann der Fall sein, wenn Sie eine Identifier für das Paket verwenden.
Der Identifier (ebenfalls 16-Bit und somit 2 Byte) befindet sich in unserem Array an fünfter und sechster Stelle. Um einen eindeutigen Identifier zu erhalten, bietet es sich an, einen Zufallswert vom Typ byte zu erzeugen:
  1. byte random_ident = Convert.ToByte(rnd.Next(255).ToString());
Dieser Wert wird anschließend an der betreffenden Stelle des ICMP-Arrays eingesetzt. Vergessen Sie nicht, die Felder drei und vier, die die Prüfsumme des Pakets aufnehmen, zunächst mit dem Wert 0 zu belegen. Das Paket mit dem neuen Identifier übergeben wir nun der Methode getICMPChecksum und erhalten als Resultat eine Prüfsumme vom Typ ushort (16-Bit). Um diese Prüfsumme in das ICMP-Paket (drittes und viertes Feld) zu schreiben, muss der 16-Bit-Wert zunächst in zwei 8-Bit-Werte vom Typ byte umgewandelt werden:
  1. ushort sum = GetTCPCheckSum(icmpPacket);
  2. byte[] checksum_array;
  3. checksum_array = BitConverter.GetBytes(sum);
  4. icmpPacket[3] = checksum_array[0];
  5. icmpPacket[2] = checksum_array[1];
Für die Umwandlung bedienen wir uns der Methode GetBytes aus der Klasse BitConverter, die ein Array mit der Länge zwei zurückliefert.Durch den Einsatz des Identifiers haben Sie nun eine zusätzliche Paket-Eigenschaft, die Sie beim Eintreffen der Antwort-Nachricht überprüfen können. Dies ist besonders dann sinnvoll, wenn viele Ping-Anfragen (beispielsweise durch verschiedene Threads) gleichzeitig ausgeführt werden.

Einerkomplement
Das Einerkomplement ist eine arithmetische Operation aus Binärzahlen. Alle Ziffern werden bei dieser Operation getauscht. Konkret bedeutet dies, dass aus dem Wert 0 der Wert 1 wird und umgekehrt. Man bezeichnet dieses Kippen der Zahlen auch als logische NOT-Operation.


Die Methode getICMPChecksum überprüft zunächst, ob das Paket, zu dem die Prüfsumme berechnet werden soll, durchgängig aus 16-Bit-Wörtern besteht. Fehlen am Ende 8 Bit zur Bildung eines vollständigen 16-Bit-Worts, werden diese durch das Anfügen eines Bytes ergänzt. Anschließend kann in einem Schleifendurchlauf mit der eigentlichen Berechnung begonnen werden. Listing 2 zeigt die komplette Methode getICMPChecksum.

Listing 2
  1. static ushort getICMPChecksum(byte[] icmpP)
  2. {
  3. byte[] buffer;
  4. if ((icmpP.Length % 2) > 0)
  5. {
  6. buffer = new byte[icmpP.Length +1];
  7. icmpP.CopyTo(buffer, 0);
  8. buffer[buffer.Length - 1] = 0;
  9. }
  10. else
  11. {
  12. buffer = new byte[icmpP.Length];
  13. icmpP.CopyTo(buffer, 0);
  14. }
  15. int checksum = 0;
  16. for (int i = 0; i < buffer.Length; i += 2)
  17. {
  18. byte firstByte = buffer[i+1];
  19. byte secondByte = buffer[i];
  20. ushort word = secondByte;
  21. word = (ushort)((word << 8) + firstByte);
  22. checksum += word;
  23. }
  24. checksum = (checksum >> 16) + (checksum & 0xFFFF);
  25. checksum += (checksum >> 16);
  26. // Einerkomplement
  27. return (ushort)(~checksum);
  28. }
Die Originalimplementierung dieser Methode in C kann auf www.ping127001.com/pingpage/ping.html eingesehen werden. Da wir sowohl die Methode Ping als auch die Methode getICMPChecksum statisch deklariert haben, kann der Einsatz ohne Klasseninstanz erfolgen.
  1. bool result = PingService.Ping(192.168.1.1, 2000);
  2. if (result)
  3. {
  4. resultLabel.Text = "Zielhost ist erreichbar";
  5. }
  6. else
  7. {
  8. resultLabel.Text = "Zielhost ist nicht erreichbar";
  9. }
Fazit
Durch die in diesem Artikel dargestellte Implementierung des Ping-Befehls konnten Sie bereits einen kleinen Einblick in die Möglichkeiten des ICMP-Protokolls erlangen. Durch die Erweiterung des TCP-Headers wird es möglich, Statusinformationen mit einfachen Mitteln zu übertragen. Das .NET Framework bietet Ihnen in Form der Klasse Socket bereits vollständige Unterstützung in diesem Bereich.


Anzeige

Kommentare

Gravatar K8nsch 13.09.2008
um 17:13 Uhr
Wo ist denn die Methode ArrayList?
Ich finde sie auch nicht in diesen Text.

MfG K8nsch
#zitieren
Gravatar Sebastian Eschweiler 14.10.2008
um 19:36 Uhr
Bei ArrayList handelt es sich nicht um eine Methode, sondern um eine Klasse aus dem Namensraum System.Collection. Der Aufruf ArrayList() bezieht sich auf den gleichnamigen Konstruktor und dient zur Initialisierung des betreffenden Objektes. Weitere Informationen zur Klasse ArrayList sind im Microsoft Developer Network unter der folgenden URL zu finden:
http://msdn.microsoft.com/en-us/library/system.collections.arraylist.aspx

Viele Grüße

Sebastian Eschweiler
#zitieren
zurück zum Seitenanfang