TCP/IP Socket-Programmierung in C# Drucken
Benutzerbewertung: / 982
SchwachPerfekt 
Geschrieben von: Kristian   
Sonntag, den 12. März 2006 um 02:20 Uhr

Einführung

Als Ende der 1960er-Jahre die Vorgängerversion des heute bekannten Internets entstand, nämlich das ARPAnet, konnten sich die damals beteiligten Personen den Einfluss eines derartigen dezentralen Computernetzwerkes noch gar nicht vorstellen. Kein Wunder, diente es anfangs doch nur einigen amerikanischen Universitäten zu Forschungszwecken um ihre Computer zu vernetzen und so die Rechenzeit besser ausnutzen zu können. Das Internet entwickelte sich in der zweiten Hälfte des 20. Jahrhunderts kontinuierlich und neue Technologien brachten neue Formen dieses Computernetzwerkes hervor. Der Begriff des Internet etablierte sich Anfang der 1970er und das in der heutigen Form bekannte World Wide Web entstand erst Anfang der 1990er-Jahre am renommierten CERN, der Europäischen Organisation für Kernforschung.

Heute nutzen über eine Milliarde Menschen auf der Welt das Internet. Millionen von Computern sind mit diesem Netzwerk verbunden. Programme interagieren miteinander und über ihren Webbrowser können Sie durch das Netz surfen. Doch wie kommunizieren diese Programme über das Internet? Obwohl die Antwort abhängig von der Anwendung und dem Betriebssystem selbst ist, kann man mit großer Sicherheit sagen dass sie eine Socket-API verwenden. Die Grundlagen der Netzwerkprogrammierung mit TCP/IP und der Socket-API werden in dem Artikel durch Protokollgrundlagen und einschlägige Implementierungsmuster für typische Client-Server-Programme vertieft.

Dieser Artikel wird Ihnen einen Einblick in die TCP/IP Socket-Programmierung mit der Programmiersprache C# gewähren. Das Dokument behandelt die unabdingbaren Grundlagen, soll aber nicht die Funktionsweise des Internets selbst und dessen Protokolle im Detail erläutern. Das würde mit Sicherheit den Rahmen sprengen. Deshalb sei hier auf den technischen Artikel „Was ist TCP/IP?“ verwiesen, der Ihnen einige Grundlagen vermittelt und einen Animationsfilm bereitstellt. Ausführliche und fundierte Kenntnisse erhalten Sie unter anderem über die folgenden Lektüren: TCP/IP von W. Richard Stevens, TCP/IP Illustrated (englisch) und The TCP/IP Guide (englisch). Das zuletzt genannte Buch steht kostenlos als Online-Referenz auf der gleichnamigen Seite zur Verfügung.

Zielgruppe

Es wird vorausgesetzt, dass Sie sich mit der Programmiersprache C# und mit ihrer grundlegenden Syntax auskennen. Auch sollten Sie in der Lage sein die gelisteten Quellcodes zu kompilieren. Ziel des Artikels ist es, dem Programmierer ein fundiertes Wissen auf dem Gebiet der Netzwerkprogrammierung zu vermitteln, welches ihn in die Lage versetzt eigene Netzwerkanwendungen zu entwickeln. Er richtet sich an Programmierer und Softwareentwickler, die zum ersten Mal in Kontakt mit der Netzwerkprogrammierung kommen oder ihre rudimentären Kenntnisse auf diesem Gebiet vertiefen möchten. Der Artikel vermittelt Grundlagen ohne dabei auf abstrakte plattformspezifische Technologien, wie die Windows Communication Foundation (WCF), einzugehen. Am Ende dieses Tutorials finden Sie eine entsprechende Microsoft Visual Studio Projektmappe, die sämtliche Codebeispiele aus diesem Artikel und viele weitere Programme beinhaltet.

Neben C# sollte Ihnen auch das .NET Framework vertraut sein und der Begriff TCP nicht gänzlich unbekannt. Unabhängig davon werden im Dokument die erforderlichen theoretischen Grundlagen zum Internet und TCP/IP behandelt. Alle Beispielcodes beschränken sich auf die Konsole, da zusätzlicher Code für die GUI von den elementaren Codezeilen ablenken würde und nur unnötiger Ballast wäre. In der VS Projektmappe im Tutorial-Anhang finden Sie dennoch auch Windows Applikationen in der gewohnten Fensteransicht. Das vorliegende Dokument bezieht sich auf C# 3.0 und das Framework 4.0. Die verwendete Entwicklungsumgebung ist Visual Studio 2010.

Grundlagen zum Netzwerk, zu Paketen und Protokollen

Ein Computer-Netzwerk besteht aus Maschinen die verbunden sind über so genannte Kommunikationskanäle um Daten auszutauschen. Wir unterteilen diese Maschinen in Hosts und Router. Ihr Computer dürfte in aller Regel ein Host sein, denn auf ihm laufen Anwendungen, wie z.B. ein Internet-Browser. Im Gegensatz zum Host besteht die Aufgabe der Router darin Informationen durch das Netzwerk zu leiten. Daher die englische Bezeichnung Router, welches von routen also leiten abstammt. Doch weshalb brauchen wir diese Router? Warum können die Hosts ihre Daten nicht einfach direkt untereinander austauschen? Nun ja, das wäre eine äußerst impraktikable Lösung. Man müsste Millionen von Computern direkt miteinander verbinden. Nein, stattdessen verbinden sich einige Computer mit einem einzelnen Router welcher seinerseits wiederum mit anderen Routern verbunden ist. Auf diese Weise entsteht ein gut skalierbares Netzwerk welches mit verhältnismäßig wenigen Kommunikationskanälen auskommt. Das folgende vereinfachte Schema zeigt den prinzipiellen Aufbau dieses Netzwerks:

network structure

Das Internet ist ein so genanntes Teilstrecken-Netzwerk. Die Kommunikation damit geschieht nicht verbindungsorientiert, wie ein Telefonat, sondern paketorientiert. Das bedeutet, dass der Datenstrom in kleine Pakete aufgeteilt wird. Um die einzelnen Informationspakete erfolgreich über das Netzwerk transferieren zu können ist ein vereinheitlichendes Protokoll notwendig. Ein Netzwerkprotokoll (auch Netzprotokoll, Übertragungsprotokoll) ist eine exakte Vereinbarung, nach der Daten zwischen Computern ausgetauscht werden, die durch ein Netz miteinander verbunden sind. Die Vereinbarung besteht aus einem Satz von Regeln und Formaten, die das Kommunikationsverhalten der kommunizierenden Instanzen in den Computern bestimmen. Ein Protokoll ist vergleichbar mit einer Sprache, die eine Syntax und Semantik besitzt. Um kommunizieren zu können, muss die Anwendung in der Lage sein das erforderliche Protokoll zu verstehen. Viele Protokolle nutzen andere Protokolle, ähnlich wie man in manchen Sprachen Bestandteile anderer Sprachen finden kann.
Für das Internet ist das TCP/IP von eminenter Bedeutung. Es handelt sich dabei um eine ganze Familie von Netzwerkprotokollen. Ursprünglich wurde TCP als monolithisches Netzwerkprotokoll entwickelt, jedoch später in die Protokolle IP und TCP aufgeteilt. Das Internet Protokoll (IP) ist in der Vermittlungsschicht angesiedelt und für die Weitervermittlung von Paketen und die Wegewahl (Routing) zuständig. Das Transmission Control Protocol (TCP) baut auf dem IP auf und stellt eine Ende-zu-Ende-Verbindung her. Es ist Bestandteil der Transportschicht im OSI-Schichtenmodell. Neben TCP und IP ist auch das User Datagram Protocol (UDP) Teil dieser wichtigen Protokollfamilie. Im Unterschied zu TCP ist das UDP nur ein äußerst minimales, verbindungsloses Transportprotokoll welches nicht auf Zuverlässigkeit ausgelegt ist. Eingebettet als so genannte Layer (Schichten) bilden diese Protokolle eine Internetprotokollfamilie, die zusammen mit weiteren Protokollen die Basis für die Netzkommunikation im Internet bilden.

Unter TCP/IP und dem Internet Protokoll Version 4 (IPv4) wird eine Maschine anhand einer binären 32-Bit (4-Byte) Nummer identifiziert. Diese IP-Adresse ist sinngemäß vergleichbar mit der Adresse auf Ihren Briefumschlägen. Alle Computer, die mit dem Internet verbunden sind, erhalten automatisch eine individuelle IP-Adresse. IPv4 bietet einen Adressraum von etwas über vier Milliarden IP-Adressen, mit denen Computer und andere Geräte angesprochen werden können. In den Anfangstagen des Internet, als es nur wenige Rechner gab die eine IP-Adresse brauchten, galt dies als weit mehr als ausreichend. Aufgrund des unvorhergesehenen Wachstums des Internet und dem Einzug von mobilen Geräten, wie internetfähigen Smartphones, herrscht heute Adressenknappheit und so wurde das Internet Protokoll Version 6 (IPv6) mit einem deutlich größeren Adressraum von 128-Bit entwickelt. Wir werden uns deshalb in diesem Tutorial mit beiden Protokollversionen befassen müssen, sowohl mit der gängigen Internet Protokoll Version 4, als auch mit der neuen Version 6, mit einem Adressbereich von 128-Bit. Das .NET Framework implementiert seit der Framework Version 2.0 aber auch die Version 6 des Internet Protokolls. Die meisten Netzwerke und damit auch das Internet befinden sich heute in einem Umbruch, hin zu IPv6. Dieser Umbruch gestaltet sich sehr langsam und so wird auch heute noch vielerorts das Internet Protokoll in der Version 4 verwendet.

Neben der IP-Adresse spielt auch die Port-Nummer eine entscheidende Rolle. Mit Hilfe der Port-Nummer ist es möglich neben der Adresse selbst den Bereich näher einzugrenzen. Identifiziert die IP-Adresse das Gebäude selbst, so ordnet die Port-Nummer das Paket einem bestimmten Raum zu. Im Netzwerk sind die Räume stellvertretend für die Anwendungen die auf einem Host laufen. Dabei kann es sich beispielsweise um ihren Browser handeln. Die Port-Nummern bestehen im Gegensatz zu IP-Adressen nur aus 16-Bit und liegen dementsprechend in einem Integer Bereich zwischen 1 und 65535, wobei die 0 bereits reserviert ist.

Domain Name System (DNS)

Da der Mensch sich die im Internet Protokoll spezifizierten Adressen, z.B. 64.233.167.99, in aller Regel relativ schlecht einprägen kann und diese Adressen aufgrund der Netztopografie fluktuieren können, wurde ein Mechanismus entwickelt der Domainnamen direkt in die zugehörige IP-Adresse und weitere Informationen abbildet. Dies ist vergleichbar mit einem Telefonbuch, das die Namen der Teilnehmer in ihre Telefonnummer auflöst. So ist es möglich das Sie einen Namen, bestehend aus lateinischen Buchstaben, Ziffern und dem Bindestrich, in die Browserleiste eintippen und dieser automatisch, in die vom TCP/IP definierte Adresse transformiert wird. Dieses System, nämlich das Domain Name System, kurz DNS, ist eine Art Datenbank die für jede Domain die zugehörigen Protokolldaten gespeichert hat. Die hierarchische Datenbank ist weltweit auf tausende Server verteilt, die den Namensraum des Internets vollständig verwalten. Ändert sich beispielsweise die IP-Adresse von www.codeplanet.eu, so mappt das DNS den Namen automatisch auf die neue IP-Adresse. Ihr Browser kann die aktuelle IP-Adresse direkt über den Domainnamen bei einem DNS-Server abfragen.

Für die Verbindung zu einem DNS-Server, der es Ihnen ermöglicht in bekannter Weise im Internet zu surfen, benötigt ihr Browser zunächst ebenfalls eine IP-Adresse, nämlich die des DNS-Servers selbst. Diese wichtige IP-Adresse wird Ihrem System bei der Einwahl in das Internet automatisch vom Provider mitgeteilt. Die Adresse lässt sich unter Windows den Netzwerkverbindungsdetails entnehmen. Ihr Browser sendet summa summarum nach Eingabe eines Domainnamens zuerst eine Anfrage an einen DNS-Server, bekommt anschließend die IP-Adresse, die zu diesem Domainnamen gehört zugeschickt und kann dann die angeforderte Seite aufrufen. Als gewöhnlicher Internetnutzer brauchen Sie sich darum keine Gedanken zu machen, das alles erledigt Ihr Browser vollautomatisch im Hintergrund.

dns-resolve

Clienten und Server

Zur Übermittlung von Informationen über einen Kommunikationskanal sind zwei Stellen erforderlich. Zum Einen der Sender der den Kommunikationskanal öffnet indem er eine Gegenstelle anruft und zum Anderen der Empfänger der diesen Anruf entgegennimmt und antwortet. Ähnlich wie bei einem Telefongespräch differenziert man auch in der Netzwerkkommunikation und unterteilt die an der Kommunikation beteiligten Stellen, nämlich in den Client und den Server. Der Client (engl. Bez. für Klient) initiiert die Kommunikation während der Server auf Anfragen wartet und dann dem Clienten antwortet. Entscheidend hierbei ist das der Client die Adresse des Servers kennen muss, nicht aber umgekehrt. Ob ein Programm auf ihrem Rechner als Client oder als Server agiert entscheidet darüber wie das Programm die Socket API verwendet, um mit dem so genannten Peer zu kommunizieren. Für einen Server ist der Peer der Client und vice versa.

In einer echten Peer-To-Peer-Anwendung ist der Client auch gleichzeitig der Server. Bekannte P2P Programme, wie z.B. eMule senden und empfangen Daten über das Netzwerk und sind deshalb Client und Server zugleich. Dies wird meist mithilfe von Threads realisiert. Während sich der eine Thread um eingehende Anforderungen kümmert (agiert als Server), sendet ein anderer Thread ausgehende Verbindungsanforderungen (agiert als Client).

Neben der IP-Adresse des Servers, die der Client unbedingt kennen muss, fällt dem Port ebenfalls eine wichtige Rolle für eine geregelte Kommunikation über TCP zu. Die IANA weist deshalb bestimmten Diensten jeweils fest definierte Ports zu. So sind gängige Ports, wie beispielsweise Port 21 (FTP) und 80 (HTTP) bereits reserviert. Dennoch können sich Client und Server entsprechend arrangieren, so dass prinzipiell jede Port-Nummer verwendet werden kann. Dies setzt allerdings voraus, dass der Client die Port-Nummer einer bestimmten Anwendung zuordnen kann.

Was ist ein Socket?

Schlägt man im Lexikon nach so findet man folgende Erklärung:

Definition: Ein Socket ist ein Endpunkt einer bi-direktionalen Software-Schnittstelle zur Interprozess- (IPC) oder Netzwerk-Kommunikation zwischen zwei Programmen. Ein Socket ist gebunden an eine Port-Nummer, so dass die TCP Schicht die Anwendung identifizieren kann für die die Informationen bestimmt sind.

