26 Gäste und 0 Benutzer online | Anmelden | Registrieren


Designelement Startseite

Designelement Windows-Artikel
  Designelement Problemlösungen
  Designelement Einstellungen
  Designelement Anleitungen
  Designelement Hardware
  Designelement FAQ
  Designelement C#.NET

Designelement Forum
Designelement Gästebuch

Designelement Programme
  Designelement Onlinetools
  Designelement Downloads

Designelement Suche
Designelement Links

Designelement Impressum
Designelement Kontakt

Designelement Anmelden





Zu den C#.NET-Artikeln

  Einfacher Webserver - C#
Am 15.10.2007 verfasst von Andreas Nägeli. Hits: 3607

Ein Webserver kommuniziert über ein Netzwerk (in der Regel das Internet) mit Hilfe des HTTP-Protokolls mit Browsern, die Informationen von diesem Server abrufen möchten. Dieses Tutorial soll Ihnen die Grundlagen des HTTP-Protokolls und der Netzwerkprogrammierung in C# vermitteln.

Ein Socket ist ursprünglich nichts anderes als die Verknüpfung von IP-Adresse eines Rechners und einer Portnummer. Mit Hilfe eines Sockets kann man sich mit anderen Rechnern verbinden (Client) oder selbst Dienste zur Verfügung stellen (Server). Ein Webserver tut nichts anderes, als einen angegebenen Port auf Verbindungsanfragen abzuhören und Clients (Browser) zu bedienen.

In C# existiert bereits die Klasse Socket, die wir hier verwenden möchten. Im Beispielprogramm verwenden wir 3 verschiedene Klassen: Program mit der Main-Funktion, Server zum Annehmen von neuen Verbindungen und Serverhandling zum Behandeln von Anfragen.

In der Serverklasse initialisieren wir zunächst ein neues Socket, indem wir es an alle IP-Adressen des Rechners und einen angegebenen Port binden. Die Klasse selbst besteht nur aus einer Funktion, dem Konstruktor.

In dieser Klasse müssen die folgenden Namespaces eingebunden werden:

using System.Net;
using System.Net.Sockets;
using System.Threading;
public Server(int Port, int Backlog) {
  Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  server.Bind(new IPEndPoint(IPAddress.Any, Port));
  server.Listen(Backlog);
  while (true) {
    Socket sock = server.Accept();
    Serverhandling handling = new Serverhandling(sock);
    Thread clientthread = new Thread(new ThreadStart(handling.Run));
    clientthread.Start();
  }
}


Zunächst erstellen wir ein neues Socket, das auf unsere Bedürfnisse zugeschnitten ist. Wir wählen dazu das IPv4- und das TCP-Protokoll (streamimgbasiert).

Im nächsten Schritt binden wir dieses Socket an eine IP-Adresse und einen Port. Da unser Webserver von jeder IP-Adresse des Rechners aus ansprechbar sein soll (z.B. 192.168.1.3, 127.0.0.1, ...) wählen wir IPAddress.Any. Als Port geben wir den Port an, der uns vom Hauptprogramm vorgegegeben wurde.

Nun müssen wir das Socket nur noch anweisen, die angegebene IP-Adresse abzuhören. Dies geschieht mit dem Befehl Listen.

Die Schleife, die nun folgt, ist charakteristisch für Server. Jeder Durchlauf der Schleife stellt eine neue Verbindung zu einem Client dar. Wenn sich also ein neuer Client verbunden hat, erhalten wir ein neues Socket, das für die Verbindung zum Client steht (sock). Da der Server ständig neue Verbindungen annehmen können muss, behandeln wir die Anfrage des Clients in einem seperaten Thread. Sonst wäre der Server erst wieder ansprechbar, wenn die Verbindung zum aktuellen Client getrennt wurde.

Die Klasse Serverhandling übernimmt die Verarbeitung der Anfrage des Clients. Da diese Klasse mit dem Client kommunizieren muss, erhält sie über den Konstruktor das Socket, das die Verbindung zum Client darstellt.

