TCP/IP Socket-Programmierung in C# - Höhere Socketklassen |
Geschrieben von: Kristian | ||||||||||||||||||||
Sonntag, den 12. März 2006 um 02:20 Uhr | ||||||||||||||||||||
Seite 3 von 7
TcpClient und TcpListenerWir haben erfahren das die .NET Socket Klasse bereits alle Möglichkeiten bereitstellt, die in der Netzwerkprogrammierung erforderlich sind. Doch neben bzw. auf der Socketklasse existieren noch eine Reihe weiterer Klassen die den Umgang mit Sockets noch weiter vereinfachen. Gemeint sind die bereits erwähnten, aber bisher nicht verwendeten Klassen TcpClient, TcpListener und UdpClient. Da wir uns bisher auf TCP beschränkt haben werfen wir zunächst einen Blick auf die TCP-Klassen. Nämlich auf TcpClient und TcpListener. Diese Klassen kapseln Teile der Socket-Klasse zu einer noch höheren Schicht und ermöglichen auf diese Art und Weise eine bequemere Handhabung bei der Erstellung von Netzwerkverbindungen. Der TcpClient Konstruktor erstellt automatisch für Sie einen TCP-Socket und etabliert implizit eine Verbindung zum angegebenen Server, welcher durch seine IP-Adresse und Port-Nummer identifiziert wird. Wie bei vielen anderen Abstraktionstechniken auch, ist die Verwendung höherer Klassen mit Einbußen in der Flexibilität und dem Leistungsumfang verknüpft. Allerdings müssen Sie sich nicht mehr mit vielen Details zu den Protokollen und Netzwerkschichten befassen. Jede TcpClient Instanz ist ein NetworkStream, da bekannterweise Socket Typen für das TCP/IP Stream Sockets sind. Die Klasse NetworkStream ist eine Unterklasse von Stream und letztlich ein abstrakter Stellvertreter für eine Sequenz an Bytes, also dem Netzwerkstrom der übertragen wird. Ähnlich wie wir Daten an den OutputStream der Konsole senden um Zeichen auf den Bildschirm auszugeben, schreiben wir zum NetworkStream wenn wir Daten über den Socket senden möchten. Um Daten zu empfangen müssen wir wiederrum vom NetworkStream lesen. using System; using System.IO; using System.Net; using System.Text; using System.Net.Sockets; namespace CodePlanet.Articles.ProgrammingSockets { /// <summary> /// Demonstration eines synchron agierenden TcpClienten. /// </summary> public class TCPClientClass { static void Main(string[] args) { if ((args.Length < 1) || (args.Length > 3)) { throw new ArgumentException("Parameters: <Word> [<Server>] [<Port>]"); } byte[] byteBuffer = Encoding.ASCII.GetBytes(args[0]); // Verwende den angegebenen Host, ansonsten nimm den lokalen Host string server = (args.Length == 2) ? args[1].ToString() : Dns.GetHostName(); // Verwende den angegebenen Port, ansonsten nimm den Standardwert 7 int servPort = (args.Length == 3) ? Int32.Parse(args[2]) : 7; TcpClient client = null; NetworkStream netStream = null; try { // Erzeuge neuen Socket der an den Server und Port gebunden ist client = new TcpClient(server, servPort); Console.WriteLine("Connected to server... sending echo string"); netStream = client.GetStream(); // Sende den kodierten string an den Server netStream.Write(byteBuffer, 0, byteBuffer.Length); Console.WriteLine("Sent {0} bytes to server...", byteBuffer.Length); int totalBytesRcvd = 0; // Anzahl der empfangenen Bytes int bytesRcvd = 0; // Anzahl der empfangenen Bytes beim letzten // Aufruf von Read() // Denselben string wieder vom Server empfangen while (totalBytesRcvd < byteBuffer.Length) { if ((bytesRcvd = netStream.Read(byteBuffer, totalBytesRcvd, byteBuffer.Length - totalBytesRcvd)) == 0) { Console.WriteLine("Connection closed prematurely."); break; } totalBytesRcvd += bytesRcvd; } Console.WriteLine("Received {0} bytes from server: {1}", totalBytesRcvd, Encoding.ASCII.GetString(byteBuffer, 0, totalBytesRcvd)); } catch (Exception e) { Console.WriteLine(e.Message); } finally { netStream.Close(); client.Close(); } } } } Wir haben in dem Beispiel TCPClientClass die Klasse TcpClient kennengelernt. Passend zum Echo-Clienten möchten wir uns nun den Echo-Server ansehen der die Klasse TcpListener nutzt um auf eingehende Verbindungen zu hören. Der folgende Code illustriert die Nutzung von TcpListener: try { // Erzeuge eine TcpListener Instanz um eingehende // Verbindungen anzunehmen listener = new TcpListener(IPAddress.Any, servPort); listener.Start(); } catch (SocketException se) { Console.WriteLine(se.ErrorCode + ": " + se.Message); Environment.Exit(se.ErrorCode); } Der listener wird mit der IPAddress Eigenschaft Any und dem Port initialisiert. Any veranlasst den Server an allen Netzwerk Schnittstellen auf Client Aktivitäten zu hören. Die Methode start() der Klasse TcpListener initialisiert den unterliegenden Socket, bindet diesen an den Endpunkt und startet den Abhörmodus. Im zweiten Teil des Echo-Servers nimmt der Server die eingehenden Verbindungen entgegen und sendet diese mithilfe der TcpClient Klasse wieder an den Client zurück. Wir erhalten eine entsprechende Instanz der TcpClient Klasse über die Methode AcceptTcpClient. TcpClient client = null; NetworkStream netStream = null; try { // Generiere eine Client Verbindung client = listener.AcceptTcpClient(); netStream = client.GetStream(); Console.Write("Handling client - "); int totalBytesEchoed = 0; while ((bytesRcvd = netStream.Read(rcvBuffer, 0, rcvBuffer.Length)) > 0) { netStream.Write(rcvBuffer, 0, bytesRcvd); totalBytesEchoed += bytesRcvd; } Console.WriteLine("echoed {0} bytes.", totalBytesEchoed); // Schließe den Socket. Wir haben den Clienten erfolgreich abgewickelt netStream.Close(); client.Close(); } catch (Exception e) { Console.WriteLine(e.Message); netStream.Close(); } Die vollständige Version des Echo-Servers finden Sie im Anhang. User Datagram Protocol (UDP)Neben TCP ist auch das UDP Teil der TCP/IP-Protokollfamilie. Man könnte den Eindruck gewinnen dass das UDP keine wichtige Rolle in der Netzwerkkommunikation spielt, schließlich haben wir uns bisher nur auf TCP konzentriert. Dies ist jedoch nicht der Fall. Deshalb möchten wir nun das UDP etwas genauer unter die Lupe nehmen. UDP unterscheidet sich in einigen wesentlichen Punkten von TCP. Im Gegensatz zu TCP wird nicht erst mittels Handshaking eine Verbindung zum Gegenüber aufgebaut, sondern die Daten werden sofort an die Gegenstelle geschickt. Das Protokoll ist im Gegensatz zu TCP also nicht verbindungsorientiert. Diese Form der Nachrichtenübermittlung ähnelt dem Versenden einer E-Mail. Das Protokoll garantiert nicht das ein einmal gesendetes Paket auch ankommt, oder dass Pakete in der gleichen Reihenfolge in der sie gesendet wurden beim Empfänger ankommen - eine Quittierung ist nicht vorgesehen. Die Kommunikationspartner können also nicht feststellen ob Pakete verloren gingen oder wie lange sie verzögert wurden. Auch eine Vervielfältigung von Paketen kann auftreten. UDP stellt lediglich eine Integritätsüberprüfung zur Verfügung, welche korrupte Daten in einem Paket erkennt und dieses anschließend aussortiert. Eine Anwendung, die UDP nutzt, muss daher gegenüber verloren gegangenen und umsortierten Paketen unempfindlich sein oder selbst entsprechende Korrekturmaßnahmen beinhalten. Ein weiterer bedeutender Unterschied liegt darin das UDP Nachrichtengrenzen einhält, da eine Nachricht stets die Größe eines UDP-Paketes hat. Das User Datagram Protocol definiert Pakete mit einer maximalen Größe von 65535 Bytes. 65507 Bytes sind davon nutzbar, der Rest wird für den UDP-Header sowie für den IP-Header benötigt.
UDP vereinfacht den Empfang von Nachrichten. Während TCP aufgrund der Fehlerkorrekturmechanismen Daten zunächst einmal immer in einem Puffer zwischenspeichert, wird eine UDP Nachricht bei Aufruf der Send Methode umgehend zur Weiterleitung an den Empfänger im Übertragungskanal eingereiht. Auf diese Weise ist es nicht möglich das beim Empfang der Daten die Methode Receive Teile ein und derselben Nachricht in zwei oder mehreren Aufrufen abarbeitet. Der Aufruf von Receive liefert also stets ein Datenpaket! Alle UDP Pakete werden in einer Queue (engl. Warteschlange) verwaltet. Anders als beispielsweise beim Stack, werden die Daten nach dem sogenannten FIFO Prinzip gespeichert. Dies unterscheidet sich grundsätzlich von TCP, in dem sich die Daten in einem aneinander gereihten Byte-Strom befinden. Die wesentlichen Vorteile von UDP liegen im einfachen Aufbau der Pakete und in der Art wie diese über das Netzwerk verschickt werden. Da vor Übertragungsbeginn nicht erst eine Verbindung aufgebaut werden muss, können die Hosts schneller mit dem Datenaustausch beginnen. Dies fällt vor allem bei Anwendungen ins Gewicht bei denen nur kleine Datenmengen ausgetauscht werden müssen. Der Overhead reduziert sich bei UDP um mehr als die Hälfte im Gegensatz zu TCP. Das User Datagram Protocol (UDP) ist ein verbindungsloses Protokoll, mit dem es möglich ist jederzeit Nachrichten an jedes beliebige Ziel zu versenden, ohne vorher erst für den Aufbau einer Verbindung sorgen zu müssen. UDP gibt keine Garantie auf die Einhaltung der Reihenfolge der Datagramme, ihre Zustellung oder die Vermeidung doppelter Nachrichtenzustellung. Multicastadressen und Broadcastadressen sind möglich. Neben dem Aspekt der Effizienz ist auch Flexibilität ein ausschlaggebender Punkt um sich eventuell für UDP zu entscheiden. Die einfache Struktur sowie der minimale Overhead von UDP ermöglichen eine flexiblere Anpassung der Anwendung an die gegebenen Erfordernisse. Die Klasse UdpClientDas .NET Framework stellt UDP nicht nur in der Socket Klasse zur Verfügung, sondern implementiert auch eine weitere aufliegende Klasse namens UdpClient. Im Gegensatz zu TCP ist hier natürlich kein Listener erforderlich. Es muss auch keine Verbindung hergestellt werden. Zwar stellt die Klasse ebenfalls eine Connect Methode bereit, jedoch dient diese nur der Voreinstellung der Empfängeradresse. Es wird keine Setup-Phase durchlaufen und es wird auch keine Verbindung mittels Drei-Wege-Handshake aufgebaut. Lange Rede, kurzes Programm. Nachfolgend sehen Sie den prinzipiellen Aufbau eines UDP-Clienten der ein Datenpaket sendet, welches anschließend von einem zweiten Clienten empfangen und ausgegeben wird: using System; using System.IO; using System.Net; using System.Text; using System.Net.Sockets; namespace CodePlanet.Articles.ProgrammingSockets { /// <summary> /// Beispiel für einen UDPClient. Ein Client sendet, /// der andere Client empfängt anschließend. /// </summary> public class UdpClientSample { public static void Main(string[] args) { try { // Generiere einen EndPoint mit einer Loopback-Adresse IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Loopback, 4201); // Instanziere UdpClient UdpClient sender = new UdpClient(); UdpClient receiver = new UdpClient(remoteIPEndPoint); sender.Connect(remoteIPEndPoint); // Bindet UdpClient nur an den Port string welcome = "Hello, how are you?"; byte[] sendPacket = Encoding.ASCII.GetBytes(welcome); sender.Send(sendPacket, sendPacket.Length); Console.WriteLine("Sent {0} bytes to the server...", sendPacket.Length); // Empfange den Echo string wieder byte[] rcvPacket = receiver.Receive(ref remoteIPEndPoint); Console.WriteLine("Received {0} bytes from {1}: {2}", rcvPacket.Length, remoteIPEndPoint, Encoding.ASCII.GetString(rcvPacket, 0, rcvPacket.Length)); // In Endlosschleife senden und empfangen while (true) { string input = Console.ReadLine(); if (input == "exit") break; sender.Send(Encoding.ASCII.GetBytes(input), input.Length); rcvPacket = receiver.Receive(ref remoteIPEndPoint); string data = Encoding.ASCII.GetString(rcvPacket, 0, rcvPacket.Length); Console.WriteLine(data); } Console.WriteLine("Stopping..."); sender.Close(); receiver.Close(); } catch (Exception e) { Console.WriteLine(e.ToString()); } } } } Bei einem verbindungslosen Protokoll, wie UDP, liest die Methode Receive das erste Datagramm aus der Warteschlange mit der unter Connect angegebenen Adresse aus. Falls das Datagramm größer ist, als ein als Parameter angegebener Buffer, wird nur ein Teil des Datagramms in den Zielpuffer geschrieben, der Rest geht verloren und es wird eine SocketException ausgelöst. Die im Codebeispiel dargestellte Methode Receive der Klasse UdpClient darf nicht mit der bereits bekannten, gleichnamigen Methode der Klasse Socket verwechselt werden. Am Parameter ist zu erkennen, dass es sich hierbei um eine andere Methode handelt, die einen IPEndPoint als Argument entgegen nimmt. Der UdpClient reserviert automatisch Speicher und gibt ein komplettes Datagramm als byte[] zurück. WebClientNeben den abstrakten Klassen TcpClient, TcpListener und UdpClient stellt das .NET Framework zahlreiche weitere nützliche Klassen zur Verfügung die zwar Sockets nutzen, dies jedoch vor dem Nutzer verbergen. Eine davon ist die Klasse WebClient aus dem Namensraum System.Net. Diese Klasse stellt gewöhnliche Methoden zum Versenden und Empfangen von Daten von einer ausgewählten Quelle zur Verfügung. Diese höheren Methoden, wie beispielsweise DownloadFile und UploadFile, sind relativ einfach konzipiert und verfügen über keine integrierte Unterstützung für asynchrone Kommunikation oder Authentifizierung. Jedoch ist es mit ihnen möglich auf sehr unspektakuläre Art und Weise Daten über Http zu laden. Das nachfolgende Beispiel lädt ein Bild von der Seite www.codeplanet.eu und speichert es lokal auf der Festplatte. using System; using System.Net; using System.Collections.Generic; using System.Runtime.InteropServices; // Für P/Invoke using System.Text; using System.IO; namespace CodePlanet.Articles.ProgrammingSockets { /// <summary> /// Lädt Dateien über Http aus dem Internet. /// </summary> class HttpClient { private static void Main(string[] args) { // Aktuellen Pfad holen ModFilePath filepath = new ModFilePath(); if (args.Length > 1) { throw new ArgumentException("Parameters: [<Uri>]"); } string remoteUri = (args.Length == 2) ? args[0].ToString() : "http://www.codeplanet.eu/images/banners/codeplanet-eu-logo-simple.png"; int ix = remoteUri.LastIndexOf("/"); // In unserem Standard-Fall "sp-logo-sitemap.png" string localFileName = filepath.StartupPath + remoteUri.Substring(ix); WebClient client = new WebClient(); // Sende HTTP Header (Indentifikation als Mozilla Browser) client.Headers.Add("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)"); Console.WriteLine("Download from " + remoteUri + " to " + localFileName); // Download durchführen client.DownloadFile(remoteUri, localFileName); Console.WriteLine("Download finished."); } } // Empfängt den vollständig qualifizierten Pfad einer Datei die das // spezifizierte Module des laufenden Prozesses enthält. public class ModFilePath { [DllImport("kernel32.dll", SetLastError = true)] [System.Security.SuppressUnmanagedCodeSecurity] // DWORD GetModuleFileName( // HMODULE hModule, // LPTSTR lpFilename, // DWORD nSize // ); private static extern uint GetModuleFileName( [In] IntPtr hModule, [Out] StringBuilder lpFilename, [In] [MarshalAs(UnmanagedType.U4)] int nSize); public string StartupPath { get { // Empfange den Datei Pfad (path) StringBuilder sb = new StringBuilder(260); if (GetModuleFileName(IntPtr.Zero, sb, sb.Capacity) == 0) { throw new ApplicationException("Could not retrive module file path!"); } return Path.GetDirectoryName(sb.ToString()); } } } } Die Klasse HttpClient beherbergt die Main Methode die im Prinzip bereits alle notwendigen Aufrufe enthält die erforderlich sind um Daten, in diesem Fall ein Bild, aus dem Netz zu laden. Optional kann man auch eine eigene URI über die Kommandozeile als Parameter übergeben. Die Eigenschaft Header der Klasse WebClient setzt den Header des HttpClienten der der Identifizierung dient. Dieser Teil ist optional und kann auch weggelassen werden. Die entscheidende Zeile des Programms ist der Aufruf der Methode DownloadFile. Die Methode ist folgendermaßen deklariert: public void DownloadFile( string address, string fileName ); Neben der eigentlichen Adresse der zu ladenden Datei ist ein zweiter Parameter erforderlich, der den Dateinamen und den Zielpfad spezifiziert. Ohne explizite Angabe des Zielpfades in fileName speichert die Methode die Datei nicht im aktuellen Ordner. Da wir die Datei in der Regel an dem Ort speichern möchten von welchem aus wir das Programm starten, müssen wir den Quellcode um ein paar Zeilen erweitern. .NET 2.0 bietet keine vorgefertigte Methode an, den aktuellen Pfad des laufenden CMD Prozesses zu extrapolieren. Deshalb ist ein direkter Aufruf der WinAPI bzw. des Kernels erforderlich. Genauer gesagt ein Aufruf der Funktion GetModuleFileName. Die Funktion liefert den vollständigen Pfad der Datei, die das Module des aktuell laufenden Prozesses enthält. Auf diese Weise erhält unsere Methode DownloadFile stets den vollständigen Pfad des Programmes. Der Aufruf von Unmanaged Code ist bekannterweise nur unter Verwendung von P/Invoke möglich und hat den Nachteil das sie die Typsicherheit Ihres Programmes aufgeben, da das Programm nicht mehr in Partial-Trusted-Szenarien läuft. Siehe dazu auch P/Invoke Grundlagen. |
||||||||||||||||||||
Zuletzt aktualisiert am Donnerstag, den 02. Januar 2014 um 23:03 Uhr |
AUSWAHLMENÜ | ||||||||
|