Was heißt das nun genau? Ein Socket kann man sich als "Steckdose" vorstellen. Maschinen können über diese Steckdose Zugang zum Netz erhalten. Im Prinzip ist ein Socket eine Abstraktion die es einer Anwendung ermöglicht sich in ein bestehendes Netzwerk einzuklinken um mit anderen Applikationen zu kommunizieren. Ein Socket kann wie eine Datei geöffnet, beschrieben, gelesen und geschlossen werden. Sockets bilden eine standardisierte Schnittstelle (API) zwischen der Netzwerk-Protokoll-Implementierung des Betriebssystems und der eigentlichen Applikation. Die erste Socket Implementierung wurde auf dem Betriebssystem FreeBSD vorgenommen und bekannt unter dem Namen Berkeley Sockets API.

Sockets unterscheiden sich je nachdem welcher Protokoll Suite sie angehören. Die wesentlichen Socket Typen für das TCP/IP sind Stream Sockets und Datagram Sockets. Stream Sockets kommunizieren über einen Zeichen-Datenstrom; Datagram Sockets über einzelne Nachrichten. Während Stream Sockets das Transmission Control Protocol verwenden, nutzen Datagram Sockets das User Datagram Protocol. Das nachfolgende Abbild illustriert sowohl TCP- als auch UDP-Sockets.

sockets protocol layer

Wie man anhand des Diagramms erkennen kann ist jeder Port einem bestimmten Socket des Hosts zugeordnet. Die auf dem Host laufenden Applikationen können auch auf denselben Socket zugreifen! Sockets setzen auf die Protokolle einer Transportschicht wie TCP oder UDP auf. Aus Sicht der Applikation ist ein Socket nichts anderes als ein Zugang zum Netzwerk bzw. Internet.

Sockets im .NET Framework

Das .NET Framework implementiert die klassische Windows Socket API, auch WinSock genannt und umschließt die C Implementierung zu einer höheren Abstraktionsschicht. Dieses hohe Maß an Abstraktion vereinfacht viele Programmierschritte, ist allerdings auch oft zu unflexibel. Microsoft stellt deshalb zwei Schichten im .NET Framework zur Verfügung. Die auf der WinSock Schicht aufliegende Socket Klasse, die im Prinzip alle wesentlichen Funktionen von WinSock 2.0 implementiert und zusätzlich auf dieser eine weitere Schicht die die höheren Socket Klassen, nämlich TcpClient, TcpListener und UdpClient bereitstellt.

Die wesentlichen Klassen zur Netzwerkprogrammierung sind in den Namespaces System.Net und System.Net.Sockets der .NET Klassenbibliothek zu finden. System.Net stellt eine einfache verwaltete API für viele Protokolle die von den heutigen Netzwerken verwendet werden zur Verfügung. Darunter befinden sich Klassen, wie Dns zur Auflösung von Adressen, Cookie für die einfache Handhabung von Cookies und IPAddress zur Manipulation von IP-Adressen. Im Namespace System.Net.Sockets befindet sich mitunter die Klasse Socket, welche die Berkeley Socket API implementiert sowie auch TcpClient für Client Verbindungen bei TCP Netzwerk Diensten. Das .NET Framework unterscheidet deutlich zwischen TCP und UDP indem es für beide Protokolle separate Klassen zur Verfügung stellt.

.NET und das Domain Name System

Wie wir in einem vorhergehenden Abschnitt erfahren haben, werden Domainnamen über das Domain Name System in die entsprechende IP-Adresse übersetzt und vice versa. Das .NET Framework stellt hierfür die Klasse Dns bereit, welche wiederum zahlreiche Methoden zur Auflösung der IP-Adresse und des Hostnamen zur Verfügung stellt. Sehen wir uns deshalb nun zunächst einmal ein vollständiges Kommandozeilenprogramm an um die Methoden der Klassen Dns und IPAddress nachzuvollziehen.

using System;
using System.Net;
using System.Net.Sockets;
 
namespace CodePlanet.Articles.ProgrammingSockets
{
    /// <summary>
    /// Das folgende Kommandozeilenprogramm illustriert einige
    /// Methoden der .NET Framework Klassen Dns und IPAddress.
    /// </summary>
    public class MyHost
    {
        public static void Main(string[] args)
        {
            // Gibt die localhost Daten aus
            try {
                Console.WriteLine("Local Host:\n");
                // Die Methode Dns.GetHostName() gibt den Namen der lokalen Maschine zurück 
                String localHostName = Dns.GetHostName();
                Console.WriteLine("\tHost Name: " + localHostName);
                PrintHostInfo(localHostName);
            } catch (Exception e) {
                Console.WriteLine(e.Message + "\n");
            }
 
            // Falls Argumente übergeben wurden, gebe die Host Informationen aus
            if (args.Length > 0) {
                foreach (String arg in args) {
                    Console.WriteLine(arg + ":");
                    PrintHostInfo(arg);
                }
            }
        }
 
        // Statische Helper-Funktion
        static void PrintHostInfo(String host)
        {
            try {
                IPHostEntry hostInfo;
 
                // Versuche die DNS für die übergebenen Host und IP-Adressen aufzulösen
                hostInfo = Dns.GetHostEntry(host);
 
                // Ausgabe des kanonischen Namens
                Console.WriteLine("\tCanonical Name: " + hostInfo.HostName);
 
                // Ausgabe der IP-Adressen
                Console.Write("\tIP Addresses: ");
 
                foreach (IPAddress ipaddr in hostInfo.AddressList) {
                    Console.Write(ipaddr.ToString() + " ");
                }
 
                // Ausgabe der Alias-Namen für diesen Host
                Console.Write("\n\tAliases: ");
 
                foreach (String alias in hostInfo.Aliases) {
                    Console.Write(alias + " ");
                }
 
                Console.WriteLine("\n");
            } catch (Exception) {
                Console.WriteLine("\tUnable to resolve host: " + host + "\n");
            }
        }
    }
}

Neben dem eigentlichen Namen des Hosts, dem so genannten CNAME (engl. für canonical name) werden auch eventuelle Alias-Namen ausgegeben. Mit Einführung des .NET Framework 2.0 wurde die ursprüngliche Methode Resolve() der Klasse Dns als obsolet (veraltet) markiert. Abgelöst wird die Methode durch die Methode GetHostEntry(). Die Methode GetHostEntry() ist zweifach überladen und löst den Host-Namen oder die IP-Adresse zu einer IPHostEntry Instanz auf. Bei IPHostEntry handelt es sich um eine Container Klasse die von einigen Dns Methoden zurückgegeben wird. Anschließend werden zwei Eigenschaften von IPHostEntry ausgenutzt um eine Liste mit IP-Adressen und Alias-Namen auszugeben. Schlägt die Auflösung zuvor fehl so wird eine entsprechende Exception ausgelöst.

Unser kleines Programm beinhaltet bereits einige Klassen und Methoden die in der Netzwerkprogrammierung von Bedeutung sind. Von Sockets ist aber weit und breit nichts zu sehen. Der Grund dafür liegt darin begründet, dass die Methoden der Klasse Dns ihr Zusammenspiel mit der .NET Socket-Klasse verbergen. Lassen Sie uns nun endlich den Kern jeder Netzwerkprogrammierung kennenlernen und einen detaillierten Blick auf die .NET Socket-Klasse werfen.

Die .NET Socket-Klasse

Da es sich bei System.Net.Sockets.Socket um einen Wrapper um die Berkeley Sockets Schnittstelle handelt, deckt die Klasse nahezu alle Funktionalitäten für die Socket-Programmierung ab. Die Komplexität der WinSock API ist aufgrund ihres Umfangs relativ hoch. Wir beschränken uns daher zunächst einmal auf das Senden und Empfangen von Daten. Doch bevor wir Daten senden und empfangen können müssen wir wissen wie wir ein Socket Objekt erstellen können?

Die Socket Klasse stellt einen Konstruktor zur Verfügung um eine neue Socket Objekt-Instanz zu initialisieren. Die Parameterliste sieht wie folgt aus:

public Socket(
   AddressFamily addressFamily,
   SocketType socketType,
   ProtocolType protocolType
);

Um ein Socket Objekt zu instanzieren müssen 3 Argumente (allesamt Enumerationen) übergeben werden.

  • Die AddressFamily spezifiziert das Adressierungschema das eine Instanz der Socket Klasse benutzen kann. Das kann z.B. InterNetwork sein welches das IPv4 implementiert oder InterNetworkV6 für IPv6 oder Unix für Unix Adressen.
  • Als zweiter Parameter wird der SocketType übergeben. Die häufig verwendeten Typen sind die Stream und Datagram Sockets. Aber auch Raw Sockets kommen oft zum Einsatz.
  • Der dritte und letzte Parameter ist ProtocolType und spezifiziert den verwendeten Protokolltyp. Mögliche Argumente sind unter anderem das TCP, UDP oder IP.

Wir erzeugen nun einmal einen Stream Socket. Wie wir wissen verwenden Stream Sockets das Transmission Control Protocol (TCP) welches auf dem Internetprotokoll (IP) aufbaut. Unser Konstruktoraufruf sieht dann so aus:

Socket sock = new Socket(AddressFamily.InterNetwork, 
                         SocketType.Stream, 
                         ProtocolType.Tcp);

Nachdem wir eine Socket Instanz erzeugt haben können wir zu einem entfernten Server verbinden indem wir die Connect() Methode der Socket Klasse aufrufen. Doch bevor wir einen neuen Kommunikationskanal etablieren können erinnern wir uns an die dafür notwendigen Daten. Erinnern Sie sich? Exakt, wir benötigen eine IP-Adresse und eine Port-Nummer. Wir bedienen uns deshalb einer Klasse im Namensraum System.Net, nämlich der Klasse IPEndPoint. Diese repräsentiert einen Netzwerk Endpunkt bestehend aus einer IP-Adresse und einer Port-Nummer. Die Klasse stellt zwei Konstruktoren zur Verfügung:

public IPEndPoint(long, int);
public IPEndPoint(IPAddress, int);

Im ersten Konstruktor wird die IP-Adresse als long Typ (__int64) übergeben. Der C# Datentyp long, intern klassifiziert als System.Int64, umfasst 8 Byte.

Anmerkung: Aufgrund des bevorstehenden langsamen Wandels des Internet Protokolls von Version 4 auf Version 6 hat Microsoft vorgesorgt und mit dem Release des .NET Frameworks 2.0 IPv6 eingeführt.

Sie fragen sich in diesem Augenblick vielleicht was passieren würde wenn wir statt InterNetwork das InterNetworkV6 in der AddressFamily angeben würden? Wie kann dann ein 128-bit großer Wert einer IPv6 Adresse als Argument übergeben werden? Die Konstruktoren der Klasse IPEndPoint akzeptieren doch nur 64-bit?! Sie haben recht. Die IPv6 Adressen umfassen 128-bit also 2^128 Adressen. Jedoch dienen nur die ersten 64 Bit der IPv6-Adresse üblicherweise der Netzadressierung. Entspricht die Adresse der so genannten hexadezimalen Notation mit Doppelpunkten (engl. colon-hexadecimal notation) so sind nur die ersten 64 Bit erforderlich. Beispielsweise wären das bei der IPv6 Adresse 3FFE:FFFF:7654:FEDA:1245:BA98:3210:4562 dann die vier 16-bit Gruppen 3FFE:FFFF:7654:FEDA.

Zurück zur Internet Protokoll Version 4 deren Adressen "nur" 32-bit umfassen. Eine Adresse als long Typ, z.B. die IP-Adresse 3232235535, zu übergeben ist in der Praxis nicht üblich. Aufgrunddessen wird normalerweise der zweite Konstruktor aufgerufen. Dieser Konstruktor erwartet ein Objekt der Klasse IPAddress. In der Regel wird eine neue Instanz der Klasse IPAddress über die Methode Parse() aufgerufen. Damit ist es möglich eine IP-Adresse, die aus 4 Oktetts in der Form xxx.xxx.xxx.xxx besteht, als Argument vom Typ string zu übergeben. Das ist die allgemein gängige Methode eine IP-Adresse anzugeben. Die oben genannte IP-Adresse vom Typ long geschrieben in der "dotted-quad notation" ist 192.168.0.16.

Beide Konstruktoren erwarten als zweites Argument einen Integer. Dabei handelt es sich um die Port-Nummer. Sie finden die Port-Nummern für verschiedene Dienste unter anderem auf Wikipedia unter dem Stichwort Port (Protokoll).

Im folgenden Beispiel ist uns die IP-Adresse bekannt. Wir nutzen die Methode Parse() und wollen zu einem Webserver Verbindung aufnehmen. Dazu benutzen wir das bereits zuvor instanziierte Socket Objekt sock:

const int Port = 80;
const string IPv4 = "232.128.10.1";
 
IPAddress ipo = IPAddress.Parse(IPv4);
IPEndPoint ipEo = new IPEndPoint(ipo, Port);
 
// Konstruktor: 
// public void Connect(
//    EndPoint remoteEP
// );
sock.Connect(ipEo);

Nachdem wir mit Hilfe der IP-Adresse und der Port-Nummer ein neues Objekt der Klasse System.Net.IPEndPoint erzeugt haben können wir dieses an die Socket Methode Connect() übergeben. Die Verbindung wird aufgebaut.

Die IP-Adressen von Internetseiten bekommt der Nutzer nur selten zu sehen. Stattdessen nutzt man die Domain Namen. In unserem ersten Programm haben wir einige Methoden und Klassen zur Auflösung lokaler Adressen verwendet. Darunter System.Net.Dns und System.Net.IPHostEntry. Die Klasse Dns empfängt Informationen zu den gegebenen Hosts vom Internet Domain Name System (DNS) um so an die entsprechend zugeordneten IP-Adressen zu gelangen. Lassen Sie uns nun die gewonnenen Informationen kombinieren um die Domain codeplanet.eu aufzulösen und anschließend eine Verbindung herzustellen. Zuvor geben wir die IP-Adressen auf die Konsole aus.

using System;
using System.Net;
using System.Net.Sockets;
 
namespace CodePlanet.Articles.ProgrammingSockets
{
    /// <summary>
    /// Das folgende Programm löst die übergebene Domain auf, stellt
    /// eine Verbindung zu dieser her und schließt diese dann wieder.
    /// </summary>
    public class DomainTests
    {
        static void Main(string[] args)
        {
            try {
                Socket sock = null;
                string host = "codeplanet.eu";  // Uri
                int port = 80;
 
                IPHostEntry hostEntry = Dns.GetHostEntry(host);
                IPAddress[] ipAddresses = hostEntry.AddressList;
 
                Console.WriteLine(host + " is mapped to the IP-Address(es): ");
 
                // Ausgabe der zugeordneten IP-Adressen
                foreach (IPAddress ipAddress in ipAddresses) {
                    Console.Write(ipAddress.ToString());
                }
 
                // Instanziere einen Endpunkt mit der ersten IP-Adresse
                IPEndPoint ipEo = new IPEndPoint(ipAddresses[0], port);
 
                // IPv4 oder IPv6, Stream Socket, TCP
                sock = new Socket(ipEo.AddressFamily,
                                  SocketType.Stream,
                                  ProtocolType.Tcp);
 
                // Öffne eine Socket Verbindung
                sock.Connect(ipEo);
 
                // Prüfe ob eine Verbindung besteht?
                if (sock.Connected) {
                    Console.WriteLine(" - Connection established!\n");
                    // Socket schließen und Ressourcen freigeben
                    sock.Close();
                }
            } catch (Exception e) {
                Console.WriteLine(e.Message);
            }
        }
    }
}