Sobald der Thread für die Anfrage des Clients ausgeführt wird, ruft die Run-Methode die Daten ab, die der Client übermittelt hat. Dies geschieht über die getContent-Funktion.

private String getContent() {
  String content = "";
  Thread.Sleep(10);
  while (sock.Available > 0) {
    int bytes = sock.Available;
    byte[] buffer = new byte[bytes];
    sock.Receive(buffer, bytes, SocketFlags.None);
    content += Encoding.UTF8.GetString(buffer);
    Thread.Sleep(10);
  }
  return content;
}


Zunächst warten wir 10 Millisekunden um dem Client genügend Zeit zu geben, Daten zu senden (zusätzlich zu der Verwaltungszeit vorher). Dann folgt eine while-Schleife, die solange ausgeführt wird bis keine Daten mehr aus dem Stream gelesen werden können. In der while-Schleife merken wir uns zunächst die Anzahl an Bytes, die wir auslesen möchten (da sich dieser Wert bis zur nächsten Zeile ändern könnte) und initialisieren ein neues Byte-Array dieser Größe. Danach lesen wir über die Receive-Methode diese Anzahl an Bytes in das Array und fügen die Daten an den Gesamtstring an. Nun warten wir wieder 10 Millisekunden und iterieren dann das Ganze.

Sind keine Daten mehr verfügbar, geben wir den Textstring zurück. An dieser Stelle kommt nun das HTTP-Protokoll zur Kommunikation des Webservers mit dem Client ins Spiel. Wir möchten realisieren, dass Dateien in einem bestimmten Verzeichnis vom Client angefragt werden können. Dazu müssen wir etwas mehr über den Aufbau des HTTP-Protokolls wissen.

Die Anfrage des Clients beginnt in der Regel mit einem GET-Befehl, d.h. der Client möchte die angegebene Datei abrufen. Andere Befehle wie den HEAD-Befehl möchten wir hier nicht implementieren, da dies nur ein einfaches Demonstrationsbeispiel ist.

Ein Request eines Clients könnte zum Beispiel so aussehen:

GET /test.html HTTP/1.1
Host: 192.168.1.3
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.8.1.7) Gecko/20070914 Firefox/2.0.0.7
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: de-de,de;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive


Alles, was wir für unseren Zweck tun müssten, wäre den Dateinamen auszulesen und die gewünschte Datei zu senden. Dazu zerlegen wir die Anfrage des Clients zunächst in die einzelnen Zeilen und dann die erste Zeile in Wörter (GET, /test.html und HTTP/1.1).

public void Run() {
  String request = getContent();
  try {
    String[] lines = request.Split(Environment.NewLine.ToCharArray());
    String[] words = lines[0].Split(" ".ToCharArray());
    if (words[0] == "GET") {
      int pos = words[1].LastIndexOf("/");
      if (pos > -1) {
        if (words[1] != "/" && words[1].Length > pos) {
          String filename = words[1].Substring(pos + 1);
          if (File.Exists(Application.StartupPath + "\\files\\" + filename)) {
            sendFile(filename);
          }
          else {
            sendResponse("404 NOT FOUND", "Datei nicht gefunden");
          }
        }
        else {
          if (File.Exists(Application.StartupPath + "\\files\\index.html"))
            sendFile("index.html");
          else
            sendResponse("200 OK", "Index");
        }
      }
      else {
        sendResponse("400 BAD REQUEST", "Protokollfehler");
      }
    }
    else {
      sendResponse("400 BAD REQUEST", "Nicht unterstützt");
    }
  }
  catch {
    sendResponse("400 BAD REQUEST", "Protokollfehler");
  }
  sock.Close();
}


Zunächst zerlegen wir im Try-Block die Anfrage in Zeilen und anschließend in Wörter. Tritt hier bereits ein Fehler auf, folgt die Anfrage wohl nicht dem HTTP-Protokoll und wird somit mit einem "400 BAD REQUEST" beantwortet.