Die Funktionsweise des Programms dürfte bekannt sein. Lediglich zwei kleine Änderungen haben sich eingeschlichen. Wir geben im Konstrukturaufruf des Sockets nicht mehr explizit die Adressfamilie an sondern nutzen stattdessen die Eigenschaft AddressFamily der Klasse IPEndPoint. Diese liefert implizit die entsprechende Adressfamilie zurück zu der dieser Endpunkt gehört. Auf diese Weise lässt sich eine Ausnahme in Folge einer inkompatiblen Adressfamilie vermeiden. Schließlich könnte es sein das Dns.GetHostEntry() eine IPv6 Adresse zurückgibt.

Seit dem Service Pack 2 von Windows Vista gibt die Methode GetHostEntry nur noch die Adressfamilie InterNetworkV6 bei lokalen Adressen zurück, so dass es bei falschem Socketaufruf zu der Ausnahme "Es wurde eine Adresse verwendet, die mit dem angeforderten Protokoll nicht kompatibel ist" kommt. Ändern Sie die Adressfamilie in diesem Fall explizit in InterNetworkV6.

Zusätzlich nutzen wir eine weitere Eigenschaft. Diesmal eine der Klasse Socket, nämlich Connected. Diese liefert den boolschen Wert true zurück falls eine Verbindung etabliert wurde. Andernfalls wird false zurückgegeben.


Daten senden und empfangen

Bisher haben sich alle Programme darauf beschränkt Hosts aufzulösen, Sockets zu öffnen, anschließend wieder zu schließen oder einfache Informationen zu IP-Adressen auszugeben. Nun führen wir die Sockets ihrem eigentlichen Zweck zu und werden mit ihrer Hilfe Daten senden und empfangen.

Um Daten synchron über das Netzwerk zu senden stellt die Socket Klasse die Methode Send bereit. Die Methode Send ist mehrfach überladen und nimmt unterschiedliche Kombinationen an Parametern entgegen. Nachfolgend werden alle Send Methoden des .NET Frameworks 2.0 aufgelistet:

public int Send(byte[] buffer);
public int Send(byte[] buffer, SocketFlags socketFlags);
public int Send(byte[] buffer, int size, SocketFlags socketFlags);
public int Send(byte[] buffer, int offset, int size, SocketFlags socketFlags);

Eines haben alle vier Implementierungen gemein. Sie erwarten ein byte[] Array das die Daten beinhaltet die übertragen werden sollen. Zudem geben alle Methoden die Anzahl der gesendeten Bytes als Integer zurück. Die Methoden sind gestaffelt und erweitern die Parameterliste mit jeder Überladung um einen neuen Parameter. Wir wenden uns deshalb gleich der vierten Überladung zu.

public int Send(byte[] buffer, int offset, int size, SocketFlags socketFlags);

Während der erste Parameter buffer die Daten selbst beinhaltet, gibt der Integer Parameter offset die Stelle im byte[] buffer an ab welchem begonnen wird die Daten zu senden. Der zweite Integer Parameter size gibt die Anzahl der zu sendenden Daten an. Als letzter Parameter spezifiziert socketFlags die bitweise Kombination der System.Net.Sockets.SocketFlags Enumerationen Member. Das sind unter Anderen None (keine besonderen Flags beim Aufruf verwenden) und DontRoute (sende ohne die Nutzung von Routing Tabellen). In der Regel werden keine besonderen Flags bei gängigen Übertragungen gesetzt.

Neben der Methode Send benötigen wir noch die Methode Receive um Daten zu empfangen. Receive ist aufgrund der Symmetrie ebenfalls vierfach überladen.

public int Receive(byte[] buffer); 
public int Receive(byte[] buffer, SocketFlags socketFlags); 
public int Receive(byte[] buffer, int size, SocketFlags socketFlags); 
public int Receive(byte[] buffer, int offset, int size, SocketFlags socketFlags);

Ein guter Weg festzustellen ob sich nach einem Aufruf der Methode Receive Daten in der Warteschlange befinden ist die Eigenschaft Available. Bei Stream Sockets sind die verfügbaren Daten die totale Anzahl an zu lesenden Daten im Puffer (engl. Buffer). Bei Datagram Sockets repräsentiert Available die erste Nachricht im Buffer. Falls sich keine Daten in der Warteschlange befinden wird 0 zurückgegeben.

Die beiden Methoden Send und Receive sind elementare Eigenschaften eines Socket und stehen sowohl unter TCP, als auch unter UDP zur Verfügung. Je nach eingesetztem Protokoll kann ihr Verhalten voneinander abweichen.

Client-Server Programmierung in .NET

Das Internet wird oft auch als ein Client-Server-System bezeichnet. Ihr Computer ist in der Regel der Client während die entfernten Rechner, die die Daten liefern meist die Rolle des Servers übernehmen. Wir haben bereits in einem vorangegangenen Abschnitt erfahren worin der genaue Unterschied zwischen einem Clienten und einem Server liegt.

clientserver

Die Server Dienste laufen auf fest definierten Ports, die bekanntermaßen dem Clienten bekannt sein müssen. Der Client startet die Kommunikation mit dem Server der passiv darauf wartet kontaktiert zu werden. Die Kommunikation zwischen Client und Server ist abhängig vom Dienst, d. h. der Dienst bestimmt, welche Daten zwischen beiden ausgetauscht werden. Der Server ist in Bereitschaft, um jederzeit auf die Kontaktaufnahme eines Clients reagieren zu können. Im Unterschied zum Client, der aktiv einen Dienst anfordert, verhält sich der Server passiv und wartet auf Anforderungen. Die Regeln der Kommunikation für einen Dienst (Format, Aufruf des Servers, und die Bedeutung der zwischen Server und Client ausgetauschten Daten), werden durch ein Protokoll festgelegt, und das Protokoll ist spezifisch für den jeweiligen Dienst.

Transmission Control Protocol (TCP)

Bevor ein Softwareentwickler ernsthaft darüber nachdenken kann Client-Server-Anwendungen zu entwickeln, muss er sich mit dem verwendeten Transportprotokoll auseinandersetzen. Er muss wissen, welche Vor- und Nachteile das verwendete Protokoll hat und natürlich wie das Protokoll funktioniert. Sie haben im Abschnitt Grundlagen bereits ein paar Informationen über das TCP und UDP erhalten, diese Kenntnisse sollen nun vertieft werden. Wir beginnen mit dem wichtigsten Transportprotokoll für das Internet, dem TCP. Das Transmission Control Protocol (TCP) (zu dt. Übertragungssteuerungsprotokoll) ist eine Vereinbarung (Protokoll) darüber, auf welche Art und Weise zwei Kommunikationspartner miteinander kommunizieren. Alle Betriebssysteme moderner Computer beherrschen TCP und nutzen es für den Datenaustausch mit anderen Rechnern. Das Protokoll ist ein zuverlässiges, verbindungsorientiertes, paketvermittelndes Transportprotokoll in Computernetzwerken. Es ist in Schicht 4 des OSI-Referenzmodells angesiedelt. Das TCP unterscheidet deutlich zwischen Client- und Server-Diensten.

Das Transmission Control Protocol (TCP) ist ein verbindungsorientiertes Protokoll, welches auf Zuverlässigkeit ausgelegt ist und einen bidirektionalen, byte-orientierten Datenstrom zwischen zwei Endpunkten etabliert. Die Daten der Pakete werden beim Empfänger in einem Puffer in der richtigen Reihenfolge zu einem Datenstrom zusammengefügt und doppelte Pakete verworfen. TCP prüft die Integrität der Daten und sendet verloren gegangene Pakete erneut zu.

Das .NET Framework stellt die Klassen TcpClient und TCPListener zur Verfügung, die sich in der Abstraktionsschicht eine Stufe über der Socket Klasse befinden und auf einfache Art und Weise die Implementierung einer Client-Server-Anwendung mit TCP ermöglichen. Bevor wir diese beiden Klassen jedoch verwenden, werden wir einen einfachen Echo-Server mit Hilfe der bereits bekannten und funktionelleren Socket-Klasse realisieren.

Ein Echo-Server ist, wie sein Name schon andeutet, ein Server der nichts anderes macht als die ihm zugesandten Daten wieder an den Clienten zurück zu schicken, so wie die Flächen von Felswänden Schallwellen verzögert reflektieren. Da der Server indirekt die Spielregeln in der Kommunikation über TCP bestimmt - er bietet an fest definierten Ports seine Dienste an - beginnen wir mit der Erstellung eines TCP-Echo-Servers. Ein Standard Echo Dienst läuft auf dem Port 7. Mit den uns bekannten Funktionen lässt sich der TCP-Server bereits zu 95% umsetzen. Lediglich zwei weitere Methoden der Socket Klasse werden benötigt um das Programm zu komplettieren. Die erste Methode ist

public void Bind(
   EndPoint localEP
);

Der Aufruf von Bind assoziiert den Socket mit einem bestimmten Endpunkt. Die Methode wirft neben einer SocketException auch eine ObjectDisposedException. Die Methode Bind kann sowohl bei verbindungsorientierten als auch bei verbindungslosen Protokollen verwendet werden. Nachdem der Socket mit einem Endpunkt assoziiert wurde, kann die zweite elementare Methode namens Listen aufgerufen werden.

public void Listen(
   int backlog
);

Listen ändert den Status des Sockets, so dass an diesem auf eingehende TCP Verbindungen gehorcht (engl. listen) wird. Der backlog Parameter spezifiziert die maximale Anzahl an eingehenden Verbindungen die in die Warteschlange eingereiht werden können. Um die maximal unterstützte Anzahl an eingehenden TCP Verbindungen die ihr System zulässt festzulegen, können Sie auf die Enumeration MaxConnections zurückgreifen. Diese kann über die Methode Socket.SetSocketOption gesetzt werden. Der TCP-Echo-Server könnte zum Beispiel so aussehen:

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Net.Sockets;
 
namespace CodePlanet.Articles.ProgrammingSockets
{
    /// <summary>
    /// Ein synchroner TCP-Server, der alle empfangenen Daten
    /// wieder umgehend an den Clienten zurück sendet.
    /// </summary>
    public class TCPEchoServerSocket
    {
        public static void Main(string[] args)
        {
            if (args.Length > 1) {
                throw new ArgumentException("Parameters: [<Port>]");
            }
 
            int servPort = (args.Length == 1) ? Int32.Parse(args[0]) : 7;
 
            Socket servSock = null;
 
            try {
                // Verwende explizit IPv6
                servSock = new Socket(AddressFamily.InterNetworkV6,
                                      SocketType.Stream,
                                      ProtocolType.Tcp);
 
                // Assoziiert den Socket mit einem lokalen Endpunkt
                servSock.Bind(new IPEndPoint(IPAddress.IPv6Any, servPort));
 
                // Versetzt den Socket in den aktiven Abhörmodus 
                servSock.Listen(BACKLOG);
            } catch (SocketException se) {
                Console.WriteLine(se.ErrorCode + ": " + se.Message);
                Environment.Exit(se.ErrorCode);
            }
 
            byte[] rcvBuffer = new byte[BUFSIZE];
            int bytesRcvd;
 
            // Lässt den Server in einer Endlosschleife laufen
            for (; ; ) {
                Socket client = null;
 
                try {
                    client = servSock.Accept();
 
                    Console.Write("Handling client at " + client.RemoteEndPoint + " - ");
 
                    // Empfange bis der client die Verbindung schließt, das geschieht indem 0
                    // zurückgegeben wird
                    int totalbytesEchoed = 0;
                    while ((bytesRcvd = client.Receive(rcvBuffer, 0, rcvBuffer.Length,
                                                       SocketFlags.None)) > 0) {
                        client.Send(rcvBuffer, 0, bytesRcvd, SocketFlags.None);
                        totalbytesEchoed += bytesRcvd;
                    }
                    Console.WriteLine("echoed {0} bytes.", totalbytesEchoed);
 
                    client.Shutdown(SocketShutdown.Both);
                    client.Close();
                } catch (Exception e) {
                    Console.WriteLine(e.Message);
                    client.Close();
                }
            }
        }
 
        private const int BUFSIZE = 32;
        private const int BACKLOG = 5;
    }
}

Im ersten Teil des Programms wird der Socket initialisiert, gebunden und in den Listening Status versetzt. Scheitert eine Methode, wird eine SocketException ausgelöst, anschließend Exit aus der Klasse System.Environment aufgerufen die den aktuellen Prozess beendet und die Kontrolle an das Betriebssystem zurückgibt.

Im zweiten Teil wird der Server in eine Endlosschleife versetzt, so dass dieser ununterbrochen auf neue eingehende Verbindungen wartet. Die Methode Accept generiert einen neuen Socket indem sie synchron die anstehenden Verbindungsanfragen aus der Warteschlange des Listening Sockets extrahiert.

Ein weiteres Novum im Code ist die Methode Shutdown aus System.Net.Sockets.Socket. Wenn Sie einen verbindungsorientierten Socket verwenden, sollten Sie stets die Shutdown Methode aufrufen bevor Sie den Socket schließen. Das garantiert das alle Daten am verbundenen Socket gesendet und empfangen werden bevor der Socket geschlossen wird. Nachdem der TCP-Echo-Server alle Daten empfangen und umgehend an den Clienten zurück gesendet hat, gibt das Programm die Anzahl der Daten in Bytes mit WriteLine aus.

Paketsegmentierung

Da es sich beim IP um ein paketorientiertes Protokoll handelt, werden Anwendungsdaten, die über das Internet übertragen werden, in kleine TCP-/IP-Pakete aufgeteilt. Um beispielsweise mit einem 1460 Byte großen Nutzdatenfeld 10 Kilobyte Daten zu versenden, teilt man die Daten auf mehrere Pakete auf, fügt einen Header hinzu und versendet die TCP-Segmente. Dieser Vorgang wird Segmentierung genannt. Jedes Segment ist nummeriert, so dass die Pakete beim Empfänger wieder nach ihrer Reihenfolge sortiert werden können. Die Maximum Segment Size (MSS) definiert in einem Rechnernetz die maximale Anzahl von Bytes, die als Nutzdaten in einem TCP-Segment versendet werden können. Der Vorgang der Datenübertragung über TCP mit Nutzdaten beliebiger Länge kann durch eine Zerlegung in Segmente beschrieben werden. Die TCP-Segmente mit dem TCP-Kopfbereich werden im OSI-Schichtenmodell nach unten zur Vermittlungsschicht weitergereicht, dort in einem IP-Paket verpackt, anschließend in der Sicherungsschicht in einem Ethernet-Frame eingebettet, so dass das Paket seine Reise durch das Internet antreten kann.

Das nachfolgende Diagramm skizziert diesen Vorgang der Segmentierung. Ein Datenpaket mit vielen TCP-Nutzdaten wird abhängig von der MSS vor der Übertragung in mehrere kleine Pakete aufgeteilt.

segmentation

Das Lesen der Anwendungsdaten beim Empfänger geschieht deshalb nicht zwangsläufig in einem Schritt, da der Empfänger die Daten in Form von diskreten Paketen mit einer spezifischen Größe, nämlich der maximalen Segmentgröße (MSS), erhält. In der Vermittlungsschicht begrenzt zudem die Maximum Transmission Unit (MTU) die zu übermittelnde Paketgröße. Das bedeutet, dass ein einzelner Aufruf von Send oder zum Beispiel Write nicht unbedingt in einem Receive- bzw. Read-Aufruf endet. Das TCP-Protokoll hält sich nicht an irgendwelche Begrenzungen in den Nachrichten und kann deshalb vereinzelte Übertragungen nicht auseinanderhalten. Das Protokoll stellt lediglich sicher, dass alle Pakete ankommen und beim Empfänger wieder korrekt zusammengefügt werden. Ein einzelner Aufruf zum Senden von Daten über Send kann auch von der Gegenstelle in mehreren Receive-Aufrufen abgearbeitet werden. Denn der Server liest die eingehenden Daten nicht direkt aus dem Netzwerk sondern aus dem internen TCP-Buffer und dieser füllt sich in unregelmäßigen Abständen mit den Daten des Senders, ähnlich einem Staubecken, das sich mit Wasser füllt. Alle eingehenden TCP-Pakete werden sequentiell in eine Warteschlange eingereiht. Sobald die Methode Receive oder eine vergleichbare Methode zum Auslesen des TCP-Buffer's aufgerufen wird, liest die Methode die Daten aus dem Buffer, unabhängig davon ob bereits alle Daten vom Sender empfangen wurden. Deshalb kann eine Übertragung über TCP auch folgendermaßen aussehen:

nonsynchsendrec

Die ursprüngliche Einzelnachricht des Clienten empfängt der Server vielleicht in mehreren Bruchstücken, die er aus dem TCP-Buffer liest. Aus diesem Grunde wird in dem Beispiel kontinuierlich innerhalb einer Schleife die Methode Receive aufgerufen, wobei die Methode solange blockiert bis genügend Daten im TCP-Buffer zur Verfügung stehen um den allokierten Puffer zu füllen. Da der Echo-Server alle eingehenden Daten einfach an den Clienten zurück sendet, muss er sich für Nachrichtengrenzen nicht interessieren, d.h. er muss keine Kenntnis von der Nachricht haben. Der Server liest in einer Schleife kontinuierlich den TCP-Buffer aus und sendet die empfangenen Daten einfach wieder direkt an den Clienten zurück. Als Abbruchbedingung dient die Methode Receive, die bei einem verbindungsorientierten Socket den Rückgabewert 0 liefert, wenn die Gegenseite den Socket schließt. Sind beim Aufruf von Reveive keine Daten im Buffer, blockiert die Methode solange, bis ein mit Socket.ReceiveTimeout gesetzter Timeout abgelaufen ist. Nach Ablauf dieser Zeitspanne löst Receive eine SocketException aus. Standardmäßig ist der Eigenschaft ReceiveTimeout der Wert 0 zugeordnet, d.h. die Methode Receive blockiert solange bis entweder Daten ausgelesen werden können oder die Gegenseite den Socket schließt.

TCP ist ein Stream (dt. Strom) und sollte auch als kontinuierlicher Datenstrom behandelt werden. Versuchen Sie nicht bei einem NetworkStream die Methode Flush aufzurufen, diese Methode hat keine Wirkung auf einen ungepufferten Netzwerkstrom. Der TCP/IP-Stack im Betriebssystem entscheidet wann und wie Pakete gesendet werden. Grundlage bildet der sogenannte Nagle-Algorithmus. Mit dem Nagle-Algorithmus wird das Versenden von vielen kleinen TCP-Datenpaketen über IP-Netze verhindert. Zu diesem Zweck werden beim Nagle-Mechanismus die kleinen Datenpakete zuerst zwischengespeichert und zu einem größeren Datenpaket zusammen gefasst. Mit dieser Technik soll das Netz von unnötig vielen kleinen Datenpaketen entlastet werden. In Verbindung mit dem Nagle-Algorithmus greift noch ein zweiter Algorithmus, der die Größe des Übertragungsfensters bei der TCP-Verbindung steuert. In der Regel versendet der TCP/IP-Stack ein Segment aus dem TCP-Buffer umgehend nach Ihrem Send-Befehl, doch dafür gibt es weder eine Garantie, noch können oder sollten Sie dieses Verhalten auf der Ebene erzwingen. Unter Windows bewirkt der Nagle-Algorithmus eine Zwischenspeicherung der Pakete am Socket für bis zu 200 Millisekunden. Es ist in .NET möglich die Winsock Option TCP_NODELAY zu setzen und damit den Nagle-Algorithmus zu deaktivieren. Dies wird mit der Eigenschaft Socket.NoDelay erreicht.

Es gibt Wege und Methoden Nachrichten die über TCP gesendet werden zu unterscheiden. Eine Möglichkeit ist immer eine fest definierte Nachrichtengröße zu transferieren. Eine weitere besteht darin jeweils immer die zugehörige Nachrichtengröße mit der Nachricht selbst zu übertragen. Und schlussendlich kann man das Protokoll so erweitern das ein eigenes Begrenzungssystem die Nachrichten voneinander eindeutig separiert. Doch das soll nicht Bestandteil dieses Tutorials sein und ist für uns auch nicht explizit relevant, solange wir wissen was sich im Hintergrund tatsächlich abspielt und unseren Code entsprechend anpassen können. Falls Sie an weiteren Details zu diesem Thema interessiert sind, können Sie einen Blick auf den Artikel http://www.codeplanet.eu/tutorials/csharp/66-client-server-modell.html werfen. Dort erfahren Sie im Kapitel Netzwerkprotokolle mehr über die Übertragung diskreter Nachrichten über das TCP.

Nun fehlt nur noch der Client der Daten an den Port des Servers sendet, so dass diese vom Server wieder zurück an den Clienten gesendet werden können. Ich überlasse die Programmierung des Clienten Ihnen und verweise auf die bisher bekannten Klassen und Methoden. Sollten Sie Probleme bei der Umsetzung haben, können Sie einen Blick in den von mir programmierten TCP-Clienten werfen. Dieser befindet sich unter den anderen Projekten im Anhang. Eine Kommunikation mit Ihrem Clienten und dem oben angegebenen Server könnte so aussehen:

Microsoft Windows XP [Version 5.1.2600]
(C) Copyright 1985-2001 Microsoft Corp.

C:\>TCPClientSocket.exe Hallo!
Connected to server... sending echo string
Sent 6 bytes to server...
Received 6 bytes from server: Hallo!
und
Handling client at 192.168.0.1:1984 - echoed 6 bytes.

HTTP-GET-Anforderung senden

Zu den alltäglichen Übertragungen über das Transmission Control Protocol im Internet gehört die HTTP-GET-Anforderung. Das HTTP ist ein Protokoll zur Übertragung von Daten über ein Netzwerk. Webbrowser nutzen das Protokoll, um Webseiten aus dem Internet zu laden. Dazu sendet der Browser eine dem HTTP entsprechend regelkonforme Anfrage an den Webserver. Der Webserver erwartet eine solche Anfrage in der Regel auf dem Port 80. Im folgenden Beispiel werden wir eine HTTP-GET 1.1 Anforderung an einen angegebenen Host senden. Diese einfache Anfrage beantwortet der Webserver indem er die Standardseite als HTML-Code zurückgibt. Üblicherweise ist das die Startseite der Homepage.

using System;
using System.Net;
using System.Text;
using System.Net.Sockets;
 
namespace CodePlanet.Articles.ProgrammingSockets
{
    /// <summary>
    /// Das folgende Kommandozeilenprogramm illustriert wie Sockets
    /// dazu verwendet werden können Daten zu empfangen und zu senden.
    /// </summary>
    public class HTTPGet
    {
        public static void Main(string[] args)
        {
            string host;
            int port = 80;
 
            if (args.Length == 0) {
                // Falls kein Hostname als Argument übergeben wurde soll
                // der lokale Host als Standard genommen werden.
                host = Dns.GetHostName();
            } else {
                host = args[0];
            }
 
            string result = SocketSendReceive(host, port);
            Console.WriteLine(result);
        }
 
        // Initialisiert die Socketverbindung und gibt diese zurück
        private static Socket ConnectSocket(string server, int port)
        {
            Socket sock = null;
            IPHostEntry hostEntry = null;
 
            hostEntry = Dns.GetHostEntry(server);
 
            // Nutze die Eigenschaft AddressFamily von IPEndPoint um Konflikte zwischen
            // IPv4 und IPv6zu vermeiden. Gehe dazu die Adressliste mit einer Schleife durch.
            foreach (IPAddress address in hostEntry.AddressList) {
                IPEndPoint ipo = new IPEndPoint(address, port);
                Socket tempSocket = new Socket(ipo.AddressFamily,
                                               SocketType.Stream,
                                               ProtocolType.Tcp);
 
                tempSocket.Connect(ipo);
 
                if (tempSocket.Connected) {
                    sock = tempSocket;
                    break;
                } else {
                    continue;
                }
            }
            return sock;
        }
 
        // Die Methode sendet eine HTTP GET 1.1 Anfrage an den Server und
        // empfängt die Daten
        private static string SocketSendReceive(string server, int port)
        {
            Socket sock = null;
 
            // Die zu sendenden Daten
            string request = "GET / HTTP/1.1\r\nHost: " + server +
                             "\r\nConnection: Close\r\n\r\n";
            // Kodiere den string in Bytes
            Byte[] bytesSent = Encoding.ASCII.GetBytes(request);
            // Lege ein Byte Array an für die zu emfangenden Daten
            Byte[] bytesReceived = new Byte[4096];
 
            // Instanziere ein gültiges Socket Objekt mit den übergebenen Argumenten
            sock = ConnectSocket(server, port);
 
            if (sock == null)
                return ("Connection failed!");
 
            // Sende den HTTP-Request
            sock.Send(bytesSent, bytesSent.Length, SocketFlags.None);
 
            int bytes = 0;
            string page = "Default HTML page on " + server + ":\r\n";
 
            // Empfange die Daten und konvertiere sie
            do {
                bytes = sock.Receive(bytesReceived, bytesReceived.Length, SocketFlags.None);
                // kovertiere die Byte Daten in einen string
                page = page + Encoding.ASCII.GetString(bytesReceived, 0, bytes);
            } while (bytes > 0);
 
            // Unterbinde alle weiteren Send() und Receive() Aktivitäten am Socket
            sock.Shutdown(SocketShutdown.Both);
            sock.Close();
 
            return page;
        }
    }
}

Die statische Methode ConnectSocket dient lediglich der Socketerstellung und Rückgabe. Je nachdem ob es sich um eine IPv4 oder IPv6 Adresse handelt wird die entsprechende Adressfamilie übergeben. In der zweiten Methode SocketSendReceive wird der übergebene Socket dazu verwendet eine HTTP-GET-Anforderung zu senden.

Neu in dem Quellcode ist die Klasse Encoding aus dem Namespace System.Text. Diese dient der Enkodierung von Zeichen. Da das Socket Objekt stets Sequenzen von Bytes sendet und empfängt, muss der string mit der HTTP-Get 1.1 Anfrage zunächst in eine Bytefolge konvertiert werden. Dazu wird die Methode GetBytes der Klasse Encoding aufgerufen. Die Methode ist mehrfach überladen:

public virtual byte[] GetBytes(char[]);
public virtual byte[] GetBytes(string);
public virtual byte[] GetBytes(char[], int, int);
public abstract int GetBytes(char[], int, int, byte[], int);
public virtual int GetBytes(string, int, int, byte[], int);

Die Klasse Encoding beinhaltet neben dem verwendeten Member ASCII, auch die Zeichensätze UTF8 und Unicode.

In einem späteren Kapitel werden Sie mehr über das HTTP und den Umgang mit diesem wichtigen Protokoll erfahren.


TcpClient und TcpListener

Wir 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.

dotnet_socket_classes

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.

Bit 0 16 32 48 64
Quell-Port Ziel-Port Länge Prüfsumme Daten
UDP-Datagramm

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 UdpClient

Das .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.

WebClient

Neben 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.


Multicast und Broadcast

Bei allen TCP-Verbindungen handelt es sich um bidirektionale, byte-orientierte, zuverlässige Datenströme zwischen exakt zwei Endpunkten, in der Regel zwischen einem Server und einem Clienten. Diese Art der Kommunikation wird Unicast genannt.

Manchmal müssen Informationen an mehr als einen Empfänger gleichzeitig versendet werden. In derartigen Fällen müsste man bei Unicast explizit eine Kopie der Daten an jeden Empfänger senden. Diese Methode ist bei großen Datenmengen und vielen Empfängern allerdings ineffizient. Unicast verschwendet innerhalb des Netzwerks unnötig Bandbreite, weil multiple Kopien mehrfach versendet werden. Tatsächlich wird die maximale Datenrate und somit die Zahl der Empfänger von unserer Bandbreite bei der Internetverbindung begrenzt. Wenn beispielsweise ein Videoserver 1 Mbps Streams versendet und die zur Verfügung stehende Bandbreite nur 3 Mbps groß ist, können allerhöchstens 3 Benutzer simultan bedient werden.

Glücklicherweise stellt das Internet Protokoll in Version 4 und 6 Mechanismen bereit, die Bandbreite effektiver zu nutzen. Statt den Sender für die Duplizierung der Datenpakete verantwortlich zu machen, kann diese Aufgabe an das Netzwerk delegiert werden. Handelt es sich um paketorientierte Datenübertragung, findet die Vervielfältigung der Pakete an jedem Verteiler (Switch, Router) auf der Route im Netzwerk statt. Man unterscheidet zwischen vier wesentlichen Routingschemen.

  • Unicast liefert eine Nachricht an einen spezifizierten Knoten;
  • Broadcast liefert eine Nachricht an alle Knoten im Netzwerk;
  • Multicast liefert eine Nachricht an eine bestimmte Gruppe von Knoten im Netzwerk, die an dieser Nachricht interessiert sind;
  • Anycast liefert eine Nachricht an jeden Knoten innerhalb einer Gruppe von Knoten, typischerweise den an der Quelle nächstgelegenen.

Es existieren zwei unterschiedliche Punkt zu einer Gruppe Verbindungen, die auch Mehrpunktverbindungen genannt werden: Broadcast und Multicast. Ein Broadcast bzw. Rundruf in einem Computernetzwerk ist eine Nachricht, bei der Datenpakete von einem Punkt aus an alle Teilnehmer eines Netzes übertragen werden. In der Vermittlungstechnik ist ein Broadcast eine spezielle Form der Mehrpunktverbindung. Multicast bezeichnet hingegen eine Nachrichtenübertragung von einem Punkt zu einer Gruppe. Dabei werden die Datenpakete an eine sogenannte Multicast-Adresse gesendet, das Netzwerk liefert die Pakete anschließend an die Hosts aus, die sich bei dieser Adresse für den Empfang von Nachrichten eingetragen haben.

broad_and_multicast