Im nächsten Schritt prüfen wir, ob das erste Wort der ersten Zeile "GET" ist. Wenn ja, möchte der Client eine Datei abrufen, wenn nein, dann möchte der Browser eine Funktion des Protokolls verwenden, die wir nicht implementieren. Dementsprechend geben wir dann eine Fehlermeldung zurück.

Nun suchen wir das letzte Vorkommen des Zeichens "/" im Dateistring der Anfrage. Grund hierfür ist, dass wir keine Unterordner unterstützen und uns damit nur der letzte Teil des Dateistrings interessiert. "/test/test.html" ist für uns das Gleiche wie "/test.html".

Besteht der Dateistring nur aus einem einzelnen "/", so wurde der Index des Servers aufgerufen. Existiert eine Datei namens "index.html" im Files-Verzeichnis, so senden wir diese, ansonsten geben wir nur "Index" aus.

Haben wir nun den Dateinamen erfolgreich extrahiert, so prüfen wir zunächst, ob sie im Unterverzeichnis "files" existiert. Wenn nicht, geben wir eine "404 NOT FOUND" Meldung zurück. Existiert sie, so senden wir die Datei mit Hilfe der sendFile-Methode zum Client.

Im Folgenden möchte ich nun noch auf die Funktionen sendFile und sendResponse eingehen, die das HTTP-Protokoll auf Serverseite implementieren.

Möchten wir nur einen Text zum Client senden, verwenden wir die Methode sendResponse, die eine Statusmeldung und einen zu sendenden Text als Übergabeparameter erwartet. Eine HTTP-Antwort hat in der Regel diese Form:

HTTP/1.1 Statuscode Statusmeldung
...
Content-Type: Art des Inhalts der Datei
Content-Lenght: Länge der Datei in Bytes

Dateiinhalt


Für Statuscode und Statusmeldungen gibt es Listen, die bestimmte Ereignise definieren. Für uns sind diese von Relevanz:

200 OK
404 NOT FOUND
400 BAD REQUEST

Beginnt der Responsecode mit einer 4, so deutet dies auf einen Fehler hin. 404 sagt aus, dass die Datei nicht gefunden wurde, 400 bemängelt die Anfrage des Clients und 200 meldet, dass alles in Ordnung ist.

Für Content-Type sind verschiedene Angaben vorgesehen. Zum Beispiel:

text/html - Eine Datei zur Anzeige im Browser
image/png - Ein (PNG-)Bild
application/force-download - Eine Datei zum Herunterladen

Wir implementieren in unserer Beispielanwendung nur den ersten Typ. Nach der Angabe der Länge der Datei in Bytes muss zwingend eine Leerzeile und danach der Dateiinhalt gesendet werden.

Für die sendResponse-Methode müssen wir keine Datei auslesen. Es reicht, wenn wir die zwei übergebenen Parameter richtig einsetzen und auswerten:

private void sendResponse(String status, String tosend) {
  LogConsole(status);
  sendText("HTTP/1.1 " + status);
  sendText("Server: CLWebserver 1.00");
  sendText("Connection: close");
  sendText("Content-Type: text/html");
  sendText("Content-Length: " + (tosend.Length + Environment.NewLine.Length));
  sendText("");
  sendText(tosend);
}


Die LogConsole-Methode sorgt dafür, dass auf der Konsole eine entsprechende Statusmeldung ausgegeben wird. Über die sendText-Funktion können wir Text über das Socket zum Client senden (wobei eine Leerzeile angehängt wird).
Dabei muss die Länge der letzten Leerzeile berücksichtigt werden, die an tosend angehängt wird.

private void sendText(String text) {
  sock.Send(Encoding.UTF8.GetBytes(text + Environment.NewLine));
}


Zu beachten ist bei dieser Methode, dass wir die zu senden Bytes zunächst in UTF8 kodieren.

Die LogConsole-Methode soll neben der Statusmeldung zusätzlich die IP-Adresse des Clients ausgeben. Dies geschieht über den RemoteEndpoint des Sockets, das unsere Verbindung darstellt:

private void LogConsole(String Message) {
  Console.WriteLine(((IPEndPoint)sock.RemoteEndPoint).Address.ToString() + " - " + Message);
}


Kommen wir nun zum Herzstück der Serverhandlingklasse, der sendFile-Methode.

private void sendFile(String filename) {
  FileStream stream = new FileStream(Application.StartupPath + "\\files\\" + filename, FileMode.Open);
  LogConsole("200 OK [ " + filename + " ]");
  sendText("HTTP/1.1 200 OK");
  sendText("Server: CLWebserver 1.00");
  sendText("Connection: close");
  sendText("Content-Type: text/html");
  sendText("Content-Length: " + stream.Length);
  sendText("");
  long read = 0;
  while (read < stream.Length) {
    int buffersize;
    if (stream.Length - read > 1024) {
      buffersize = 1024;
      read += 1024;
    }
    else {
      buffersize = (int)(stream.Length - read);
      read = stream.Length;
    }
    byte[] tosend = new byte[buffersize];
    stream.Read(tosend, 0, buffersize);
    sock.Send(tosend);
    Thread.Sleep(1);
  }
  stream.Close();
}


Zunächst geben wir den Statuscode auf der Konsole aus und öffnen die Datei in Form eines FileStreams. Nun können wir mit den gegebenen Informationen den Header der HTTP-Antwort generieren.

Um den Sendevorgang dynamischer zu gestalten senden wir immer nur Daten bis maximal 1024 Byte Länge. Würden wir zum Beispiel eine 10 MB große Datei an einem Stück senden, wäre dies sehr Ressourcen belastend.

Wir merken uns nun also, wieviel Bytes wir bereits gesendet haben (mit der Variable read) und setzen unsere Buffergröße auf 1024 Bytes, wenn noch genug Zeichen verfügbar sind, sonst auf die Anzahl der noch verfügbaren Bytes. Daraus folgt, dass sich read immer der Abbruchbedingung der while-Schleife annähern muss.

Zu beachten ist, dass die Länge der Datei in einer Int64-Integer gespeichert ist, für die Buffergröße aber nur eine Int32-Integer zur Verfügung steht. Wir können den Wert allerdings getrost auf Int32 casten, da die Differenz zwischen der Länge des Streams und der read-Variable an dieser Stelle maximal 1024 Byte betragen kann.

Wir warten nach jedem Schleifendurchlauf eine Millisekunde, um anderen Threads Ausführungszeit zu geben bzw. die CPU-Auslastung des Threads gering zu halten. Würden wir diese Pause weglassen, würde bei einer 10 MB großen Datei eine Auslastung von 100% die Folge sein. Die Pause kann bei Bedarf auch höher ausfallen, da das Netzwerk in der Regel sowieso deutlich langsamer als die CPU beziehungsweise die Festplatte ist.

Abschließend gilt es zu sagen, dass dieses Projekt nur als eine Demonstration für die absoluten Grundlagen des HTTP-Protokolls beziehungsweise der Socketprogrammierung gelten kann. Dieser Server verwendet nur einen Bruchteil des HTTP-Protokolls und implementiert damit das Protokoll nicht!

Wichtig für einen echten Webserver ist auch, dass dieser sehr robust programmiert werden muss, da eine lange fehlerfreie Laufzeit erwünscht ist. Unser Beispielserver könnte zum Beispiel über ein endlos langes Anfragerequest leicht angegriffen werden.

Bei Fragen zu diesem Artikel können Sie sich gerne ans Forum wenden.

Kommentiertes Codebeispiel herunterladen (CLWebserver.rar, 27 KB, VS80)

Bewertung dieses Artikels von 33 Benutzern: Mit 10 von 10 Punkten bewertet - 9.94 / 10 Punkte

Wie finden Sie diesen Artikel?











  2002 - 2008 Designelement Computerleben.net Designelement Sitemap