Es ist nur UDP-Sockets gestattet Broadcast und Multicast zu nutzen, weil eine TCP-Verbindung technisch bedingt immer vom Typ Unicast ist. Da die herkömmlichen, im Internet eingesetzten Router in aller Regel nicht Broadcast- und Multicast-fähig sind, bleiben Multicast-fähige Teilnetze im Internet häufig isoliert und bilden so genannte Multicasting-Inseln. Diese Inseln können mittels Tunneling, also durch Kapseln eines Multicast-Pakets in ein Unicast-Paket, überbrückt werden und bilden so ein weltumspannendes Multicast-Netz, den MBONE.

Broadcast

Ein Broadcast wird in einem Computernetz vorwiegend verwendet, wenn die Adresse des Empfängers der Nachricht noch unbekannt ist. Das Broadcasting von UDP Datagrammen ist ähnlich wie das Unicasting von Datagrammen, mit der Ausnahme das statt einer normalen IP-Adresse (Unicast) eine spezielle Broadcast-Adresse verwendet wird. Die lokale Broadcast-Adresse 255.255.255.255 sendet eine Nachricht an jeden Host, der sich in demselben Netzwerk befindet. Dieses Ziel liegt immer im eigenen Netz und wird direkt in einen Ethernet-Broadcast umgesetzt. Lokale Broadcast Nachrichten werden niemals von Routern weitergeleitet. Das IP spezifiziert auch directed broadcasts. Das Ziel sind die Teilnehmer eines bestimmten Netzes. Aufgrund von Sicherheitsproblemen mit DoS-Angriffen wurde das voreingestellte Verhalten von Routern in RFC 2644 für directed broadcasts geändert. Router sollten directed broadcasts nicht weiterleiten, weshalb diese Art hier im Artikel nicht weiter besprochen wird.

Es existiert keine Broadcast-Adresse, mit der es möglich wäre Nachrichten an alle Hosts zu versenden. Würde es eine derartige Adresse geben, hätte das fatale Folgen auf jeden Host im Internet. Beim Versenden eines Datagramms, würde es im gesamten Netzwerk zu einer enormen Zahl an Verfielfältigungen, hervorgerufen durch die Router, kommen. Dies würde die Bandbreite in allen Netzen erheblich beeinträchtigen.

Innerhalb eines überschaubaren Netzwerks kann ein Broadcast allerdings sinnvoll sein. Oft wird er verwendet um Informationen innerhalb eines Spielenetzwerks zu versenden, wo sich alle Spieler innerhalb desselben Netzes bewegen. In C# ist der Code für Broadcast und Unicast derselbe. Hierzu muss lediglich die IP-Adresse durch die Broadcast-Adresse ausgetauscht werden. Sie können das im Beispiel UdpClientSample ausprobieren.

Multicast

Multicast ist die übliche Bezeichnung für IP-Multicast, das es ermöglicht, in IP-Netzwerken effizient Daten an viele Empfänger zur gleichen Zeit zu senden. Der Vorteil von Multicast besteht darin, dass gleichzeitig Nachrichten an mehrere Teilnehmer oder an eine geschlossene Teilnehmergruppe übertragen werden können, ohne dass sich beim Sender die Bandbreite mit der Zahl der Empfänger multipliziert. Der Unterschied zu Broadcast besteht darin, dass beim Broadcast Content verbreitet wird, den jeder mit der entsprechenden Empfangsausrüstung ansehen kann, beim Multicast dagegen eine vorherige Anmeldung bei dem Aussender des Contents vonnöten ist. Das passiert mit einer speziellen Multicast-Adresse. In IPv4 ist hierfür der Adress-Bereich 224.0.0.0 bis 239.255.255.255, in IPv6 jede mit FF00::/8 beginnende Adresse reserviert.

Bei der Übertragung über Ethernet werden die IPv4- bzw. IPv6-Multicastadressen auf bestimmte Pseudo-MAC-Adressen abgebildet, um bereits durch die Netzwerkkarte eine Filterung nach relevantem Traffic zu ermöglichen. Die Abbildung erfolgt dabei nach folgenden Regeln:

multicast_to_mac

In C# kommunizieren Anwendungen, die Multicast verwenden, in der Regel indem sie eine Instanz der Klasse UdpClient verwenden. Es ist wichtig zu wissen das es sich bei einem Multicast-Socket tatsächlich um einen UDP-Socket handelt, der über zusätzliche, spezifische Multicast-Attribute verfügt, die manuell beeinflußt werden können.

Mit den Informationen lässt sich zunächst eine Hilfsklasse implementieren, die eine gegebene IP-Adresse überprüft und true zurückliefert, wenn es sich um eine gültige Multicast-Adresse handelt. Wir beschränken uns hier auf IPv4.

using System;
using System.Collections.Generic;
using System.Text;
 
namespace CodePlanet.Articles.ProgrammingSockets
{
    public class MCIPAddress
    {
        // Überprüfe ob es sich um eine gültige IPv4 Multicast-Adresse handelt
        public static bool isValid(string ip)
        {
            try {
                int octet1 = Int32.Parse(ip.Split(new Char[] { '.' }, 4)[0]);
                if ((octet1 >= 224) && (octet1 <= 239))
                    return true;
            } catch (Exception e) {
            }
 
            return false;
        }
    }
}

Anschließend kann eine Information über UDP versendet werden. Bitte beachten Sie das einige Klassen, wie ItemQuote, hier nicht näher aufgeführt werden. Die Klasse repräsentiert ein Artikelobjekt, welches später kodiert (serialisiert) und in einem UDP-Paket versendet wird. Die fehlenden Klassen befinden sich im Dateianhang zu diesem Artikel.

using System;
using System.Net;
using System.Net.Sockets;
 
namespace CodePlanet.Articles.ProgrammingSockets
{
    public class SendUDPMulticast
    {
        public static void Main(string[] args)
        {
            if ((args.Length < 2) || (args.Length > 3))
                throw new ArgumentException("Parameter(s): <Multicast Addr> <Port> [<TTL>]");
 
            // Prüfe ob es sich um eine gültige Multicast-Adresse handelt
            if (!MCIPAddress.isValid(args[0]))
                throw new ArgumentException("Valid MC addr: 224.0.0.0 - 239.255.255.255");
 
            IPAddress destAddr = IPAddress.Parse(args[0]);  // Zieladresse
 
            int destPort = Int32.Parse(args[1]);    // Zielport
 
            int TTL;    // Time-to-live für das Datagramm
 
            if (args.Length == 3)
                TTL = Int32.Parse(args[2]);
            else
                TTL = 1;    // Standard TTL
 
            // Generiere einen neuen Artikel
            ItemQuote quote = new ItemQuote(1234567890987654L, "5mm Super Widgets",
                                            1000, 12999, true, false);
 
            Socket sock = new Socket(AddressFamily.InterNetwork,
                                     SocketType.Dgram,
                                     ProtocolType.Udp); // Multicast Socket
 
            // Setze TTL
            sock.SetSocketOption(SocketOptionLevel.IP,
                                 SocketOptionName.MulticastTimeToLive,
                                 TTL);
 
            // Textkodierer
            ItemQuoteEncoderText encoder = new ItemQuoteEncoderText();
 
            // Kodiere den Artikel mit dem Textkosierer in eine Bytesequenz
            byte[] codedQuote = encoder.encode(quote);
 
            // Generiere Endpunkt
            IPEndPoint endPoint = new IPEndPoint(destAddr, destPort);
 
            // Sende den kodierten Artikel als UDP-Paket
            sock.SendTo(codedQuote, 0, codedQuote.Length, SocketFlags.None, endPoint);
 
            sock.Close();
        }
    }
}

Es ist zu erkennen das es nur einen signifikanten Unterschied zwischen Unicast und Multicast gibt. Neben dem Umstand, dass nun zuerst die Adresse geprüft wird, wird auch eine sogenannte TTL gesetzt. Die Time-to-live oder TTL ist der Name eines Header-Felds des Internetprotokolls, das verhindert, dass unzustellbare Pakete endlos lange von Router zu Router weitergeleitet werden. Laut RFC 791 muss die TTL von Paketen auf jedem Router, den das Paket auf dem Weg vom Sender zum Ziel passiert, um mindestens 1 verringert werden. Sofern das Paket längere Zeit auf dem Router „hängt“, sollte das TTL-Feld pro Sekunde um 1 verringert werden. IP-Pakete mit einer TTL von 0 werden vom Router als Irrläufer verworfen und das Paket geht verloren.

Mit der Angabe der TTL können wir also die Lebenszeit des Paketes einschränken und somit die Distanz die es zurücklegen kann. Innerhalb der Klasse Socket wird hierfür die Methode SetSocketOption verwendet und die Eigenschaft SocketOptionName.MulticastTimeToLive.

multicast_routing

Im Gegensatz zu Broadcast, wo die Empfänger nichts weiter zu tun haben, müssen bei Multicast die Empfänger zunächst einer Multicast-Gruppe beitreten. Das Beitreten veranlasst den Betriebssystemkern dazu Pakete an höhere Schichten weiterzuleiten. Beim guten alten Analogradio entspräche das der Frequenz, die die Hörer für den Empfang eines Senders einstellen müssen. Im Fall von IP-Multicast muss der Kernel also sozusagen die "Frequenz" (die Multicast-Gruppe) für den Empfang von Multicast-Paketen erfahren, damit der Anwender zum Beispiel einer Videokonferenz beiwohnen kann. Er muss der Multicast-Gruppe "beitreten" (Join), um Daten zu empfangen.

Um eine Multicast-Gruppe wieder zu verlassen, muss ein Drop an die Adresse versendet werden. Alternativ dazu kann der Socket geschlossen werden, was implizit dazu führt das man die Gruppe verlässt. In .NET stellt die höhere Socketklasse UdpClient die Methoden JoinMulticastGroup und DropMulticastGroup zur Verfügung. In dem nachfolgenden Beispiel, wird auf dem Socket die Option SocketOptionName.AddMembership verwendet um einer Gruppe beizutreten.

using System;
using System.Net;
using System.Net.Sockets;
 
namespace CodePlanet.Articles.ProgrammingSockets
{
    public class ReceiveUDPMulticast
    {
        public static void Main(string[] args)
        {
            if ( args.Length != 2 )
                throw new ArgumentException("Parameter(s): <Multicast Addr> <Port>");
 
            if (!MCIPAddress.isValid(args[0]))
                throw new ArgumentException("Valid MC addr: 224.0.0.0 - 239.255.255.255");
 
            IPAddress address = IPAddress.Parse(args[0]);  // Zieladresse
 
            int port = Int32.Parse(args[1]);    // Multicast port
 
            Socket sock = new Socket(AddressFamily.InterNetwork,
                                     SocketType.Dgram,
                                     ProtocolType.Udp); // Multicast Socket
 
            // Adresse wiederverwenden
            sock.SetSocketOption(SocketOptionLevel.Socket,
                                 SocketOptionName.ReuseAddress, 1);
 
            // Generiere Endpunkt
            IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, port);
            sock.Bind(endPoint);
 
            // Mitgliedschaft in der Multicast Gruppe
            sock.SetSocketOption(SocketOptionLevel.IP,
                                 SocketOptionName.AddMembership,
                                 new MulticastOption(address, IPAddress.Any));
 
            IPEndPoint receivePoint = new IPEndPoint(IPAddress.Any, 0);
            EndPoint tempReceivePoint = (EndPoint)receivePoint;
 
            // Generiere und empfange das Datagramm
            byte[] packet = new byte[ItemQuoteTextConst.MAX_WIRE_LENGTH];
            int length = sock.ReceiveFrom(packet, 0, ItemQuoteTextConst.MAX_WIRE_LENGTH,
                                          SocketFlags.None, ref tempReceivePoint);
 
            // Textdekodierer
            ItemQuoteDecoderText decoder = new ItemQuoteDecoderText();
            ItemQuote quote = decoder.decode(packet);
 
            Console.WriteLine(quote);
 
            // Kündige Mitgliedschaft in der Multicast Gruppe
            sock.SetSocketOption(SocketOptionLevel.IP,
                                 SocketOptionName.DropMembership,
                                 new MulticastOption(address, IPAddress.Any));
 
            sock.Close();
        }
    }
}

Starten Sie den Empfänger in der Konsole mit einer Multicast-Adresse und einer Portnummer. Versenden Sie anschließend mit dem Sender ein Paket an diese Gruppe. Sie werden erkennen können, dass der Empfänger in der Methode ReceiveFrom blockiert, bis er ein Datenpaket für die Gruppe empfangen hat.

Microsoft Windows [Version 6.0.6001]
Copyright (c) 2006 Microsoft Corporation. Alle Rechte vorbehalten.

C:\>ReceiveUDPMulticast.exe 224.0.0.0 777
Item# = 1234567890987654
Description = 5mm Super Widgets
Quantity = 1000
Price (each) = 12999
Total Price = 12999000 (discounted)
Out of Stock

Multicast Datagramme können direkt mit Sockets versendet werden oder mit der höheren Klasse UdpClient, indem einfach eine gültige Multicast-Adresse verwendet wird. In diesem Fall beträgt die TTL 1. Die Entscheidung für oder gegen Broad- bzw. Multicast, hängt von verschiedenen Faktoren ab. Für gewöhnlich wird Broadcast verwendet, wenn Datagramme in einem sehr begrenzten lokalen Netzwerk versendet werden sollen. Sollen Daten innerhalb eines größeren Netzwerkes übertragen werden, wird auf Multicast zurückgegriffen, sofern die Router in diesem Netzwerk Multicast-fähig sind. Die Nutzung von Broadcast wird innerhalb des Internets nicht unterstützt. Multicast beschränkt sich heute im Internet auf globale Multicast Backbones (MBONE) mit den entsprechenden Routern. Obwohl das IPv4 und IPv6 sowohl Broad- als auch Multicast definieren, sind die im Internet eingesetzten Router größtenteils nicht Broadcast- und Multicast-fähig, so dass ihre Nutzung auf lokale Netzwerke beschränkt bleibt. Gründe sind unter anderem darin zu finden, dass die Umsetzung von Multicast bei großen Netzwerken, wie dem Internet, technische Schwierigkeiten bereitet, die insbesondere die Skalierbarkeit betreffen. Netzwerkteilnehmer könnten einen Broadcast missbrauchen und ganze Netzwerke im Internet mit Paketen fluten.

Die Nutzung von Multicast setzt voraus, dass dem Sender und Empfänger die Adresse der Gruppe bekannt ist, damit sie sich dort registrieren können. Die unterliegende Schicht bleibt bei den verschiedenen Übertragungsarten immer das User Datagram Protocol. Ein Empfänger, der an Port Y lauscht, empfängt sowohl Unicast, als auch Multicast Datagramme, die für diesen Port bestimmt sind.


Ping mit C#

Eines der trivialsten Dinge im Netzwerk ist das sogenannte Anpingen eines Host. Damit kann überprüft werden ob ein Host überhaupt online ist und wie groß die Verzögerung zwischen ausgehenden Paketen und der Antwort ist. Diese Zeit wird auch Latenzzeit genannt. Ping sendet dazu ein ICMP-Echo-Request-Paket an die Zieladresse des zu überprüfenden Hosts. Der Empfänger muss, sofern er das Protokoll unterstützt, laut Protokollspezifikation eine Antwort zurücksenden, das sogenannte ICMP Echo-Reply. Aus der Differenz errechnet sich die Latenzzeit.

Ein Ping ist relativ einfach strukturiert. Im Gegensatz zu komplexen Netzwerkkommunikationen werden nur einfache genormte Pakete versendet. Umso überraschender ist, dass die .NET-Klassenbibliothek bis zur Version 2.0 keine vorgefertigte Klasse bzw. Lösung für das Anpingen eines Hosts bereitstellte. Stattdessen mussten Sie das selbst mit C# realisieren. Die Socket Klasse stellt dafür den SocketType Raw zur Verfügung. Mithilfe von Raw Sockets ist es möglich unter Verwendung des ICMP ein eigens erstelltes Ping Paket über das Netzwerk zu senden.
Bedenken Sie aber das alle Windows Versionen der NT-Serie seit den von Microsoft zur Verfügung gestellten Service Packs einen vollständigen Zugriff auf Raw Sockets nicht gestatten. Im Gegensatz zur gängigen Berkeley Unix Raw Socket Implementierung, sind Anwendungen vom "lower-level" Zugriff zum unterliegenden physikalischen Internet abgeschnitten. Beispielsweise ist es nicht möglich die Quell-IP-Adresse eines Paketes (spoofing) zu beeinflussen.

Microsoft hat sich für diese Einschränkung entschlossen um Missbrauch entgegenzuwirken, nachdem führende Sicherheitsexperten die unter Windows XP/2000 anfangs noch vollständig vorhandene Raw Socket Implementierung stark kritisiert hatten. Wenn Sie dennoch in den vollen Genuss von Berkeley Unix Raw Sockets unter Windows kommen möchten, sind Sie gezwungen einen eigenen Kernel-Mode Treiber zu schreiben der sich in den NDIS Stack einklinkt und direkt mit dem Gerätetreiber der Netzwerkkarte kommuniziert. Auf der Rawether for Windows Webseite von PCAUSA finden Sie mehrere vorgefertigte Lösungen zu diesem Problem.

Da der Code für einen selbst erstellten Ping vollständig von Grund auf implementiert wird, führt das oft zu sehr vielen Codezeilen. Als erster Programmierer hat der Microsoft-Entwickler Lance Olson das Problem gelöst und ein komplettes Programm im MSDN Magazine veröffentlicht. Seitdem wurde dieses in die verschiedensten Varianten abgeändert und erweitert. Das Prinzip ist jedoch das gleiche geblieben. Das folgende Programm zeigt den Aufbau der Klasse Ping.

using System;
using System.Net;
using System.Net.Sockets;
 
namespace CodePlanet.Articles.ProgrammingSockets
{
    /// <summary>
    /// Implementierung eines Pings in C#.
    /// </summary>
    class Ping
    {
        const int SOCKET_ERROR = -1;
        const int ICMP_ECHO = 8;
 
        public int GetPingTime(string host)
        {
            int nBytes = 0, dwStart = 0, dwStop = 0, PingTime = 0;
 
            IPHostEntry serverHE, fromHE;
            IcmpPacket packet = new IcmpPacket();
 
            if (host == null)
                return -1;
 
            // Einen Raw-Socket erstellen.
            Socket socket = new Socket(AddressFamily.InterNetwork,
                                       SocketType.Raw,
                                       ProtocolType.Icmp);
 
            serverHE = Dns.GetHostEntry(host);
 
            if (serverHE == null) {
                return -1; // Fehler
            }
 
            // Den IPEndPoint des Servers in einen EndPoint konvertieren.
            IPEndPoint ipepServer = new IPEndPoint(serverHE.AddressList[0], 0);
            EndPoint epServer = (ipepServer);
 
            // Den empfangenen Endpunkt für den Client-Rechner setzen.
            fromHE = Dns.GetHostEntry(Dns.GetHostName());
            IPEndPoint ipEndPointFrom = new IPEndPoint(fromHE.AddressList[0], 0);
            EndPoint EndPointFrom = (ipEndPointFrom);
 
            int PacketSize = 0;
 
            for (int j = 0; j < 1; j++) {
                // Das zu sendende Paket erstellen.
                packet.Type = ICMP_ECHO;
                packet.SubCode = 0;
                packet.CheckSum = UInt16.Parse("0");
                packet.Identifier = UInt16.Parse("45");
                packet.SequenceNumber = UInt16.Parse("0");
 
                int PingData = 32;
                packet.Data = new byte[PingData];
 
                for (int i = 0; i < PingData; i++)
                    packet.Data[i] = (byte)'#';
 
                PacketSize = PingData + 8;
 
 
                // Stelle sicher dass das icmp_pkt_buffer Byte Array 
                // eine gerade Zahl ist.
                if (PacketSize % 2 == 1)
                    ++PacketSize;
 
                byte[] icmp_pkt_buffer = new byte[PacketSize];
 
                int index = 0;
 
                index = Serialize(packet,
                                  icmp_pkt_buffer,
                                  PacketSize,
                                  PingData);
 
                if (index == -1)
                    return -1;
 
                // Die Prüfsumme für das Paket berechnen.
                double double_length = Convert.ToDouble(index);
 
                double dtemp = Math.Ceiling(double_length / 2);
 
                int cksum_buffer_length = Convert.ToInt32(dtemp);
 
                UInt16[] cksum_buffer = new UInt16[cksum_buffer_length];
 
                int icmp_header_buffer_index = 0;
 
                for (int i = 0; i < cksum_buffer_length; i++) {
                    cksum_buffer[i] = BitConverter.ToUInt16(icmp_pkt_buffer, icmp_header_buffer_index);
                    icmp_header_buffer_index += 2;
                }
 
                UInt16 u_cksum = checksum(cksum_buffer, cksum_buffer_length);
                packet.CheckSum = u_cksum;
 
                // Nachdem nun die Prüfsumme vorhanden ist, das Paket erneut serialisieren.
                byte[] sendbuf = new byte[PacketSize];
 
                index = Serialize(packet,
                                  sendbuf,
                                  PacketSize,
                                  PingData);
 
                if (index == -1)
                    return -1;
 
                dwStart = System.Environment.TickCount; // Starte den Timer
 
                if ((nBytes = socket.SendTo(sendbuf, PacketSize, 0, epServer)) == SOCKET_ERROR) {
                    Console.WriteLine("Error calling sendto");
                    return -1; // Fehler
                }
 
                // Initialisiere den Buffer. Der Empfänger-Buffer ist die Größe des
                // ICMP Header plus den IP Header (20 bytes)
                byte[] ReceiveBuffer = new byte[256];
 
                nBytes = 0;
                nBytes = socket.ReceiveFrom(ReceiveBuffer, 256, 0, ref EndPointFrom);
 
                if (nBytes == SOCKET_ERROR) {
                    dwStop = SOCKET_ERROR;
                } else {
                    // Stoppe den Timer
                    dwStop = System.Environment.TickCount - dwStart;
                }
            }
 
            socket.Close();
            PingTime = (int)dwStop;
            return PingTime;
        }
 
        private static int Serialize(IcmpPacket packet, byte[] Buffer, int PacketSize, int PingData)
        {
            int cbReturn = 0;
 
            // Serialisiere den struct in ein Array
            int Index = 0;
 
            byte[] b_type = new byte[1];
            b_type[0] = (packet.Type);
 
            byte[] b_code = new byte[1];
            b_code[0] = (packet.SubCode);
 
            byte[] b_cksum = BitConverter.GetBytes(packet.CheckSum);
            byte[] b_id = BitConverter.GetBytes(packet.Identifier);
            byte[] b_seq = BitConverter.GetBytes(packet.SequenceNumber);
 
            // Console.WriteLine("Serialize type ");
            Array.Copy(b_type, 0, Buffer, Index, b_type.Length);
            Index += b_type.Length;
 
            // Console.WriteLine("Serialize code ");
            Array.Copy(b_code, 0, Buffer, Index, b_code.Length);
            Index += b_code.Length;
 
            // Console.WriteLine("Serialize cksum ");
            Array.Copy(b_cksum, 0, Buffer, Index, b_cksum.Length);
            Index += b_cksum.Length;
 
            // Console.WriteLine("Serialize id ");
            Array.Copy(b_id, 0, Buffer, Index, b_id.Length);
            Index += b_id.Length;
 
            Array.Copy(b_seq, 0, Buffer, Index, b_seq.Length);
            Index += b_seq.Length;
 
            // Kopiere die Daten
 
            Array.Copy(packet.Data, 0, Buffer, Index, PingData);
 
            Index += PingData;
 
            if (Index != PacketSize /* sizeof(IcmpPacket) */) {
                cbReturn = -1;
                return cbReturn;
            }
 
            cbReturn = Index;
            return cbReturn;
        }
 
        private static UInt16 checksum(UInt16[] buffer, int size)
        {
            int cksum = 0;
            int counter;
 
            counter = 0;
 
            while (size > 0) {
                UInt16 val = buffer[counter];
 
                cksum += Convert.ToInt32(buffer[counter]);
                counter += 1;
                size -= 1;
            }
 
            cksum = (cksum >> 16) + (cksum & 0xffff);
            cksum += (cksum >> 16);
            return (UInt16)(~cksum);
        }
    }
 
    public class IcmpPacket
    {
        public byte Type;               // Message Typ
        public byte SubCode;            // Subcode Typ
        public byte[] Data;             // Byte Array
        public UInt16 CheckSum;         // Checksumme
        public UInt16 Identifier;       // Identifizierer
        public UInt16 SequenceNumber;   // Sequenznummer 
    }
}

Viele DOS-Angriffe, auch Denial of Service Attacks genannt, werden ähnlich wie beim Ping über das ICMP realisiert. Im Gegensatz zum "normalen" Ping werden hierbei korrupte Pakete in sehr großer Anzahl an den Server gesendet. Bei entsprechender Menge gelingt es diesen durch Überlastung zum Absturz zu bringen.

Achten Sie darauf das ihr Programm durch Abänderung nicht versehentlich korrupte Datenpakete versendet, so dass ihre IP-Adresse bei Tests eventuell auf die Blacklist des Servers gelangt.

Seit der .NET Framework Version 2.0 steht ein neuer Namensraum zur Verfügung der zahlreiche neue Funktionen für den Zugriff auf Traffic Daten, Netzwerk Adress Informationen, Benachrichtigungsmethoden usw. bereitstellt. Der Namespace heißt System.Net.NetworkInformation und erweitert den Namespace System.Net. Der Namensraum enthält nun endlich auch eine Klasse für das Anpingen von Hosts mit dem Namen Ping. Das folgende Beispiel stammt aus der MSDN und zeigt den prinzipiellen Aufbau eines C#-Programmes das die Klasse Ping nutzt:

using System;
using System.Net;
using System.Net.NetworkInformation;
using System.Text;
 
namespace CodePlanet.Articles.ProgrammingSockets
{
    /// <summary>
    /// Das folgende Programm pingt einen gegebenen Host an, indem es
    /// die Klasse Ping aus dem Namensraum System.Net.NetworkInformation
    /// verwendet. Das Beispiel stammt aus der MSDN Library -
    /// ©2006 Microsoft Corporation.
    /// </summary>
    public class PingClass
    {
        // Das Argument args[0] kann eine IPaddress ode ein Host-Name sein.
        public static void Main(string[] args)
        {
            if (args.Length < 1) {
                throw new ArgumentException("Parameters: [<Uri>]");
            }
 
            Ping pingSender = new Ping();
            PingOptions options = new PingOptions();
 
            // Benutze den Standard TTL Wert (Time To Live) der bei 128ms liegt,
            // aber ändere das Fragmentationsverhalten.
            options.DontFragment = true;
 
            // Erzeuge einen Puffer mit der Länge von 32 Bytes 
            // die versendet werden sollen.
            string data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
            byte[] buffer = Encoding.ASCII.GetBytes(data);
            int timeout = 120;
 
            PingReply reply = pingSender.Send(args[0], timeout, buffer, options);
            if (reply.Status == IPStatus.Success) {
                Console.WriteLine("Address: {0}", reply.Address.ToString());
                Console.WriteLine("RoundTrip time: {0}", reply.RoundtripTime);
                Console.WriteLine("Time to live: {0}", reply.Options.Ttl);
                Console.WriteLine("Don't fragment: {0}", reply.Options.DontFragment);
                Console.WriteLine("Buffer size: {0}", reply.Buffer.Length);
            }
        }
    }
}

Der oben gezeigte Quellcode verwendet die Klasse Ping synchron. Falls Ihre Applikation nicht blocken soll, müssen Sie die asynchrone Methode SendAsync benutzen. Ein Aufruf von SendAsync wird in einem separaten Thread ausgeführt der automatisch Teil eines Thread Pools ist. Sobald die asynchrone Operation vervollständigt ist, wird ein PingCompleted Ereignis ausgelöst. Näheres zum Thema Blocking, synchrone und asynchrone I/O erfahren Sie in den nachfolgenden Abschnitten. Eine asynchrone Implementierung der Klasse Ping ist ebenfalls im Dateianhang dieses Tutorials enthalten.

Blocking

In unseren bisherigen Programmen haben wir Sockets dazu verwendet Daten zu empfangen, Daten zu senden, Hosts anzupingen uvm. Unsere Programme waren einfach aufgebaut und erfüllten ihren Zweck. Die bisherige Nutzung der Sockets hat allerdings einen entscheidenden Nachteil. Methoden wie beispielsweise Send, Receive und viele weitere blockieren die Programmausführung solange sie nicht ordnungsgemäß abgelaufen sind. Dieses sogenannte Blocking (engl. für blockieren) kann beispielsweise in der Setup Phase während des Verbindungsaufbau's sehr lang sein, wenn der Server nur sehr langsam auf Anfragen reagiert. Während dieser Zeit ist das Programm praktisch gelähmt und kann keine weiteren Prozesse ausführen. Wird eine Blocking-Methode aufgerufen die Daten vom Socket Buffer einliest, diese jedoch nicht vorhanden sind, kann die Methode im schlechtesten Fall das Programm vollständig einfrieren. Dies passiert z.B. bei verloren gegangenen UDP-Datagramms.

In unseren Kommandozeilen-Programmen ist das noch relativ unspektakulär und auch nebensächlich. Doch was passiert wenn wir ausgefeiltere Programme schreiben möchten? Womöglich mit Fenstern und Eingabefeldern? Würden wir die bisherigen Wege verwenden über Sockets zu kommunizieren, so hätten wir ein Problem. Solange die Methoden das Programm blockieren, kann dieses nämlich weder auf Mausbewegungen noch auf Tastatureingaben reagieren!

Nonblocking I/O, Threads und asynchrone I/O

Doch wie sieht die Lösung diese Problems aus? Es existieren einige Möglichkeiten in .NET damit vernünftig umzugehen. Die erste triviale Möglichkeit ist einfach keine Aufrufe zu tätigen die das Blockieren des Programmes zur Folge hätten. Eigenschaften und Methoden der Klassen können hierbei genutzt werden um den Status festzustellen bevor eine Block Methode aufgerufen wird:

TcpClient client = new TcpClient(server, port);
NetworkStream netstream = client.GetStream();
 
// Befinden sich überhaupt Daten im Stream?
if(netstream.DataAvailable)
    int len = netstream.Read(buffer, 0, buffer.Length);
 
...

Eine weitere Möglichkeit besteht darin blockierende Methoden mit einem Timeout zu versehen. Wird eine bestimmte Zeitspanne überschritten, so übergibt die Methode die Ausführung wieder an das Programm. Dieser Ansatz wird auch als Polling (engl. zyklische Abfrage) bezeichnet. Sinngemäß existiert auch eine Methode namens Poll.

// Erzeugt einen Socket der Daten über TCP versendet.
Socket sock = new Socket(AddressFamily.InterNetwork, 
                  SocketType.Stream,
                  ProtocolType.Tcp);
 
// Verbinde zum Endpunkt
sock.Connect(EPhost);
 
if (!sock.Connected) {
    strRetPage = "Unable to connect to host";
}
 
// Benutze die SelectWrite Enumeration um den Socket Status zu erhalten.
// Kein bestimmtes Timeout!
if(sock.Poll(-1, SelectMode.SelectWrite)) {
    Console.WriteLine("This Socket is writable.\n");
} else if (sock.Poll(1000000, SelectMode.SelectRead)) {
    // Warte diesmal 1 Sekunde lang, breche dann ab.
    Console.WriteLine("This should not print. Because this is not a listening Socket,"
                      + "no incoming connecton requests are expected.\n" );
} else if (sock.Poll(-1, SelectMode.SelectError)) {
    Console.WriteLine("This Socket has an error.");
}

Das Polling ist jedoch sehr ineffizient. Es werden in gleichmäßigen Intervallen ständig Aufrufe zur Überprüfung des aktuellen Status durchgeführt. Ihr Programm läuft also in einer Warteschleife und wartet auf Ereignisse die sich womöglich in nur sehr unregelmäßigen Abständen ereignen. Ein klassischer Fall von Performanceverschwendung.

Ähnlich zum Polling über fest gesetzte Timeouts ist das Setzen eines bestimmten Socket-Flags. Wird dieses Flag auf false gesetzt, so blockieren die Methoden dieses Sockets bei ihrem Aufruf das Programm nicht mehr. Mithilfe von Exceptions ist es möglich durch eine entsprechende Fehlerbehandlung darauf zu reagieren.

// Erstelle einen Raw Socket und benutze ICMP.
Socket sock = new Socket(AddressFamily.InterNetwork, 
                         SocketType.Raw, 
                         ProtocolType.Icmp);
 
// Setze das Flag Blocking auf false und versetze
// den Socket auf diese Weise in den Non-Blocking-Modus.
sock.Blocking = false;
 
// Send() befindet sich nun im Non-Blocking-Modus
// und wird nicht mehr blockieren.
try {
    sock.Send(buffer, buffer.Length, 0)
    // ...
} catch(SocketException se) {
    // ...
}

Wir haben gesehen wie in .NET Nonblocking Verfahren verwendet werden können um anderweitig Code auszuführen während wir gleichzeitig auf die Socket Methoden warten. Da diese fast auschliesslich auf Polling beruhen sind sie jedoch oft einfach ungeeignet. Neben dem zeitlichen Aspekt hat das Polling auch noch einen weiteren entscheidenden Nachteil. Es kann immer nur eine bestimmte Anzahl an Verbindungen abgefertigt werden. Ein als Server agierendes Programm wird einen neuen Clienten erst dann bedienen wenn es mit dem aktuellen Clienten abgeschlossen hat. Dies kann bei bestimmten Anwendungen sogar erwünscht sein, insbesondere dann wenn Clienten sequentiell abgefertigt werden sollen, ist aber in den meisten Fällen unzureichend.

Hilfreich wäre ein Verfahren in dem die Prozesse separat agieren und sich nicht gegenseitig behindern. Als C# Programmierer werden Sie wahrscheinlich sofort an Threads denken. Und mit dieser Vermutung liegen Sie goldrichtig! In der Tat lässt sich das Problem des Blockings am effektivsten mit Threads lösen.

Thread t1 = new Thread(new ThreadStart(ClientHandle));

Threads ermöglichen Multitasking innerhalb der Anwendung. Konkret gesagt ist das die Fähigkeit mehrere Dinge im Programm gleichzeitig zu machen. Sie können mithilfe von Threads einen Echo-Server schreiben der jeden Clienten in einem separaten Thread verwaltet. Ein einfaches Beispiel das sich Threads zu Nutze macht finden Sie im Dateianhang.

Die Common Language Runtime fasst einen Großteil der Thread-Unterstützung in Klassen zusammen. Auf diese Weise kapseln viele Klassen in .NET Threads in ihrer eigenen Ausführung und Sie als Nutzer bekommen wenig davon mit. So ist es auch beim dritten und letzten Verfahren mit dem es möglich ist das Problem des Blockings effektiv zu lösen. Nämlich bei den asynchronen Inputs und Outputs. In diesem gängigen Verfahren werden Methoden verwendet die asynchron ablaufen. Wenn das Programm beispielsweise Daten über einen Socket sendet, wird parallel ein Thread etabliert der das Senden überwacht und durchführt. Sobald der Aufruf beendet ist übergibt der Thread die Daten zurück an das Programm. Während dieser Zeit läuft der Hauptprozess weiter und führt entsprechende Operationen aus. Die Übergabe der Daten sowie die Erzeugung des Threads werden vollautomatisch durchgeführt.

asynccalls

Neben der BeginSend Methode, die Daten asynchron an einen verbundenen Socket sendet, ist es auch möglich Daten mit BeginWrite direkt zu einem NetworkStream zu schreiben. Hierbei wird eine separate Klasse erzeugt, die für das asynchrone Streaming der Daten sorgt. Das Streaming beginnt mit einem Datenblock, indem es die Methode BeginWrite der Klasse NetworkStream aufruft und eine Rückrufmethode (engl. Callback) zur Verfügung stellt. Zunächst startet die Methode BeginWrite das asynchrone Senden an den Host. Sobald die Anwendung BeginWrite aufruft, generiert das System einen separaten Thread um dort die entsprechende Rückrufmethode auszuführen. Der Callback implementiert die Methode EndWrite die den Thread solange blockiert bis sämtliche Bytes vom NetworkStream übertragen wurden. Wenn Sie möchten das der aufrufende Thread blockiert wird nachdem die Methode BeginWrite aufgerufen wurde, benutzen Sie WaitOne. Soll der aufrufende Thread weiterhin ausgeführt werden, können Sie auf ManualResetEvent.Set in der Rückrufmethode zurückgreifen. Sobald der Rückruf ausgelöst und ein entsprechendes Signal an WaitHandle gesendet wird, können Sie den nächsten Datenblock senden oder bei einem Echo-Server die Daten wieder empfangen.

NetworkStream netStream = client.GetStream();
 
// Gekapseltes Objekt für den Callback
ClientState cs = new ClientState(netStream, "SomeString");
 
// Sende den kodierten String asynchron an den Server
IAsyncResult result = netStream.BeginWrite(cs.ByteBuffer, 0,
                                           cs.ByteBuffer.Length,
                                           new AsyncCallback(WriteCallback),
                                           cs);
 
// Mache irgend etwas anderes
DoOtherStuff();
 
result.AsyncWaitHandle.WaitOne(); // blockiere bis EndWrite aufgerufen wird
 
...
 
// Die Klasse IAsyncResult repräsentiert den Status der
// asynchronen Operation und kann dazu verwendet werden
// um die Rückgabe der Operation zu blocken
// oder zu pollen (zyklische Abfrage).
public static void WriteCallback(IAsyncResult asyncResult)
{
    ClientState cs = (ClientState)asyncResult.AsyncState;
 
    // Behandelt das Ende des asynchronen Schreibens.
    cs.NetStream.EndWrite(asyncResult);
 
    ...
}

Threads können in Anwendungen in denen viele asynchrone Prozesse ablaufen ziemlich kostenintensiv werden. Bedenken Sie das für einen neuen Thread zunächst einmal ein Kernel Objekt allokiert werden muss, der zugehörige Stack initialisiert wird und anschließend sendet Windows® allen DLL's im Prozess eine entsprechende Benachrichtigung. Wird der Thread wieder zerstört so geht das Spiel in entgegengesetzter Richtung wieder von vorne los. Deshalb können Sie dieses gesamte Prozedere auch mithilfe der .NET ThreadPool Klasse realisieren. Unter Umständen sparen Sie auf diese Weise effektiv Ressourcen ein.

Nachfolgend sehen Sie ein grafisch anspruchsloses Client-Server Programm in bekannter Windows Fenster Form. Während der Server Operationen asynchron durchführt, arbeitet der Client sequentiell. Sie finden beide Programme im Anhang. Eine weiteres Beispiel das vollständig asynchrone I/O verwendet ist ebenfalls enthalten.

server-and-client

Hypertext Transfer Protocol (HTTP)

Zu den wesentlichen Operationen im Internet gehört das Ausfüllen und Versenden von Formularen. Ob es sich um eine Suchanfrage bei einer Suchmaschine handelt oder der Registrierung in einem Forum, stets sind Formulardaten im Spiel. Grundlage für die Präsentation und Manipulation von Webseiten ist das Hypertext Transfer Protocol (HTTP, dt. Hypertext-Übertragungsprotokoll). Das HTTP ist ein Protokoll zur Übertragung von Daten über ein Netzwerk und wird durch das RFC 2616 beschrieben. Es gehört der sogenannten Anwendungsschicht etablierter Netzwerkmodelle an und befindet sich somit direkt über der Transportschicht, in der das TCP und UDP verankert sind. Die Anwendungsschicht wird von den Anwendungsprogrammen angesprochen, im Fall des HTTP ist dies meistens ein Webbrowser. Im ISO/OSI-Schichtenmodell entspricht die Anwendungsschicht den Schichten 5–7.

Durch Erweiterung seiner Anfragemethoden, Header-Informationen und Statuscodes ist das HTTP nicht auf Hypertext beschränkt, sondern wird zunehmend zum Austausch beliebiger Daten verwendet. Zur Kommunikation ist HTTP auf ein zuverlässiges Transportprotokoll angewiesen. In nahezu allen Fällen wird hierfür das Transmission Control Protocol (TCP) verwendet.

Das HTTP wurde 1989 von Tim Berners-Lee am CERN zusammen mit der URL und der Hypertext Markup Language (HTML) entwickelt, wodurch praktisch das World Wide Web geboren wurde. Es wird hauptsächlich eingesetzt, um Webseiten aus dem World Wide Web (WWW) in einen Webbrowser zu laden. Die Flexibilität von HTTP hat dazu geführt, dass heute zahlreiche Netzwerkprotokolle, wie SOAP (ursprünglich für Simple Object Access Protocol), auf HTTP aufbauen.

Aufbau

Die Kommunikationseinheiten im HTTP zwischen Client und Server werden als Nachrichten bezeichnet, von denen es zwei unterschiedliche Arten gibt: die Anfrage (engl. Request) vom Client an den Server und die Antwort (engl. Response) als Reaktion darauf vom Server zum Client.

http-request

Jede Nachricht besteht dabei aus zwei Teilen, dem Nachrichtenkopf (engl. Message Header, kurz: Header oder auch HTTP-Header genannt) und dem Nachrichtenkörper (engl. Message Body, kurz: Body). Der Nachrichtenkopf enthält wichtige Informationen über den Nachrichtenkörper wie etwa verwendete Kodierungen oder den Inhaltstyp, damit dieser vom Empfänger korrekt interpretiert werden kann. Der Nachrichtenkörper enthält schließlich die Nutzdaten.

POST und GET

Häufig will der Nutzer einer Website spezielle Informationen senden. Dazu stellt HTTP prinzipiell zwei Möglichkeiten zur Verfügung. Beim sogenannten HTTP-GET sind die Daten Teil der URL und bleiben deshalb beim Speichern oder der Weitergabe des Links erhalten. Die zweite Variante nennt sich HTTP-POST. Hier werden die Daten mit einer speziell dazu vorgesehenen Anfrageart in den HTTP-Kopfdaten übertragen, so dass sie in der URL nicht sichtbar sind.

HTTP-GET

Hierbei werden die Parameter-Wertepaare durch das Zeichen ? in der URL eingeleitet.

Oft wird diese Vorgehensweise gewählt, um eine Liste von Parametern zu übertragen, die die Gegenstelle bei der Bearbeitung einer Anfrage berücksichtigen soll. Häufig besteht diese Liste aus mit dem Zeichen & getrennten Wertepaaren, die je aus einem Parameternamen, dem Zeichen = und dem Wert des Parameters bestehen. Seltener wird das Zeichen ; zur Trennung von Einträgen der Liste benutzt.

Ein Beispiel: Das Eingabefeld auf einer Login-Seite hat zwei Felder mit den Namen „user“ und „password“. Der Browser sendet folgende oder ähnliche Anfrage an den Server:

	GET /login.php?user=joe&password=guessme HTTP/1.1
	Host: www.mysite.com
	User-Agent: Mozilla/4.0

Der Aufruf erfolgt direkt über den Uniform Resource Locator (URL), ist also in der Adresszeile des Browsers sichtbar.

HTTP-POST

Da sich die Daten nicht in der URL befinden, können per POST große Datenmengen, z. B. Bilder, übertragen werden.

Im folgenden Beispiel wird wieder ein Login ausgeführt, doch diesmal verwendet der Browser aufgrund eines modifizierten HTML-Codes (method="POST") eine POST-Anfrage. Die Variablen stehen dabei nicht in der URL, sondern gesondert im Body-Teil, etwa:

	POST /login.php HTTP/1.1
	Host: www.mysite.com
	User-Agent: Mozilla/4.0
	Content-Length: 27
	Content-Type: application/x-www-form-urlencoded

	user=joe&password=guessme

Alle zu übertragenden Daten müssen ggf. URL-kodiert werden, d. h. reservierte Zeichen müssen mit „%“ und Leerzeichen mit „+“ dargestellt werden.

HttpWebRequest und HttpWebResponse

Im .NET Framework gibt es zwei Basisklassen für den Umgang mit HTTP. Sinngemäß nennen sie sich HttpWebRequest und HttpWebResponse. Auch diese Klassen setzen direkt auf Sockets auf und ermöglichen einen abstrakten, bequemen Umgang mit dem Hypertext-Übertragungsprotokoll. Die Klassen generieren implizit die notwendigen TCP-Sockets und senden bzw. empfangen die spezifizierten Daten.

Wie man einen HTTP-GET auf Socketebene ausführt, haben Sie bereits in einem vorangegangenen Kapitel erfahren. In dem Codebeispiel HTTPGet wurde eine Zeichenkette verwendet, um über einen offenen TCP-Socket eine HTTP/1.1 Anfrage an den Server zu senden. Diese Anfrage beantwortet der Server standardmäßig mit der Ausstrahlung der Startseite der angeforderten Webseite. Die Klasse HttpWebRequest erspart Ihnen die Konstruktion des TCP-Sockets und alle weiteren Details. Stattdessen legen Sie mit dem Property Method die Art des Requests fest und geben anschließend nur noch die Anfrage selbst an. Mithilfe von GetRequestStream lässt sich die Antwort des Servers anschließend direkt auslesen.

Wir wollen nach der Theorie nun mit C# ein Formular ausfüllen. Auf der Seite http://www.codeplanet.eu/files/post/index.html steht hierfür ein einfaches Post Formular bereit.

post-form

Das Formular besitzt den folgenden HTML-Quelltext:

 
<form action="form.php" method="POST">
    <table>
    <tr>
        <td>Name:</td>
        <td><input type="text" name="name"></td>
    </tr>
    <tr>
        <td>Email:</td>
        <td><input type="text" name="email"></td>
    </tr>
    <tr>
        <td>Nachricht:</td>
        <td><textarea name="msg"></textarea></td>
    </tr>
    <tr>
        <td><input type="submit" name="submitform" value="Senden"></td>
    </tr>
<form> 
 

Es lässt sich erkennen, dass das Formular drei Eingabefelder besitzt. Jedes Eingabefeld trägt einen Namen. Diese Namen sind bei einem HTTP-Request von Bedeutung. Sobald der Benutzer den Sendebutton betätigt, wird per POST das PHP-Skript form.php aufgerufen. Das Skript verwertet die per POST übermittelten Daten und gibt diese formatiert aus. Sie können das im Originalformular ausprobieren.

Das folgende Beispiel generiert POST-Daten und sendet sie mit HttpWebRequest an das genannte Skript. Anschließend wird mithilfe von HttpWebResponse die Antwort des Servers empfangen und der Quelltext der Antwort auf der Konsole ausgegeben.

using System;
using System.Net;
using System.Text;
using System.IO;
 
namespace CodePlanet.Articles.ProgrammingSockets
{
    /// <summary>
    /// Demonstriert einen HTTP-Post. Füllt ein Formular
    /// im Internet aus und empfängt die Antwort als HTML-Quelltext.
    /// </summary>
    public class HTTPPostExample
    {
        public static void Main(string[] args)
        {
            // Setze den Uniform Resource Identifier.
            Uri uri = new Uri("http://www.codeplanet.eu/files/post/form.php");
            // Generiere einen Request mit einer Adresse, die einen POST erwartet.
            WebRequest request = WebRequest.Create(uri);
            // Setze die Methodeneigenschaft des Request auf POST.
            request.Method = "POST";
            // Generiere die POST Daten und konvertiere sie in ein byte Array.
            string postData = "submitform=Senden&" +
                              "name=Max Mustermann&" +
                              "email=max\@muster.com&" +
                              "msg=Hello my dear friend, how are you?";
            byte[] byteArray = Encoding.UTF8.GetBytes(postData);
            // Setze die ContentType Eigenschaft von WebRequest.
            request.ContentType = "application/x-www-form-urlencoded";
            // Setze die ContentLength Eigenschaft von WebRequest.
            request.ContentLength = byteArray.Length;
            // Empfange den Request Stream.
            Stream dataStream = request.GetRequestStream();
            // Schreibe die Daten in den Request Stream.
            dataStream.Write(byteArray, 0, byteArray.Length);
            // Schließe das Stream Objekt.
            dataStream.Close();
            // Empfange die Antwort.
            WebResponse response = request.GetResponse();
            // Zeige den Status an.
            Console.WriteLine(((HttpWebResponse)response).StatusDescription);
            // Empfange den Stream mit dem Inhalt vom Server.
            dataStream = response.GetResponseStream();
            // Öffne den Stream mit dem StreamReader.
            StreamReader reader = new StreamReader(dataStream);
            // Lese den Inhalt.
            string responseFromServer = reader.ReadToEnd();
            // Zeige den Inhalt in der Konsole an.
            Console.WriteLine(responseFromServer);
            // Schließe alle Streams.
            reader.Close();
            dataStream.Close();
            response.Close();
        }
    }
}

Cookies

Ein Problem bei HTTP ist das Informationen aus früheren Anforderungen verloren (zustandsloses Protokoll) gehen. Über Cookies in den Header-Informationen können aber Anwendungen realisiert werden, die Statusinformationen (Benutzereinträge, Warenkörbe) zuordnen können. Dadurch werden Anwendungen möglich, die Status- bzw. Sitzungseigenschaften erfordern. Auch eine Benutzerauthentifizierung ist möglich.

Ein HTTP-Cookie, auch Browser-Cookie genannt (engl., „Plätzchen“, „Keks“), bezeichnet Informationen, die ein Webserver zu einem Browser sendet oder die clientseitig durch JavaScript erzeugt werden. Der Client sendet die Informationen in der Regel bei späteren Zugriffen an denselben Webserver im Hypertext-Transfer-Protocol-Header an den Server. Cookies sind clientseitig persistente/gespeicherte Daten und werden im RFC 2965 detailliert beschrieben.

Im nachfolgenden Beispiel bittet der Server den Clienten mit der „Set-Cookie“-Zeile, sich Daten zu merken:

	HTTP/1.0 200 OK
	Set-Cookie: letzteSuche="cookie aufbau";
	            expires=Tue, 29-Mar-2005 19:30:42 GMT;
	            Max-Age=2592000;
	            Path=/cgi/suche.py;
	            Version="1"

Cookies kommen im Internet sehr oft zur Anwendung. Denken Sie an die Anmeldung in einem Internetforum, bei der die Sitzung gespeichert werden soll. Sie können Ihren Rechner herunterfahren und die Seite zu einem späteren Zeitpunkt erneut besuchen. Wenn Sie die Speicherung von Cookies mit Ihrem Webbrowser gestattet haben, müssen Sie sich nicht erneut anmelden. Der Webbrowser sendet einfach die Identifikationsdaten, die im Cookie gespeichert sind, an den Server.

Das unten gezeigte Beispiel demonstriert die Anmeldung in unserem Forum. Die POST-Daten für den Login setzen sich aus dem Forennamen und dem Passwort zusammen. Das Passwort wird als MD5-Hash versendet. Die statische Methode getMd5Hash generiert aus einer beliebigen Zeichenkette den Hashwert. Das Programm sendet die Formulardaten an den Server von CodePlanet und meldet sich im Forum an. Parallel dazu werden die Cookiedaten empfangen und ihr Inhalt auf der Kommandozeile ausgegeben.

using System;
using System.Net;
using System.Text;
using System.IO;
using System.Security.Cryptography;
 
namespace CodePlanet.Articles.ProgrammingSockets
{
    /// <summary>
    /// Demonstriert den Login auf einem Internet-Portal,
    /// in diesem Fall im CodePlanet Forum.
    /// </summary>
    public class HTTPWebResponse
    {
        public static void Main(string[] args)
        {
            string loginname = "User";
            string password = "Password";
            string uri = "http://codeplanet.eu/forum/login.php?do=login";
            string loginData = "vb_login_username=" +
                               loginname +
                               "&vb_login_password=&s=&do=login&vb_login_md5password=" +
                               getMd5Hash(password) +
                               "&vb_login_md5password_utf=" +
                               getMd5Hash(password);
 
            HttpWebResponse webResponse = DoLogin(uri, loginData);
            ShowCookie(webResponse);
        }
 
        public static HttpWebResponse DoLogin(string loginUri, string loginData)
        {
            // Der cookieContainer speichert den Cookie beim Login
            CookieContainer cookieContainer = new CookieContainer();
            // Klicke zuerst auf die Login Seite
            HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(loginUri);
            // Identifiziere dich als Mozilla Firefox 2.0
            req.UserAgent = "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.8.1.1) Gecko/20061204 Firefox/2.0.0.1";
            req.CookieContainer = cookieContainer;
            req.Method = "POST";
            req.ContentType = "application/x-www-form-urlencoded";
            ASCIIEncoding encoding = new ASCIIEncoding();
            byte[] loginDataBytes = encoding.GetBytes(loginData);
            req.ContentLength = loginDataBytes.Length;
            Stream stream = req.GetRequestStream();
            stream.Write(loginDataBytes, 0, loginDataBytes.Length);
            stream.Close();
            return (HttpWebResponse)req.GetResponse();
        }
 
        public static void ShowCookie(HttpWebResponse res)
        {
            // Gebe die Details des Cookies aus
            foreach (Cookie cook in res.Cookies) {
                Console.WriteLine("Cookie:");
                Console.WriteLine("{0} = {1}", cook.Name, cook.Value);
                Console.WriteLine("Domain: {0}", cook.Domain);
                Console.WriteLine("Path: {0}", cook.Path);
                Console.WriteLine("Port: {0}", cook.Port);
                Console.WriteLine("Secure: {0}", cook.Secure);
 
                Console.WriteLine("When issued: {0}", cook.TimeStamp);
                Console.WriteLine("Expires: {0} (expired? {1})", cook.Expires, cook.Expired);
                Console.WriteLine("Don't save: {0}", cook.Discard);
                Console.WriteLine("Comment: {0}", cook.Comment);
                Console.WriteLine("Uri for comments: {0}", cook.CommentUri);
                Console.WriteLine("Version: RFC {0}", cook.Version == 1 ? "2109" : "2965");
 
                Console.WriteLine("String: {0}", cook.ToString());
            }
        }
 
        // Hashe einen string und generiere einen 32 Zeichen langen Hex-Hash.
        public static string getMd5Hash(string input)
        {
            // Generiere ein MD5CryptoServiceProvider Objekt
            MD5 md5Hasher = MD5.Create();
 
            // Konvertiere den Eingabestring in ein byte[] und berechne den Hash
            byte[] data = md5Hasher.ComputeHash(Encoding.Default.GetBytes(input));
 
            StringBuilder sBuilder = new StringBuilder();
 
            for (int i = 0; i < data.Length; i++) {
                sBuilder.Append(data[i].ToString("x2"));
            }
 
            return sBuilder.ToString();
        }
    }
}

Wie Sie wahrscheinlich bereits erkannt haben, ist es für den korrekten Aufruf der Daten erforderlich den Aufbau des entsprechenden Formulars zu kennen. Der Aufbau lässt sich dem Quelltext der Webseite entnehmen. Eine einfachere Variante nutzt ein Programm zur Netzwerkanalyse, mit dem sich die Netzwerk-Kommunikationsverbindungen zwischen Webbrowser und Server direkt beobachten lassen. Dazu müssen Sie in Ihrem Webbrowser lediglich das gewünschte Formular ausfüllen und absenden. Anschließend lassen sich, z.B. mithilfe von Wireshark oder dem Firefox-Plugin Firebug die vom Webbrowser übertragenen Formulardaten direkt einsehen. Insbesondere komplexere Formulare können so in der Praxis schnell erfasst werden ohne sich detailliert mit dem HTML-Quelltext auseinander setzen zu müssen.


E-Mail verschicken

Vor dem Aufkommen von E-Mail wurden Nachrichten als Brief oder Telegramm, später als Fernschreiben und Teletex (die letzteren beiden waren immerhin schon digitale Übertragungsverfahren) sowie Telefax übermittelt. Ende der 1980er Jahre begann dann der Siegeszug der E-Mail – sie war eine der ersten Anwendungen, welche die Möglichkeiten des ARPAnets nutzte.

Das Format einer E-Mail wird heute durch den RFC 2822 festgelegt. Danach bestehen E-Mails nur aus Textzeichen (7-Bit-ASCII-Zeichen). E-Mails sind intern in zwei Teile geteilt: Den Header mit Kopfzeilen und den Body mit dem eigentlichen Inhalt der Nachricht. Die Header genannten Kopfzeilen einer E-Mail geben Auskunft über den Weg, den eine E-Mail genommen hat, und bieten Hinweise auf Absender, Empfänger, Datum der Erstellung und Stationen der Übermittlung. Der Body einer E-Mail ist durch eine Leerzeile vom Header getrennt und enthält die zu übertragenden Informationen in einem oder mehreren Teilen.

Eine E-Mail darf gemäß RFC 2822 Abschnitt 2.3 nur Zeichen des 7-Bit-ASCII-Zeichensatzes enthalten. Sollen andere Zeichen, wie zum Beispiel deutsche Umlaute, oder Daten, wie zum Beispiel Bilder, übertragen werden, müssen diese zuvor passend kodiert werden.

E-Mails werden vorwiegend per SMTP über das Internet und in lokalen Netzen übertragen. Das Simple Mail Transfer Protocol ist ein Protokoll der Internetprotokollfamilie und liegt in der Anwendungsschicht eine Ebene über der Transportschicht (TCP). Der verwendete Port ist 25 bzw. bei neueren Servern Port 587.

Das .NET Framework stellt für das Versenden von E-Mails separate Klassen zur Verfügung die im Namespace System.Net.Mail versammelt sind. Die Basisklasse für alle Übertragungen bildet die Klasse SmtpClient. Mit dieser Klasse lassen sich Nachrichten auf bequeme Art und Weise versenden. Im nachfolgenden Beispiel wird eine Nachricht synchron versendet. Dabei gilt es zu beachten das viele SMTP-Server eine Authentifizierung des Nutzers erfordern. Die Authentifizierung kann mittels der Klasse NetworkCredential erfolgen.

using System;
using System.Net.Mail;
 
namespace CodePlanet.Articles.ProgrammingSockets
{
    /// <summary>
    /// Versendet eine E-Mail über SMTP samt Bild als Anhang.
    /// </summary>
    public class SendMail
    {
        public static void Main(string[] args)
        {
            // Daten eingeben
            try {
                MailAddress from = new MailAddress("fromAdress", "Ali Baba");
                MailAddress to = new MailAddress("toAdress", "Forty Thieves");
                MailMessage message = new MailMessage(from, to);
                message.Subject = "Our Subject";
                message.Body = "This is a message from...";
                message.IsBodyHtml = false;
                message.Priority = MailPriority.High;
 
                // Ein Bild als Anhang mitsenden
                Attachment a = new Attachment("photo.jpg",
                    System.Net.Mime.MediaTypeNames.Image.Jpeg);
 
                message.Attachments.Add(a);
 
                // SMTP Server benötigt ggf. eine Authentfizierung!
                SmtpClient emailClient = new SmtpClient("smtp.domain.com", 587);
                System.Net.NetworkCredential SMTPUserInfo =
                    new System.Net.NetworkCredential("User", "Password");
                emailClient.UseDefaultCredentials = false;
                emailClient.Credentials = SMTPUserInfo;
                emailClient.Send(message);
                Console.WriteLine("Message Sent");
            } catch (SmtpException ex) {
                Console.WriteLine(ex.ToString());
            }
        }
    }
}

Sollen viele Nachrichten versendet werden, ist oftmals die synchrone Lösung unzufriedenstellend. Auch in diesem Fall lassen sich mittels Callback asynchrone Operationen durchführen, die ein Blocking der Applikation verhindern. Das Versenden kann sequentiell ebenfalls von einem parallelen Thread ausgeführt werden.

Schluss

Damit haben wir das Ende dieses Tutorials erreicht. Der Bereich Sockets und insbesondere Protokolle würde allein schon ausreichen um ein Buch mit 500 Seiten zu füllen. Dieses Tutorial gibt lediglich einen kleinen Überblick über TCP/IP in der Programmiersprache C#.

Hoffentlich konnte der Artikel Ihnen die .NET Version von Winsock, sowie die wesentlichen Klassen für den Umgang mit dem Internet in C#, ein wenig näher bringen. Im Dateianhang sind neben den im Tutorial aufgeführten Beispielen noch einige weitere Quellcodes zu finden, die Sie sich bequem in Ruhe näher ansehen können. Happy Programming!

Zuletzt aktualisiert am Donnerstag, den 02. Januar 2014 um 23:03 Uhr
 
AUSWAHLMENÜ